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
75
cli.py
75
cli.py
|
|
@ -3266,18 +3266,25 @@ class HermesCLI:
|
|||
# Lowercase only for dispatch matching; preserve original case for arguments
|
||||
cmd_lower = command.lower().strip()
|
||||
cmd_original = command.strip()
|
||||
|
||||
# Resolve aliases via central registry so adding an alias is a one-line
|
||||
# change in hermes_cli/commands.py instead of touching every dispatch site.
|
||||
from hermes_cli.commands import resolve_command as _resolve_cmd
|
||||
_base_word = cmd_lower.split()[0].lstrip("/")
|
||||
_cmd_def = _resolve_cmd(_base_word)
|
||||
canonical = _cmd_def.name if _cmd_def else _base_word
|
||||
|
||||
if cmd_lower in ("/quit", "/exit", "/q"):
|
||||
if canonical in ("quit", "exit", "q"):
|
||||
return False
|
||||
elif cmd_lower == "/help":
|
||||
elif canonical == "help":
|
||||
self.show_help()
|
||||
elif cmd_lower == "/tools":
|
||||
elif canonical == "tools":
|
||||
self.show_tools()
|
||||
elif cmd_lower == "/toolsets":
|
||||
elif canonical == "toolsets":
|
||||
self.show_toolsets()
|
||||
elif cmd_lower == "/config":
|
||||
elif canonical == "config":
|
||||
self.show_config()
|
||||
elif cmd_lower == "/clear":
|
||||
elif canonical == "clear":
|
||||
self.new_session(silent=True)
|
||||
# Clear terminal screen. Inside the TUI, Rich's console.clear()
|
||||
# goes through patch_stdout's StdoutProxy which swallows the
|
||||
|
|
@ -3318,9 +3325,9 @@ class HermesCLI:
|
|||
else:
|
||||
self.show_banner()
|
||||
print(" ✨ (◕‿◕)✨ Fresh start! Screen cleared and conversation reset.\n")
|
||||
elif cmd_lower == "/history":
|
||||
elif canonical == "history":
|
||||
self.show_history()
|
||||
elif cmd_lower.startswith("/title"):
|
||||
elif canonical == "title":
|
||||
parts = cmd_original.split(maxsplit=1)
|
||||
if len(parts) > 1:
|
||||
raw_title = parts[1].strip()
|
||||
|
|
@ -3391,9 +3398,9 @@ class HermesCLI:
|
|||
_cprint(f" No title set. Usage: /title <your session title>")
|
||||
else:
|
||||
_cprint(" Session database not available.")
|
||||
elif cmd_lower in ("/reset", "/new"):
|
||||
elif canonical == "new":
|
||||
self.new_session()
|
||||
elif cmd_lower.startswith("/model"):
|
||||
elif canonical == "model":
|
||||
# Use original case so model names like "Anthropic/Claude-Opus-4" are preserved
|
||||
parts = cmd_original.split(maxsplit=1)
|
||||
if len(parts) > 1:
|
||||
|
|
@ -3480,50 +3487,50 @@ class HermesCLI:
|
|||
print(" Note: Model will revert on restart. Use a verified model to save to config.")
|
||||
else:
|
||||
self._show_model_and_providers()
|
||||
elif cmd_lower == "/provider":
|
||||
elif canonical == "provider":
|
||||
self._show_model_and_providers()
|
||||
elif cmd_lower.startswith("/prompt"):
|
||||
elif canonical == "prompt":
|
||||
# Use original case so prompt text isn't lowercased
|
||||
self._handle_prompt_command(cmd_original)
|
||||
elif cmd_lower.startswith("/personality"):
|
||||
elif canonical == "personality":
|
||||
# Use original case (handler lowercases the personality name itself)
|
||||
self._handle_personality_command(cmd_original)
|
||||
elif cmd_lower == "/plan" or cmd_lower.startswith("/plan "):
|
||||
elif canonical == "plan":
|
||||
self._handle_plan_command(cmd_original)
|
||||
elif cmd_lower == "/retry":
|
||||
elif canonical == "retry":
|
||||
retry_msg = self.retry_last()
|
||||
if retry_msg and hasattr(self, '_pending_input'):
|
||||
# Re-queue the message so process_loop sends it to the agent
|
||||
self._pending_input.put(retry_msg)
|
||||
elif cmd_lower == "/undo":
|
||||
elif canonical == "undo":
|
||||
self.undo_last()
|
||||
elif cmd_lower == "/save":
|
||||
elif canonical == "save":
|
||||
self.save_conversation()
|
||||
elif cmd_lower.startswith("/cron"):
|
||||
elif canonical == "cron":
|
||||
self._handle_cron_command(cmd_original)
|
||||
elif cmd_lower.startswith("/skills"):
|
||||
elif canonical == "skills":
|
||||
with self._busy_command(self._slow_command_status(cmd_original)):
|
||||
self._handle_skills_command(cmd_original)
|
||||
elif cmd_lower == "/platforms" or cmd_lower == "/gateway":
|
||||
elif canonical == "platforms":
|
||||
self._show_gateway_status()
|
||||
elif cmd_lower == "/verbose":
|
||||
elif canonical == "verbose":
|
||||
self._toggle_verbose()
|
||||
elif cmd_lower.startswith("/reasoning"):
|
||||
elif canonical == "reasoning":
|
||||
self._handle_reasoning_command(cmd_original)
|
||||
elif cmd_lower == "/compress":
|
||||
elif canonical == "compress":
|
||||
self._manual_compress()
|
||||
elif cmd_lower == "/usage":
|
||||
elif canonical == "usage":
|
||||
self._show_usage()
|
||||
elif cmd_lower.startswith("/insights"):
|
||||
elif canonical == "insights":
|
||||
self._show_insights(cmd_original)
|
||||
elif cmd_lower == "/paste":
|
||||
elif canonical == "paste":
|
||||
self._handle_paste_command()
|
||||
elif cmd_lower == "/reload-mcp":
|
||||
elif canonical == "reload-mcp":
|
||||
with self._busy_command(self._slow_command_status(cmd_original)):
|
||||
self._reload_mcp()
|
||||
elif cmd_lower.startswith("/browser"):
|
||||
elif _base_word == "browser":
|
||||
self._handle_browser_command(cmd_original)
|
||||
elif cmd_lower == "/plugins":
|
||||
elif canonical == "plugins":
|
||||
try:
|
||||
from hermes_cli.plugins import get_plugin_manager
|
||||
mgr = get_plugin_manager()
|
||||
|
|
@ -3544,15 +3551,15 @@ class HermesCLI:
|
|||
print(f" {status} {p['name']}{version}{detail}{error}")
|
||||
except Exception as e:
|
||||
print(f"Plugin system error: {e}")
|
||||
elif cmd_lower.startswith("/rollback"):
|
||||
elif canonical == "rollback":
|
||||
self._handle_rollback_command(cmd_original)
|
||||
elif cmd_lower == "/stop":
|
||||
elif canonical == "stop":
|
||||
self._handle_stop_command()
|
||||
elif cmd_lower.startswith("/background") or cmd_lower.startswith("/bg"):
|
||||
elif canonical == "background":
|
||||
self._handle_background_command(cmd_original)
|
||||
elif cmd_lower.startswith("/skin"):
|
||||
elif canonical == "skin":
|
||||
self._handle_skin_command(cmd_original)
|
||||
elif cmd_lower.startswith("/voice"):
|
||||
elif canonical == "voice":
|
||||
self._handle_voice_command(cmd_original)
|
||||
else:
|
||||
# Check for user-defined quick commands (bypass agent loop, no LLM call)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue