sonicoder / code /commands /__init__.py
R-Kentaren's picture
feat(agent): add Claude Code-style agent, skills, slash-commands, hooks, todos, sandboxed workspace, and full-stack scaffolding
81aa0b5 verified
Raw
History Blame Contribute Delete
4.77 kB
"""Slash commands system — Claude Code-style.
Commands are markdown files with YAML frontmatter that define
prompt templates triggered by `/command` syntax.
Built-in commands live in code/commands/builtins/.
User commands live in workspace's .sonicoder/commands/.
"""
from __future__ import annotations
import logging
import os
import re
from typing import Any
from code.skills import _parse_frontmatter
logger = logging.getLogger(__name__)
_BUILTIN_COMMANDS_DIR = os.path.join(os.path.dirname(__file__), "builtins")
_USER_COMMANDS_DIRNAME = ".sonicoder/commands"
def _command_dirs() -> list[str]:
dirs = [_BUILTIN_COMMANDS_DIR]
try:
from code.tools.fs import get_workspace_root
user_dir = os.path.join(get_workspace_root(), _USER_COMMANDS_DIRNAME)
if os.path.isdir(user_dir):
dirs.append(user_dir)
except Exception:
pass
return dirs
def _load_command(filepath: str) -> dict[str, Any] | None:
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
except Exception as exc:
logger.warning("Failed to read %s: %s", filepath, exc)
return None
meta, body = _parse_frontmatter(content)
name = meta.get("name") or os.path.splitext(os.path.basename(filepath))[0]
return {
"name": name,
"description": meta.get("description", ""),
"argument_hint": meta.get("argument-hint", ""),
"allowed_tools": [t.strip() for t in meta.get("allowed-tools", "").split(",") if t.strip()],
"body": body.strip(),
"path": filepath,
}
def list_commands() -> list[dict[str, Any]]:
"""List all available slash commands."""
commands: list[dict[str, Any]] = []
seen: set[str] = set()
for cmds_dir in _command_dirs():
if not os.path.isdir(cmds_dir):
continue
for entry in sorted(os.listdir(cmds_dir)):
if not entry.endswith(".md"):
continue
filepath = os.path.join(cmds_dir, entry)
cmd = _load_command(filepath)
if cmd and cmd["name"] not in seen:
seen.add(cmd["name"])
commands.append({
"name": cmd["name"],
"description": cmd["description"],
"argument_hint": cmd["argument_hint"],
})
return commands
def get_command(name: str) -> dict[str, Any] | None:
"""Get full command content by name."""
for cmds_dir in _command_dirs():
if not os.path.isdir(cmds_dir):
continue
# Try name.md and name/something.md
direct = os.path.join(cmds_dir, f"{name}.md")
if os.path.isfile(direct):
return _load_command(direct)
# Try subdirectory: name/command.md
if os.path.isdir(os.path.join(cmds_dir, name)):
for entry in os.listdir(os.path.join(cmds_dir, name)):
if entry.endswith(".md"):
return _load_command(os.path.join(cmds_dir, name, entry))
return None
def parse_command_input(user_input: str) -> tuple[str | None, str]:
"""Parse a user input string for a slash command.
Returns (command_name, arguments) or (None, user_input) if not a command.
"""
stripped = user_input.strip()
if not stripped.startswith("/"):
return None, user_input
# Match /command-name or /namespace:command
match = re.match(r"^/([a-zA-Z][\w:-]*)\s*(.*)$", stripped, re.DOTALL)
if not match:
return None, user_input
return match.group(1), match.group(2).strip()
def expand_command(name: str, arguments: str = "") -> dict[str, Any]:
"""Expand a slash command into a full prompt for the model.
Replaces $ARGUMENTS placeholder with the user-provided arguments.
"""
cmd = get_command(name)
if not cmd:
return {
"success": False,
"error": f"Unknown command: /{name}",
"available": [c["name"] for c in list_commands()],
}
body = cmd["body"]
# Replace $ARGUMENTS
expanded = body.replace("$ARGUMENTS", arguments)
# Also support bash-style $(cmd) execution for context blocks (like Claude Code)
# e.g. !`git status` becomes the output of `git status`
from code.tools.bash import run_bash
def _exec_bash(match: re.Match) -> str:
cmd_str = match.group(1)
result = run_bash(cmd_str, timeout=10)
return result.get("stdout", "") + result.get("stderr", "")
expanded = re.sub(r"!`([^`]+)`", _exec_bash, expanded)
return {
"success": True,
"name": cmd["name"],
"description": cmd["description"],
"prompt": expanded,
"allowed_tools": cmd["allowed_tools"],
}