refactor: centralize slash command registry (#1603)
* refactor: centralize slash command registry Replace 7+ scattered command definition sites with a single CommandDef registry in hermes_cli/commands.py. All downstream consumers now derive from this registry: - CLI process_command() resolves aliases via resolve_command() - Gateway _known_commands uses GATEWAY_KNOWN_COMMANDS frozenset - Gateway help text generated by gateway_help_lines() - Telegram BotCommands generated by telegram_bot_commands() - Slack subcommand map generated by slack_subcommand_map() Adding a command or alias is now a one-line change to COMMAND_REGISTRY instead of touching 6+ files. Bugfixes included: - Telegram now registers /rollback, /background (were missing) - Slack now has /voice, /update, /reload-mcp (were missing) - Gateway duplicate 'reasoning' dispatch (dead code) removed - Gateway help text can no longer drift from CLI help Backwards-compatible: COMMANDS and COMMANDS_BY_CATEGORY dicts are rebuilt from the registry, so existing imports work unchanged. * docs: update developer docs for centralized command registry Update AGENTS.md with full 'Slash Command Registry' and 'Adding a Slash Command' sections covering CommandDef fields, registry helpers, and the one-line alias workflow. Also update: - CONTRIBUTING.md: commands.py description - website/docs/reference/slash-commands.md: reference central registry - docs/plans/centralize-command-registry.md: mark COMPLETED - plans/checkpoint-rollback.md: reference new pattern - hermes-agent-dev skill: architecture table * chore: remove stale plan docs
This commit is contained in:
parent
b798062501
commit
46176c8029
14 changed files with 571 additions and 802 deletions
|
|
@ -789,24 +789,11 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
user_id = command.get("user_id", "")
|
||||
channel_id = command.get("channel_id", "")
|
||||
|
||||
# Map subcommands to gateway commands
|
||||
subcommand_map = {
|
||||
"new": "/reset", "reset": "/reset",
|
||||
"status": "/status", "stop": "/stop",
|
||||
"help": "/help",
|
||||
"model": "/model", "personality": "/personality",
|
||||
"retry": "/retry", "undo": "/undo",
|
||||
"compact": "/compress", "compress": "/compress",
|
||||
"resume": "/resume",
|
||||
"background": "/background",
|
||||
"bg": "/bg",
|
||||
"usage": "/usage",
|
||||
"insights": "/insights",
|
||||
"title": "/title",
|
||||
"reasoning": "/reasoning",
|
||||
"provider": "/provider",
|
||||
"rollback": "/rollback",
|
||||
}
|
||||
# Map subcommands to gateway commands — derived from central registry.
|
||||
# Also keep "compact" as a Slack-specific alias for /compress.
|
||||
from hermes_cli.commands import slack_subcommand_map
|
||||
subcommand_map = slack_subcommand_map()
|
||||
subcommand_map["compact"] = "/compress"
|
||||
first_word = text.split()[0] if text else ""
|
||||
if first_word in subcommand_map:
|
||||
# Preserve arguments after the subcommand
|
||||
|
|
|
|||
|
|
@ -240,29 +240,13 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
)
|
||||
|
||||
# Register bot commands so Telegram shows a hint menu when users type /
|
||||
# List is derived from the central COMMAND_REGISTRY — adding a new
|
||||
# gateway command there automatically adds it to the Telegram menu.
|
||||
try:
|
||||
from telegram import BotCommand
|
||||
from hermes_cli.commands import telegram_bot_commands
|
||||
await self._bot.set_my_commands([
|
||||
BotCommand("new", "Start a new conversation"),
|
||||
BotCommand("reset", "Reset conversation history"),
|
||||
BotCommand("model", "Show or change the model"),
|
||||
BotCommand("reasoning", "Show or change reasoning effort"),
|
||||
BotCommand("personality", "Set a personality"),
|
||||
BotCommand("retry", "Retry your last message"),
|
||||
BotCommand("undo", "Remove the last exchange"),
|
||||
BotCommand("status", "Show session info"),
|
||||
BotCommand("stop", "Stop the running agent"),
|
||||
BotCommand("sethome", "Set this chat as the home channel"),
|
||||
BotCommand("compress", "Compress conversation context"),
|
||||
BotCommand("title", "Set or show the session title"),
|
||||
BotCommand("resume", "Resume a previously-named session"),
|
||||
BotCommand("usage", "Show token usage for this session"),
|
||||
BotCommand("provider", "Show available providers"),
|
||||
BotCommand("insights", "Show usage insights and analytics"),
|
||||
BotCommand("update", "Update Hermes to the latest version"),
|
||||
BotCommand("reload_mcp", "Reload MCP servers from config"),
|
||||
BotCommand("voice", "Toggle voice reply mode"),
|
||||
BotCommand("help", "Show available commands"),
|
||||
BotCommand(name, desc) for name, desc in telegram_bot_commands()
|
||||
])
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
|
|
|
|||
|
|
@ -1285,45 +1285,47 @@ class GatewayRunner:
|
|||
# Check for commands
|
||||
command = event.get_command()
|
||||
|
||||
# Emit command:* hook for any recognized slash command
|
||||
_known_commands = {"new", "reset", "help", "status", "stop", "model", "reasoning",
|
||||
"personality", "plan", "retry", "undo", "sethome", "set-home",
|
||||
"compress", "usage", "insights", "reload-mcp", "reload_mcp",
|
||||
"update", "title", "resume", "provider", "rollback",
|
||||
"background", "bg", "reasoning", "voice"}
|
||||
if command and command in _known_commands:
|
||||
# Emit command:* hook for any recognized slash command.
|
||||
# GATEWAY_KNOWN_COMMANDS is derived from the central COMMAND_REGISTRY
|
||||
# in hermes_cli/commands.py — no hardcoded set to maintain here.
|
||||
from hermes_cli.commands import GATEWAY_KNOWN_COMMANDS, resolve_command as _resolve_cmd
|
||||
if command and command in GATEWAY_KNOWN_COMMANDS:
|
||||
await self.hooks.emit(f"command:{command}", {
|
||||
"platform": source.platform.value if source.platform else "",
|
||||
"user_id": source.user_id,
|
||||
"command": command,
|
||||
"args": event.get_command_args().strip(),
|
||||
})
|
||||
|
||||
if command in ["new", "reset"]:
|
||||
|
||||
# Resolve aliases to canonical name so dispatch only checks canonicals.
|
||||
_cmd_def = _resolve_cmd(command) if command else None
|
||||
canonical = _cmd_def.name if _cmd_def else command
|
||||
|
||||
if canonical == "new":
|
||||
return await self._handle_reset_command(event)
|
||||
|
||||
if command == "help":
|
||||
if canonical == "help":
|
||||
return await self._handle_help_command(event)
|
||||
|
||||
if command == "status":
|
||||
if canonical == "status":
|
||||
return await self._handle_status_command(event)
|
||||
|
||||
if command == "stop":
|
||||
if canonical == "stop":
|
||||
return await self._handle_stop_command(event)
|
||||
|
||||
if command == "model":
|
||||
if canonical == "model":
|
||||
return await self._handle_model_command(event)
|
||||
|
||||
if command == "reasoning":
|
||||
if canonical == "reasoning":
|
||||
return await self._handle_reasoning_command(event)
|
||||
|
||||
if command == "provider":
|
||||
if canonical == "provider":
|
||||
return await self._handle_provider_command(event)
|
||||
|
||||
if command == "personality":
|
||||
if canonical == "personality":
|
||||
return await self._handle_personality_command(event)
|
||||
|
||||
if command == "plan":
|
||||
if canonical == "plan":
|
||||
try:
|
||||
from agent.skill_commands import build_plan_path, build_skill_invocation_message
|
||||
|
||||
|
|
@ -1340,51 +1342,48 @@ class GatewayRunner:
|
|||
)
|
||||
if not event.text:
|
||||
return "Failed to load the bundled /plan skill."
|
||||
command = None
|
||||
canonical = None
|
||||
except Exception as e:
|
||||
logger.exception("Failed to prepare /plan command")
|
||||
return f"Failed to enter plan mode: {e}"
|
||||
|
||||
if command == "retry":
|
||||
if canonical == "retry":
|
||||
return await self._handle_retry_command(event)
|
||||
|
||||
if command == "undo":
|
||||
if canonical == "undo":
|
||||
return await self._handle_undo_command(event)
|
||||
|
||||
if command in ["sethome", "set-home"]:
|
||||
if canonical == "sethome":
|
||||
return await self._handle_set_home_command(event)
|
||||
|
||||
if command == "compress":
|
||||
if canonical == "compress":
|
||||
return await self._handle_compress_command(event)
|
||||
|
||||
if command == "usage":
|
||||
if canonical == "usage":
|
||||
return await self._handle_usage_command(event)
|
||||
|
||||
if command == "insights":
|
||||
if canonical == "insights":
|
||||
return await self._handle_insights_command(event)
|
||||
|
||||
if command in ("reload-mcp", "reload_mcp"):
|
||||
if canonical == "reload-mcp":
|
||||
return await self._handle_reload_mcp_command(event)
|
||||
|
||||
if command == "update":
|
||||
if canonical == "update":
|
||||
return await self._handle_update_command(event)
|
||||
|
||||
if command == "title":
|
||||
if canonical == "title":
|
||||
return await self._handle_title_command(event)
|
||||
|
||||
if command == "resume":
|
||||
if canonical == "resume":
|
||||
return await self._handle_resume_command(event)
|
||||
|
||||
if command == "rollback":
|
||||
if canonical == "rollback":
|
||||
return await self._handle_rollback_command(event)
|
||||
|
||||
if command == "background":
|
||||
if canonical == "background":
|
||||
return await self._handle_background_command(event)
|
||||
|
||||
if command == "reasoning":
|
||||
return await self._handle_reasoning_command(event)
|
||||
|
||||
if command == "voice":
|
||||
if canonical == "voice":
|
||||
return await self._handle_voice_command(event)
|
||||
|
||||
# User-defined quick commands (bypass agent loop, no LLM call)
|
||||
|
|
@ -2093,30 +2092,10 @@ class GatewayRunner:
|
|||
|
||||
async def _handle_help_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /help command - list available commands."""
|
||||
from hermes_cli.commands import gateway_help_lines
|
||||
lines = [
|
||||
"📖 **Hermes Commands**\n",
|
||||
"`/new` — Start a new conversation",
|
||||
"`/reset` — Reset conversation history",
|
||||
"`/status` — Show session info",
|
||||
"`/stop` — Interrupt the running agent",
|
||||
"`/model [provider:model]` — Show/change model (or switch provider)",
|
||||
"`/provider` — Show available providers and auth status",
|
||||
"`/personality [name]` — Set a personality",
|
||||
"`/retry` — Retry your last message",
|
||||
"`/undo` — Remove the last exchange",
|
||||
"`/sethome` — Set this chat as the home channel",
|
||||
"`/compress` — Compress conversation context",
|
||||
"`/title [name]` — Set or show the session title",
|
||||
"`/resume [name]` — Resume a previously-named session",
|
||||
"`/usage` — Show token usage for this session",
|
||||
"`/insights [days]` — Show usage insights and analytics",
|
||||
"`/reasoning [level|show|hide]` — Set reasoning effort or toggle display",
|
||||
"`/rollback [number]` — List or restore filesystem checkpoints",
|
||||
"`/background <prompt>` — Run a prompt in a separate background session",
|
||||
"`/voice [on|off|tts|status]` — Toggle voice reply mode",
|
||||
"`/reload-mcp` — Reload MCP servers from config",
|
||||
"`/update` — Update Hermes Agent to the latest version",
|
||||
"`/help` — Show this message",
|
||||
*gateway_help_lines(),
|
||||
]
|
||||
try:
|
||||
from agent.skill_commands import get_skill_commands
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue