"""Skill loader for SKILL.md files."""
import logging
from pathlib import Path
import yaml
from .skill import Skill
logger = logging.getLogger(__name__)
class SkillLoader:
"""Load skills from SKILL.md files."""
def __init__(self, skills_dir: Path | str | None = None):
"""Initialize skill loader.
Args:
skills_dir: Directory containing skill folders.
Defaults to ../../../skills relative to this file.
"""
if skills_dir is None:
# Default to v2/../skills (original skills directory)
skills_dir = Path(__file__).parent.parent.parent.parent.parent / "skills"
self.skills_dir = Path(skills_dir)
def list_skills(self) -> list[str]:
"""List available skill IDs."""
skills = []
if not self.skills_dir.exists():
logger.warning(f"Skills directory not found: {self.skills_dir}")
return skills
for path in self.skills_dir.iterdir():
if path.is_dir() and (path / "SKILL.md").exists():
skills.append(path.name)
return skills
def load(self, skill_id: str) -> Skill:
"""Load a skill by ID.
Args:
skill_id: Skill directory name
Returns:
Loaded Skill object
"""
skill_dir = self.skills_dir / skill_id
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
raise FileNotFoundError(f"Skill not found: {skill_id}")
content = skill_file.read_text(encoding="utf-8")
# Parse YAML frontmatter
frontmatter, body = self._parse_frontmatter(content)
# Build skill
skill = Skill(
id=frontmatter.get("id", skill_id),
name=frontmatter.get("name", skill_id),
display_name=frontmatter.get("display_name", skill_id),
description=frontmatter.get("description", ""),
system_prompt=body.strip(),
emoji=frontmatter.get("emoji", ""),
voice_file=self._resolve_voice(skill_dir, frontmatter.get("voice")),
avatar_file=self._resolve_path(skill_dir, frontmatter.get("avatar")),
initial_memories=frontmatter.get("initial_memories", []),
metadata=frontmatter.get("metadata", {}),
personality_traits=frontmatter.get("personality_traits", []),
speech_patterns=frontmatter.get("speech_patterns", []),
allowed_tools=frontmatter.get("allowed_tools", []),
output_filters=frontmatter.get("output_filters", []),
input_validation=frontmatter.get("input_validation", {}),
)
logger.info(f"Loaded skill: {skill.id} ({skill.display_name})")
return skill
def _parse_frontmatter(self, content: str) -> tuple[dict, str]:
"""Parse YAML frontmatter from markdown content.
Args:
content: Full file content
Returns:
Tuple of (frontmatter dict, body content)
"""
if not content.startswith("---"):
return {}, content
# Find end of frontmatter
end = content.find("---", 3)
if end == -1:
return {}, content
frontmatter_str = content[3:end].strip()
body = content[end + 3 :].strip()
try:
frontmatter = yaml.safe_load(frontmatter_str) or {}
except yaml.YAMLError as e:
logger.warning(f"Failed to parse frontmatter: {e}")
frontmatter = {}
return frontmatter, body
def _resolve_voice(self, skill_dir: Path, voice: str | None) -> str | None:
"""Resolve voice file path."""
if not voice:
# Check for default reference.wav
default = skill_dir / "reference.wav"
if default.exists():
return str(default)
return None
return self._resolve_path(skill_dir, voice)
def _resolve_path(self, skill_dir: Path, filename: str | None) -> str | None:
"""Resolve a file path relative to skill directory."""
if not filename:
return None
path = skill_dir / filename
if path.exists():
return str(path)
return None