|
| 1 | +"""Operation for loading skill metadata. |
| 2 | +
|
| 3 | +This module provides the LoadSkillMetadataOp class which scans the skills |
| 4 | +directory and extracts metadata (name and description) from all SKILL.md files. |
| 5 | +""" |
| 6 | + |
| 7 | +from pathlib import Path |
| 8 | + |
| 9 | +from loguru import logger |
| 10 | + |
| 11 | +from ...core.context import C |
| 12 | +from ...core.op import BaseAsyncToolOp |
| 13 | +from ...core.schema import ToolCall |
| 14 | + |
| 15 | + |
| 16 | +@C.register_op() |
| 17 | +class LoadSkillMetadataOp(BaseAsyncToolOp): |
| 18 | + """Operation for loading metadata from all available skills. |
| 19 | +
|
| 20 | + This tool scans the skills directory for SKILL.md files and extracts |
| 21 | + their metadata (name and description) from YAML frontmatter. |
| 22 | + Returns a JSON string containing a list of skill metadata. |
| 23 | + """ |
| 24 | + |
| 25 | + def build_tool_call(self) -> ToolCall: |
| 26 | + """Build the tool call definition for load_skill_metadata. |
| 27 | +
|
| 28 | + Returns: |
| 29 | + A ToolCall object defining the load_skill_metadata tool. |
| 30 | + """ |
| 31 | + tool_params = { |
| 32 | + "name": "load_skill_metadata", |
| 33 | + "description": "Load metadata (name and description) for all available skills from the skills directory.", |
| 34 | + "input_schema": {}, |
| 35 | + } |
| 36 | + return ToolCall(**tool_params) |
| 37 | + |
| 38 | + @staticmethod |
| 39 | + async def parse_skill_metadata(content: str, path: str) -> dict[str, str] | None: |
| 40 | + """Extract skill metadata (name and description) from SKILL.md content. |
| 41 | +
|
| 42 | + Parses YAML frontmatter from SKILL.md files to extract the skill name |
| 43 | + and description. The frontmatter should be in the format: |
| 44 | + --- |
| 45 | + name: skill_name |
| 46 | + description: skill description |
| 47 | + --- |
| 48 | +
|
| 49 | + Args: |
| 50 | + content: The content of the SKILL.md file. |
| 51 | + path: The file path (used for logging purposes). |
| 52 | +
|
| 53 | + Returns: |
| 54 | + A dictionary with 'name' and 'description' keys, or None if |
| 55 | + parsing fails or required fields are missing. |
| 56 | + """ |
| 57 | + parts = content.split("---") |
| 58 | + if len(parts) < 3: |
| 59 | + logger.warning(f"No YAML frontmatter found in skill from {path}") |
| 60 | + return None |
| 61 | + |
| 62 | + frontmatter_text = parts[1].strip() |
| 63 | + name = None |
| 64 | + description = None |
| 65 | + |
| 66 | + for line in frontmatter_text.split("\n"): |
| 67 | + line = line.strip() |
| 68 | + if line.startswith("name:"): |
| 69 | + name = line.split(":", 1)[1].strip().strip("\"'") |
| 70 | + elif line.startswith("description:"): |
| 71 | + description = line.split(":", 1)[1].strip().strip("\"'") |
| 72 | + |
| 73 | + if not name or not description: |
| 74 | + logger.warning(f"Missing name or description in skill from {path}") |
| 75 | + return None |
| 76 | + |
| 77 | + return { |
| 78 | + "name": name, |
| 79 | + "description": description, |
| 80 | + } |
| 81 | + |
| 82 | + async def async_execute(self): |
| 83 | + """Execute the load skill metadata operation. |
| 84 | +
|
| 85 | + Scans the skills directory recursively for all SKILL.md files, |
| 86 | + extracts their metadata, and returns a JSON string containing |
| 87 | + a list of all skill metadata entries. |
| 88 | + """ |
| 89 | + skill_dir = Path(self.context.skill_dir) |
| 90 | + logger.info(f"🔧 Tool called: load_skill_metadata(path={skill_dir})") |
| 91 | + skill_files = list(skill_dir.rglob("SKILL.md")) |
| 92 | + |
| 93 | + skill_metadata_dict = {} |
| 94 | + for skill_file in skill_files: |
| 95 | + content = skill_file.read_text(encoding="utf-8") |
| 96 | + metadata = await self.parse_skill_metadata(content, str(skill_file)) |
| 97 | + if metadata: |
| 98 | + skill_dir = skill_file.parent.as_posix() |
| 99 | + skill_metadata_dict[metadata["name"]] = { |
| 100 | + "description": metadata["description"], |
| 101 | + "skill_dir": skill_dir, |
| 102 | + } |
| 103 | + logger.info(f"✅ Loaded skill {metadata['name']} metadata skill_dir={skill_dir}") |
| 104 | + |
| 105 | + logger.info(f"✅ Loaded {len(skill_metadata_dict)} skill metadata entries") |
| 106 | + self.set_output(skill_metadata_dict) |
0 commit comments