Spaces:
Running
Running
feat(agent): add Claude Code-style agent, skills, slash-commands, hooks, todos, sandboxed workspace, and full-stack scaffolding
81aa0b5 verified | """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) | |