Spaces:
Building
Building
File size: 6,712 Bytes
81aa0b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | """Skills system β load markdown skill files at runtime.
Inspired by Claude Code's Skill system. Each skill is a directory with:
- SKILL.md: the skill instructions (markdown with YAML frontmatter)
- references/ (optional): supporting docs
- scripts/ (optional): helper scripts
Skills are discovered under code/skills/builtins/ and can also be loaded
from the workspace's .sonicoder/skills/ directory.
"""
from __future__ import annotations
import logging
import os
import re
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
# βββ Skill discovery roots ββββββββββββββββββββββββββββββββββββββββββββββ
_BUILTIN_SKILLS_DIR = os.path.join(os.path.dirname(__file__), "builtins")
_USER_SKILLS_DIRNAME = ".sonicoder/skills" # relative to workspace root
def _skill_dirs() -> list[str]:
"""Return all directories to search for skills."""
dirs = [_BUILTIN_SKILLS_DIR]
# Add user skills dir from workspace
try:
from code.tools.fs import get_workspace_root
user_dir = os.path.join(get_workspace_root(), _USER_SKILLS_DIRNAME)
if os.path.isdir(user_dir):
dirs.append(user_dir)
except Exception:
pass
return dirs
# βββ YAML frontmatter parsing βββββββββββββββββββββββββββββββββββββββββββ
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL)
def _parse_frontmatter(content: str) -> tuple[dict[str, str], str]:
"""Parse YAML frontmatter from markdown. Returns (metadata, body)."""
match = _FRONTMATTER_RE.match(content)
if not match:
return {}, content
raw_yaml = match.group(1)
body = match.group(2)
# Very simple YAML parser (key: value pairs only)
meta: dict[str, str] = {}
for line in raw_yaml.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if ":" in line:
key, _, value = line.partition(":")
meta[key.strip()] = value.strip().strip("\"'")
return meta, body
# βββ Skill loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _load_skill(skill_dir: str) -> dict[str, Any] | None:
"""Load a single skill from a directory."""
skill_md = os.path.join(skill_dir, "SKILL.md")
if not os.path.isfile(skill_md):
return None
try:
with open(skill_md, "r", encoding="utf-8") as f:
content = f.read()
except Exception as exc:
logger.warning("Failed to read %s: %s", skill_md, exc)
return None
meta, body = _parse_frontmatter(content)
# Load any reference files
references: dict[str, str] = {}
refs_dir = os.path.join(skill_dir, "references")
if os.path.isdir(refs_dir):
for fname in os.listdir(refs_dir):
if fname.endswith((".md", ".txt")):
try:
with open(os.path.join(refs_dir, fname), "r", encoding="utf-8") as f:
references[fname] = f.read()
except Exception:
pass
return {
"name": meta.get("name", os.path.basename(skill_dir)),
"description": meta.get("description", ""),
"language": meta.get("language", ""),
"tags": [t.strip() for t in meta.get("tags", "").split(",") if t.strip()],
"body": body.strip(),
"references": references,
"path": skill_dir,
}
def list_skills() -> list[dict[str, Any]]:
"""List all available skills (metadata only, no body)."""
skills: list[dict[str, Any]] = []
seen_names: set[str] = set()
for skills_dir in _skill_dirs():
if not os.path.isdir(skills_dir):
continue
for entry in sorted(os.listdir(skills_dir)):
entry_path = os.path.join(skills_dir, entry)
if not os.path.isdir(entry_path):
continue
skill = _load_skill(entry_path)
if skill and skill["name"] not in seen_names:
seen_names.add(skill["name"])
skills.append({
"name": skill["name"],
"description": skill["description"],
"language": skill["language"],
"tags": skill["tags"],
})
return skills
def get_skill(name: str) -> dict[str, Any] | None:
"""Get full skill content by name."""
for skills_dir in _skill_dirs():
if not os.path.isdir(skills_dir):
continue
# Try directory match
for entry in os.listdir(skills_dir):
entry_path = os.path.join(skills_dir, entry)
if not os.path.isdir(entry_path):
continue
skill = _load_skill(entry_path)
if skill and skill["name"] == name:
return skill
return None
def invoke_skill(name: str) -> dict[str, Any]:
"""Invoke a skill by name β returns its full body and references."""
skill = get_skill(name)
if not skill:
return {
"success": False,
"error": f"Skill not found: {name}",
"available": [s["name"] for s in list_skills()],
}
return {
"success": True,
"name": skill["name"],
"description": skill["description"],
"body": skill["body"],
"references": skill["references"],
}
def build_skills_context(skill_names: list[str] | None = None) -> str:
"""Build a context string with skill bodies to inject into the prompt.
If skill_names is None, includes all skills (brief listing only).
"""
if not skill_names:
# List all skills briefly
skills = list_skills()
if not skills:
return ""
lines = ["Available skills (use /skill <name> to load full instructions):"]
for s in skills:
desc = s["description"][:120]
lines.append(f"- {s['name']}: {desc}")
return "\n".join(lines)
# Load full bodies for requested skills
parts: list[str] = []
for name in skill_names:
skill = get_skill(name)
if skill:
parts.append(f"# Skill: {skill['name']}\n\n{skill['body']}")
for ref_name, ref_body in skill["references"].items():
parts.append(f"\n## Reference: {ref_name}\n\n{ref_body}")
else:
parts.append(f"# Skill: {name}\n\n(Skill not found)")
return "\n\n---\n\n".join(parts)
|