@@ -514,6 +514,110 @@ def _generate_skill_file(self, package_info, primitives: dict, skill_dir: Path)
514514
515515 return files_copied
516516
517+ def _integrate_native_claude_skill (
518+ self , package_info , project_root : Path , source_skill_md : Path
519+ ) -> SkillIntegrationResult :
520+ """Copy a native Claude Skill (with existing SKILL.md) to .claude/skills/.
521+
522+ For packages that already have a SKILL.md at their root (like those from
523+ awesome-claude-skills), we copy the entire skill folder rather than
524+ regenerating from .apm/ primitives.
525+
526+ The skill folder name is the source folder name (e.g., `mcp-builder`).
527+
528+ We add APM tracking metadata to the copied SKILL.md so sync_integration
529+ can properly identify and clean up orphaned skills.
530+
531+ Args:
532+ package_info: PackageInfo object with package metadata
533+ project_root: Root directory of the project
534+ source_skill_md: Path to the source SKILL.md file
535+
536+ Returns:
537+ SkillIntegrationResult: Results of the integration operation
538+ """
539+ package_path = package_info .install_path
540+
541+ # Use the source folder name as the skill name
542+ # e.g., apm_modules/ComposioHQ/awesome-claude-skills/mcp-builder → mcp-builder
543+ skill_name = package_path .name
544+
545+ skill_dir = project_root / ".claude" / "skills" / skill_name
546+ target_skill_md = skill_dir / "SKILL.md"
547+
548+ # Check if we need to update
549+ skill_created = False
550+ skill_updated = False
551+ skill_skipped = False
552+
553+ if target_skill_md .exists ():
554+ # Check existing metadata for version/commit changes
555+ existing_metadata = self ._parse_skill_metadata (target_skill_md )
556+ current_version = package_info .package .version
557+ current_commit = (
558+ package_info .resolved_reference .resolved_commit
559+ if package_info .resolved_reference
560+ else "unknown"
561+ )
562+
563+ if (existing_metadata .get ('Version' ) == current_version and
564+ existing_metadata .get ('Commit' ) == current_commit ):
565+ skill_skipped = True
566+ else :
567+ skill_updated = True
568+ else :
569+ skill_created = True
570+
571+ files_copied = 0
572+
573+ if skill_created or skill_updated :
574+ # Remove existing skill directory if updating
575+ if skill_dir .exists ():
576+ shutil .rmtree (skill_dir )
577+
578+ # Copy the entire package directory to the skill location
579+ shutil .copytree (package_path , skill_dir )
580+
581+ # Add APM tracking metadata to the SKILL.md for orphan detection
582+ self ._add_apm_metadata_to_skill (target_skill_md , package_info )
583+
584+ # Count files copied
585+ files_copied = sum (1 for _ in skill_dir .rglob ('*' ) if _ .is_file ())
586+
587+ return SkillIntegrationResult (
588+ skill_created = skill_created ,
589+ skill_updated = skill_updated ,
590+ skill_skipped = skill_skipped ,
591+ skill_path = target_skill_md if (skill_created or skill_updated ) else None ,
592+ references_copied = files_copied ,
593+ links_resolved = 0
594+ )
595+
596+ def _add_apm_metadata_to_skill (self , skill_path : Path , package_info ) -> None :
597+ """Add APM tracking metadata to a SKILL.md file.
598+
599+ This ensures sync_integration can identify APM-managed skills for cleanup.
600+ """
601+ post = frontmatter .load (skill_path )
602+
603+ # Add nested metadata for APM tracking
604+ if 'metadata' not in post .metadata :
605+ post .metadata ['metadata' ] = {}
606+
607+ post .metadata ['metadata' ]['apm_package' ] = package_info .get_canonical_dependency_string ()
608+ post .metadata ['metadata' ]['apm_version' ] = package_info .package .version
609+ post .metadata ['metadata' ]['apm_commit' ] = (
610+ package_info .resolved_reference .resolved_commit
611+ if package_info .resolved_reference
612+ else "unknown"
613+ )
614+ post .metadata ['metadata' ]['apm_installed_at' ] = (
615+ package_info .installed_at or datetime .now ().isoformat ()
616+ )
617+
618+ with open (skill_path , 'w' , encoding = 'utf-8' ) as f :
619+ f .write (frontmatter .dumps (post ))
620+
517621 def integrate_package_skill (self , package_info , project_root : Path ) -> SkillIntegrationResult :
518622 """Generate SKILL.md for a package in .claude/skills/ directory.
519623
@@ -554,7 +658,12 @@ def integrate_package_skill(self, package_info, project_root: Path) -> SkillInte
554658
555659 package_path = package_info .install_path
556660
557- # Discover all primitives
661+ # Check if this is a native Claude Skill (already has SKILL.md at root)
662+ source_skill_md = package_path / "SKILL.md"
663+ if source_skill_md .exists ():
664+ return self ._integrate_native_claude_skill (package_info , project_root , source_skill_md )
665+
666+ # Discover all primitives for APM packages without SKILL.md
558667 instruction_files = self .find_instruction_files (package_path )
559668 agent_files = self .find_agent_files (package_path )
560669 context_files = self .find_context_files (package_path )
@@ -582,11 +691,9 @@ def integrate_package_skill(self, package_info, project_root: Path) -> SkillInte
582691 )
583692
584693 # Determine target paths - write to .claude/skills/{skill-name}/
585- # Use skill_name_from_dependency for unique, consistent names:
586- # - owner/repo → owner-repo
587- # - owner/repo/skill → owner-repo-skill
588- canonical_dep = package_info .get_canonical_dependency_string ()
589- skill_name = skill_name_from_dependency (canonical_dep )
694+ # Use the install folder name for simplicity and consistency
695+ # e.g., apm_modules/danielmeppiel/design-guidelines → design-guidelines
696+ skill_name = package_path .name
590697 skill_dir = project_root / ".claude" / "skills" / skill_name
591698 skill_dir .mkdir (parents = True , exist_ok = True )
592699
@@ -632,6 +739,7 @@ def sync_integration(self, apm_package, project_root: Path) -> Dict[str, int]:
632739 """Sync .claude/skills/ with currently installed packages.
633740
634741 Removes skill directories for packages that are no longer installed.
742+ Uses apm_package metadata in SKILL.md to identify APM-managed skills.
635743
636744 Args:
637745 apm_package: APMPackage with current dependencies
@@ -644,27 +752,24 @@ def sync_integration(self, apm_package, project_root: Path) -> Dict[str, int]:
644752 if not skills_dir .exists ():
645753 return {'files_removed' : 0 , 'errors' : 0 }
646754
647- # Get list of currently installed package skill names
648- # Use skill_name_from_dependency to match how integrate_package_skill names them
649- installed_skill_names = set ()
755+ # Get canonical dependency strings for all installed packages
756+ installed_packages = set ()
650757 for dep in apm_package .get_apm_dependencies ():
651- skill_name = skill_name_from_dependency (dep .get_canonical_dependency_string ())
652- installed_skill_names .add (skill_name )
758+ installed_packages .add (dep .get_canonical_dependency_string ())
653759
654- # Find orphaned skill directories (those with -apm suffix pattern or all if needed)
760+ # Find orphaned skill directories by checking apm_package metadata
655761 files_removed = 0
656762 errors = 0
657763
658764 for skill_subdir in skills_dir .iterdir ():
659765 if skill_subdir .is_dir ():
660- # Check if this skill came from APM (has apm_package in metadata)
661766 skill_md = skill_subdir / "SKILL.md"
662767 if skill_md .exists ():
663768 try :
664769 metadata = self ._parse_skill_metadata (skill_md )
665- if metadata .get ('Package' ): # Has APM metadata
666- skill_name = skill_subdir . name
667- if skill_name not in installed_skill_names :
770+ apm_package_ref = metadata .get ('Package' )
771+ if apm_package_ref : # This is an APM-managed skill
772+ if apm_package_ref not in installed_packages :
668773 # Remove orphaned skill directory
669774 shutil .rmtree (skill_subdir )
670775 files_removed += 1
0 commit comments