Spaces:
Running
Running
File size: 4,765 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 | """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"],
}
|