Skill loader
| 45 | |
| 46 | |
| 47 | class SkillLoader: |
| 48 | """Skill loader""" |
| 49 | |
| 50 | def __init__(self, skills_dir: str = "./skills"): |
| 51 | """ |
| 52 | Initialize Skill Loader |
| 53 | |
| 54 | Args: |
| 55 | skills_dir: Skills directory path |
| 56 | """ |
| 57 | self.skills_dir = Path(skills_dir) |
| 58 | self.loaded_skills: Dict[str, Skill] = {} |
| 59 | |
| 60 | def load_skill(self, skill_path: Path) -> Optional[Skill]: |
| 61 | """ |
| 62 | Load single skill from SKILL.md file |
| 63 | |
| 64 | Args: |
| 65 | skill_path: SKILL.md file path |
| 66 | |
| 67 | Returns: |
| 68 | Skill object, or None if loading fails |
| 69 | """ |
| 70 | try: |
| 71 | content = skill_path.read_text(encoding="utf-8") |
| 72 | |
| 73 | # Parse YAML frontmatter |
| 74 | frontmatter_match = re.match(r"^---\n(.*?)\n---\n(.*)$", content, re.DOTALL) |
| 75 | |
| 76 | if not frontmatter_match: |
| 77 | print(f"⚠️ {skill_path} missing YAML frontmatter") |
| 78 | return None |
| 79 | |
| 80 | frontmatter_text = frontmatter_match.group(1) |
| 81 | skill_content = frontmatter_match.group(2).strip() |
| 82 | |
| 83 | # Parse YAML |
| 84 | try: |
| 85 | frontmatter = yaml.safe_load(frontmatter_text) |
| 86 | except yaml.YAMLError as e: |
| 87 | print(f"❌ Failed to parse YAML frontmatter: {e}") |
| 88 | return None |
| 89 | |
| 90 | # Required fields |
| 91 | if "name" not in frontmatter or "description" not in frontmatter: |
| 92 | print(f"⚠️ {skill_path} missing required fields (name or description)") |
| 93 | return None |
| 94 | |
| 95 | # Get skill directory (parent of SKILL.md) |
| 96 | skill_dir = skill_path.parent |
| 97 | |
| 98 | # Replace relative paths in content with absolute paths |
| 99 | # This ensures scripts and resources can be found from any working directory |
| 100 | processed_content = self._process_skill_paths(skill_content, skill_dir) |
| 101 | |
| 102 | # Create Skill object |
| 103 | skill = Skill( |
| 104 | name=frontmatter["name"], |
no outgoing calls