diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 03e3df81b9b..be1da354bbb 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -508,6 +508,64 @@ def telegram_bot_commands() -> list[tuple[str, str]]: return result +_TELEGRAM_MENU_PRIORITY = ( + "debug", + "restart", + "update", + "verbose", + "commands", + "help", + "new", + "stop", + "status", + "resume", + "sessions", + "approve", + "deny", + "queue", + "steer", + "background", + "model", + "reasoning", + "usage", + "platforms", + "platform", + "profile", + "whoami", +) +"""Built-in commands that should stay visible in Telegram's capped menu. + +Telegram only displays a small BotCommand menu in practice. The full Hermes +registry is still dispatchable when typed manually, but operational commands +need to survive the visible menu cap ahead of lower-priority built-ins. +""" + + +def _prioritize_telegram_menu_commands( + commands: list[tuple[str, str]], +) -> list[tuple[str, str]]: + priority = { + _sanitize_telegram_name(name): index + for index, name in enumerate(_TELEGRAM_MENU_PRIORITY) + } + return [ + command + for _index, command in sorted( + enumerate(commands), + key=lambda item: ( + 0, + priority[item[1][0]], + item[0], + ) + if item[1][0] in priority + else ( + 1, + item[0], + ), + ) + ] + + _CMD_NAME_LIMIT = 32 """Max command name length shared by Telegram and Discord.""" @@ -721,11 +779,12 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str Returns: (menu_commands, hidden_count) where hidden_count is the number of - skill commands omitted due to the cap. + commands omitted due to the cap. """ - core_commands = list(telegram_bot_commands()) + core_commands = _prioritize_telegram_menu_commands(list(telegram_bot_commands())) reserved_names = {n for n, _ in core_commands} all_commands = list(core_commands) + hidden_core_count = max(0, len(all_commands) - max_commands) remaining_slots = max(0, max_commands - len(all_commands)) entries, hidden_count = _collect_gateway_skill_entries( @@ -737,7 +796,7 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str ) # Drop the cmd_key — Telegram only needs (name, desc) pairs. all_commands.extend((n, d) for n, d, _k in entries) - return all_commands[:max_commands], hidden_count + return all_commands[:max_commands], hidden_count + hidden_core_count def discord_skill_commands( diff --git a/tests/hermes_cli/test_commands.py b/tests/hermes_cli/test_commands.py index 6de778347e1..7324adbe430 100644 --- a/tests/hermes_cli/test_commands.py +++ b/tests/hermes_cli/test_commands.py @@ -951,6 +951,30 @@ class TestTelegramMenuCommands: f"Command '{name}' is {len(name)} chars (limit {_TG_NAME_LIMIT})" ) + def test_operational_builtins_survive_thirty_command_cap(self, tmp_path, monkeypatch): + (tmp_path / "config.yaml").write_text( + "display:\n tool_progress_command: true\n" + ) + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + + menu, hidden = telegram_menu_commands(max_commands=30) + names = [name for name, _desc in menu] + + assert len(names) == 30 + assert hidden > 0 + for name in ( + "debug", + "restart", + "update", + "verbose", + "commands", + "help", + "new", + "stop", + "status", + ): + assert name in names + def test_includes_plugin_commands_via_lazy_discovery(self, tmp_path, monkeypatch): """Telegram menu generation should discover plugin slash commands on first access.""" from unittest.mock import patch