simplify(compat): hermes_cli/commands — drop the last PEP 562 lazy re-export hook (25 names), repoint 10 callers + 11 test files to commands_platforms/commands_completion

This commit is contained in:
Teknium
2026-09-03 17:00:26 -07:00
parent 342879f267
commit deb9f3c1bd
22 changed files with 44 additions and 97 deletions
+1 -1
View File
@@ -122,7 +122,7 @@ def _normalize_slack_parent_command(text: str, message_type: MessageType) -> tup
if not parent_parts or parent_parts[0] != "/hermes":
return text, message_type
from hermes_cli.commands import slack_subcommand_map
from hermes_cli.commands_platforms import slack_subcommand_map
payload = parent_parts[1].strip() if len(parent_parts) > 1 else ""
subcommand_map = slack_subcommand_map()
+1 -1
View File
@@ -829,7 +829,7 @@ def _telegramize_command_mentions(text: str, platform: Any) -> str:
if platform_value != "telegram":
return text
from hermes_cli.commands import _sanitize_telegram_name
from hermes_cli.commands_platforms import _sanitize_telegram_name
def _replace(match: re.Match[str]) -> str:
sanitized = _sanitize_telegram_name(match.group(1))
+1 -1
View File
@@ -15,7 +15,7 @@ import threading
import time
from agent.interrupt_compat import request_hard_interrupt
from hermes_cli.commands import SlashCommandAutoSuggest, SlashCommandCompleter
from hermes_cli.commands_completion import SlashCommandAutoSuggest, SlashCommandCompleter
from pathlib import Path
from prompt_toolkit.filters import Condition
from prompt_toolkit.history import FileHistory
-23
View File
@@ -467,26 +467,3 @@ def _iter_plugin_command_entries() -> list[tuple[str, str, str]]:
return [(name, str(meta.get("description") or f"Run /{name}"),
str(meta.get("args_hint") or "").strip())
for name, meta in commands.items() if isinstance(name, str) and isinstance(meta, dict)]
# Lazy re-exports (PEP 562): platform derivations and the prompt_toolkit completer live in
# sibling modules; their names stay importable here (``from hermes_cli.commands import X``,
# ``patch("hermes_cli.commands.X")``) while this module stays import-light for the gateway.
_LAZY_EXPORTS: dict[str, str] = {name: "hermes_cli.commands_platforms" for name in (
"_CMD_NAME_LIMIT", "_DEFAULT_TELEGRAM_MENU_MAX_COMMANDS", "_SLACK_RESERVED_COMMANDS",
"_SLACK_VIA_HERMES_ONLY", "_TELEGRAM_MENU_PRIORITY", "_clamp_command_names",
"_collect_gateway_skill_entries", "_iter_gateway_skills", "_prioritize_telegram_menu_candidates",
"_requires_argument", "_sanitize_slack_name", "_sanitize_telegram_name",
"_telegram_command_menu_config", "_truncate_desc", "discord_skill_commands_by_category",
"slack_app_manifest", "slack_native_slashes", "slack_subcommand_map", "telegram_bot_commands",
"telegram_menu_commands", "telegram_menu_max_commands")}
_LAZY_EXPORTS.update({name: "hermes_cli.commands_completion" for name in (
"SlashCommandCompleter", "SlashCommandAutoSuggest", "_file_size_label", "_short_desc")})
def __getattr__(name: str):
module = _LAZY_EXPORTS.get(name)
if module is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(module), name)
+2 -6
View File
@@ -148,9 +148,7 @@ def _prioritize_telegram_menu_candidates(
"""Order ``(final_name, description, source, raw_name)`` candidates; the default priority
applies to core only, "replace" mode ignores it. ``raw_name`` is the pre-clamp name so a
configured long command stays addressable."""
# Lazy origin import: tests patch ``hermes_cli.commands._telegram_command_menu_config``.
from hermes_cli.commands import _telegram_command_menu_config as menu_config
menu_cfg = menu_config()
menu_cfg = _telegram_command_menu_config()
configured_rank = _sanitized_rank(menu_cfg["priority"])
default_rank = _sanitized_rank(_TELEGRAM_MENU_PRIORITY)
tiers = _TELEGRAM_PRIORITY_TIERS[menu_cfg["priority_mode"]]
@@ -272,9 +270,7 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str
telegram-disabled skills excluded). Tiers keep relative order unless named in
``platforms.telegram.extra.command_menu.priority`` — applied *before* the cap, so a
prioritized dynamic command can displace an unprioritized core command."""
# Lazy origin import: tests patch ``hermes_cli.commands.telegram_bot_commands``.
from hermes_cli.commands import telegram_bot_commands as bot_commands
core_commands = list(bot_commands(include_plugins=False))
core_commands = list(telegram_bot_commands(include_plugins=False))
entries, hidden_count = _collect_gateway_skill_entries(
platform="telegram", max_slots=None, reserved_names={n for n, _ in core_commands},
desc_limit=40, sanitize_name=_sanitize_telegram_name)
+2 -2
View File
@@ -18,7 +18,7 @@ def _build_full_manifest(
Other sections (OAuth scopes, socket mode) are sensible Hermes defaults, tweakable in the Slack
UI after pasting.
"""
from hermes_cli.commands import slack_app_manifest
from hermes_cli.commands_platforms import slack_app_manifest
if messaging_experience is None:
messaging_experience = "assistant" if include_assistant else "none"
messaging_experience = str(messaging_experience).strip().lower()
@@ -113,7 +113,7 @@ def slack_manifest_command(args) -> int:
messaging_experience = "assistant"
if slashes_only:
from hermes_cli.commands import slack_app_manifest
from hermes_cli.commands_platforms import slack_app_manifest
manifest = slack_app_manifest()["features"]["slash_commands"]
else:
manifest = _build_full_manifest(
+1 -1
View File
@@ -4671,7 +4671,7 @@ class DiscordAdapter(BasePlatformAdapter):
def _refresh_skill_catalog_state(self) -> None:
"""Re-scan disk and repopulate ``self._skill_entries``/``_skill_lookup`` in place.
No Discord API calls: autocomplete and handler read these attributes directly."""
from hermes_cli.commands import discord_skill_commands_by_category
from hermes_cli.commands_platforms import discord_skill_commands_by_category
reserved = getattr(self, "_skill_group_reserved_names", set())
categories, uncategorized, hidden = discord_skill_commands_by_category(
reserved_names=set(reserved),
+2 -2
View File
@@ -1479,7 +1479,7 @@ class SlackAdapter(BasePlatformAdapter):
# Every COMMAND_REGISTRY command is a native slash via one regex matcher. Commands must
# ALSO be declared in the app manifest (`hermes slack manifest`): Socket Mode won't
# deliver undeclared commands at all.
from hermes_cli.commands import slack_native_slashes
from hermes_cli.commands_platforms import slack_native_slashes
_slash_names = [name for name, _d, _h in slack_native_slashes()]
if _slash_names:
_slash_pattern = re.compile(
@@ -5257,7 +5257,7 @@ class SlackAdapter(BasePlatformAdapter):
if slash_name not in {"hermes", ""}:
return f"/{slash_name}" if not raw_text else f"/{slash_name} {raw_text}"
legacy_text = raw_text.strip()
from hermes_cli.commands import slack_subcommand_map
from hermes_cli.commands_platforms import slack_subcommand_map
subcommand_map = slack_subcommand_map()
subcommand_map["compact"] = "/compress"
first_word = legacy_text.split()[0] if legacy_text.split() else ""
+2 -2
View File
@@ -2522,7 +2522,7 @@ class TelegramAdapter(BasePlatformAdapter):
"""Register the command menu (from COMMAND_REGISTRY) in every scope — Telegram picks the
narrowest matching one per chat type; forum topics are handled lazily by _ensure_forum_commands."""
from telegram import BotCommand, BotCommandScopeAllPrivateChats, BotCommandScopeAllGroupChats, BotCommandScopeDefault
from hermes_cli.commands import telegram_menu_commands, telegram_menu_max_commands
from hermes_cli.commands_platforms import telegram_menu_commands, telegram_menu_max_commands
if not self._bot:
return
# Telegram allows 100 commands but has an undocumented ~4KB payload limit; default cap 60.
@@ -5630,7 +5630,7 @@ class TelegramAdapter(BasePlatformAdapter):
if chat_id in self._forum_command_registered:
return
from telegram import BotCommand, BotCommandScopeChat
from hermes_cli.commands import telegram_menu_commands, telegram_menu_max_commands
from hermes_cli.commands_platforms import telegram_menu_commands, telegram_menu_max_commands
menu_commands, _ = telegram_menu_commands(max_commands=telegram_menu_max_commands())
bot_commands = [BotCommand(name, desc) for name, desc in menu_commands]
await self._bot.set_my_commands(bot_commands, scope=BotCommandScopeChat(chat_id=chat_id))
+1 -1
View File
@@ -33,7 +33,7 @@ def collect_inline_catalog() -> List[Dict[str, str]]:
catalog.append({"name": name, "description": desc or ""})
try:
from hermes_cli.commands import _collect_gateway_skill_entries, _sanitize_telegram_name, telegram_bot_commands
from hermes_cli.commands_platforms import _collect_gateway_skill_entries, _sanitize_telegram_name, telegram_bot_commands
except Exception: # pragma: no cover - defensive
logger.debug("inline picker: commands registry unavailable", exc_info=True)
return catalog
+1 -1
View File
@@ -68,7 +68,7 @@ class TestPlanRegistryWiring:
assert "plan" in GATEWAY_KNOWN_COMMANDS
def test_plan_in_telegram_bot_commands(self):
from hermes_cli.commands import telegram_bot_commands
from hermes_cli.commands_platforms import telegram_bot_commands
names = {n for n, _ in telegram_bot_commands()}
assert "plan" in names
+1 -1
View File
@@ -193,7 +193,7 @@ class TestBackgroundInCLICommands:
def test_bg_autocompletes(self):
"""The /bg and /btw commands appear in autocomplete results."""
pytest.importorskip("prompt_toolkit")
from hermes_cli.commands import SlashCommandCompleter
from hermes_cli.commands_completion import SlashCommandCompleter
from prompt_toolkit.document import Document
completer = SlashCommandCompleter()
+2 -2
View File
@@ -530,7 +530,7 @@ def test_register_skill_command_callback_dispatches_by_name(adapter):
]
with patch(
"hermes_cli.commands.discord_skill_commands_by_category",
"hermes_cli.commands_platforms.discord_skill_commands_by_category",
return_value=(mock_categories, mock_uncategorized, 0),
):
adapter._register_slash_commands()
@@ -580,7 +580,7 @@ def test_register_skill_command_payload_fits_discord_8kb_limit(adapter):
]
with patch(
"hermes_cli.commands.discord_skill_commands_by_category",
"hermes_cli.commands_platforms.discord_skill_commands_by_category",
return_value=(large_categories, [], 0),
):
adapter._register_slash_commands()
@@ -69,7 +69,7 @@ class TestRefreshSkillGroup:
)
monkeypatch.setattr(
"hermes_cli.commands.discord_skill_commands_by_category",
"hermes_cli.commands_platforms.discord_skill_commands_by_category",
fake_collector,
)
@@ -113,7 +113,7 @@ class TestRegisterSkillGroupUsesInstanceState:
0,
)
monkeypatch.setattr(
"hermes_cli.commands.discord_skill_commands_by_category",
"hermes_cli.commands_platforms.discord_skill_commands_by_category",
fake_collector,
)
@@ -35,7 +35,7 @@ async def test_ensure_forum_commands_registers_once():
adapter = _make_test_adapter()
msg = _forum_message(chat_id=-123, is_forum=True)
with patch("hermes_cli.commands.telegram_menu_commands") as mock_menu:
with patch("hermes_cli.commands_platforms.telegram_menu_commands") as mock_menu:
mock_menu.return_value = ([("new", "Start new session"), ("help", "Show help")], 0)
with patch("telegram.BotCommand") as MockBotCommand:
instances = []
@@ -73,7 +73,7 @@ async def test_ensure_forum_commands_race_safety():
adapter = _make_test_adapter()
msg = _forum_message(chat_id=-789, is_forum=True)
with patch("hermes_cli.commands.telegram_menu_commands") as mock_menu:
with patch("hermes_cli.commands_platforms.telegram_menu_commands") as mock_menu:
mock_menu.return_value = ([("new", "Start new session")], 0)
with patch("telegram.BotCommand"):
with patch("telegram.BotCommandScopeChat"):
+3 -8
View File
@@ -6,14 +6,9 @@ from unittest.mock import MagicMock, patch
import yaml
from cli import HermesCLI
from hermes_cli.commands import (
GATEWAY_KNOWN_COMMANDS,
SUBCOMMANDS,
SlashCommandCompleter,
gateway_help_lines,
resolve_command,
telegram_bot_commands,
)
from hermes_cli.commands import GATEWAY_KNOWN_COMMANDS, SUBCOMMANDS, gateway_help_lines, resolve_command
from hermes_cli.commands_completion import SlashCommandCompleter
from hermes_cli.commands_platforms import telegram_bot_commands
from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document
@@ -14,7 +14,7 @@ from __future__ import annotations
from pathlib import Path
from typing import Iterable
from hermes_cli.commands import SlashCommandCompleter
from hermes_cli.commands_completion import SlashCommandCompleter
def _run(tmp_path: Path, word: str) -> list[tuple[str, str]]:
+14 -35
View File
@@ -3,30 +3,9 @@
from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document
from hermes_cli.commands import (
COMMAND_REGISTRY,
COMMANDS,
COMMANDS_BY_CATEGORY,
CommandDef,
GATEWAY_KNOWN_COMMANDS,
SUBCOMMANDS,
SlashCommandAutoSuggest,
SlashCommandCompleter,
_CMD_NAME_LIMIT,
_SLACK_RESERVED_COMMANDS,
_SLACK_VIA_HERMES_ONLY,
_clamp_command_names,
_sanitize_telegram_name,
command_desktop_meta,
gateway_help_lines,
infer_argument_mode,
resolve_command,
slack_app_manifest,
slack_native_slashes,
slack_subcommand_map,
telegram_bot_commands,
telegram_menu_commands,
)
from hermes_cli.commands import COMMAND_REGISTRY, COMMANDS, COMMANDS_BY_CATEGORY, CommandDef, GATEWAY_KNOWN_COMMANDS, SUBCOMMANDS, command_desktop_meta, gateway_help_lines, infer_argument_mode, resolve_command
from hermes_cli.commands_completion import SlashCommandAutoSuggest, SlashCommandCompleter
from hermes_cli.commands_platforms import _CMD_NAME_LIMIT, _SLACK_RESERVED_COMMANDS, _SLACK_VIA_HERMES_ONLY, _clamp_command_names, _sanitize_telegram_name, slack_app_manifest, slack_native_slashes, slack_subcommand_map, telegram_bot_commands, telegram_menu_commands
def _completions(completer: SlashCommandCompleter, text: str):
@@ -589,7 +568,7 @@ class TestGatewaySkillCollector:
def test_long_skill_name_clamped_but_cmd_key_retained(self, tmp_path):
from unittest.mock import patch
from hermes_cli.commands import _collect_gateway_skill_entries
from hermes_cli.commands_platforms import _collect_gateway_skill_entries
long_name = "this-is-a-very-long-skill-name-that-exceeds-limit"
skills_dir = tmp_path / "skills"
@@ -617,7 +596,7 @@ class TestGatewaySkillCollector:
def test_cap_trims_skills_only(self, tmp_path):
from unittest.mock import patch
from hermes_cli.commands import _collect_gateway_skill_entries
from hermes_cli.commands_platforms import _collect_gateway_skill_entries
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
@@ -814,11 +793,11 @@ class TestTelegramMenuCommands:
menu_cfg = {"max_commands": 2, "priority_mode": "prepend", "priority": ["gym"]}
with (
patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core),
patch("hermes_cli.commands_platforms.telegram_bot_commands", return_value=fake_core),
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("hermes_cli.plugins.get_plugin_commands", return_value=fake_plugins),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch("hermes_cli.commands._telegram_command_menu_config", return_value=menu_cfg),
patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg),
):
menu, hidden = telegram_menu_commands(max_commands=len(fake_core))
@@ -855,10 +834,10 @@ class TestTelegramMenuCommands:
menu_cfg = {"max_commands": 2, "priority_mode": "prepend", "priority": []}
with (
patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core),
patch("hermes_cli.commands_platforms.telegram_bot_commands", return_value=fake_core),
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch("hermes_cli.commands._telegram_command_menu_config", return_value=menu_cfg),
patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg),
):
menu, hidden = telegram_menu_commands(max_commands=2)
@@ -891,10 +870,10 @@ class TestTelegramMenuCommands:
}
with (
patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core),
patch("hermes_cli.commands_platforms.telegram_bot_commands", return_value=fake_core),
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch("hermes_cli.commands._telegram_command_menu_config", return_value=menu_cfg),
patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg),
):
menu, hidden = telegram_menu_commands(max_commands=1)
@@ -922,7 +901,7 @@ class TestTelegramMenuCommands:
patch("hermes_cli.plugins.get_plugin_commands", return_value=fake_plugins),
patch("agent.skill_commands.get_skill_commands", return_value={}),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch("hermes_cli.commands._telegram_command_menu_config", return_value=menu_cfg),
patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg),
):
menu, hidden = telegram_menu_commands(max_commands=1)
@@ -957,7 +936,7 @@ class TestTelegramMenuCommands:
def test_scalar_configured_priority_is_accepted_as_one_command(self):
"""The config CLI's scalar value form must work for a single priority."""
from unittest.mock import patch
from hermes_cli.commands import _telegram_command_menu_config
from hermes_cli.commands_platforms import _telegram_command_menu_config
raw_config = {
"platforms": {
@@ -980,7 +959,7 @@ class TestTelegramMenuCommands:
# Discord skill commands grouped by category
# ---------------------------------------------------------------------------
from hermes_cli.commands import discord_skill_commands_by_category # noqa: E402
from hermes_cli.commands_platforms import discord_skill_commands_by_category
class TestDiscordSkillCommandsByCategory:
@@ -23,7 +23,7 @@ def test_clamp_collision_emits_warning_naming_both_skills(
tmp_path: Path, caplog
) -> None:
"""Two skills with identical first 32 chars — warning names both."""
from hermes_cli.commands import discord_skill_commands_by_category
from hermes_cli.commands_platforms import discord_skill_commands_by_category
# Craft cmd_keys that share the first 32 chars.
# 40-char prefix 'skill-collision-prefix-identical-first-32'
@@ -98,7 +98,7 @@ def test_long_skill_name_preserves_cmd_key_through_by_category(
This is the actual runtime path used by the Discord adapter via
``_refresh_skill_catalog_state``.
"""
from hermes_cli.commands import discord_skill_commands_by_category
from hermes_cli.commands_platforms import discord_skill_commands_by_category
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
+1 -1
View File
@@ -7,7 +7,7 @@ import pytest
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import to_plain_text
from hermes_cli.commands import SlashCommandCompleter, _file_size_label
from hermes_cli.commands_completion import SlashCommandCompleter, _file_size_label
def _display_names(completions):
+1 -1
View File
@@ -11527,7 +11527,7 @@ def test_plugins_list_surfaces_loader_error(monkeypatch):
def test_complete_slash_surfaces_completer_error(monkeypatch):
with patch(
"hermes_cli.commands.SlashCommandCompleter",
"hermes_cli.commands_completion.SlashCommandCompleter",
side_effect=Exception("no completer"),
):
resp = server.handle_request(
+1 -1
View File
@@ -226,7 +226,7 @@ def _(rid, params: dict) -> dict:
text = params.get("text", "")
if not text.startswith("/"):
return _ok(rid, {"items": []})
from hermes_cli.commands import SlashCommandCompleter
from hermes_cli.commands_completion import SlashCommandCompleter
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import to_plain_text
from agent.skill_commands import get_skill_commands