|
| 1 | +# Hermes slash-command monkeypatch |
| 2 | + |
| 3 | +This document describes the Hermes Agent-side patch needed when `/bm-*` slash commands do not appear in the gateway even though the Basic Memory provider itself is installed and working. |
| 4 | + |
| 5 | +Agents may use this file as a guide to add support for `/bm-*` slash commands. |
| 6 | + |
| 7 | +## Problem |
| 8 | + |
| 9 | +`hermes-basic-memory` is an **exclusive memory-provider plugin**. Hermes loads exclusive memory providers through `plugins.memory`, not through the normal `PluginManager` discovery path. |
| 10 | + |
| 11 | +Gateway adapters register native slash commands during startup by calling Hermes's plugin command discovery APIs. In affected Hermes builds, that startup path only sees commands registered by normal plugins. The active memory provider has not been loaded yet, and the memory-provider loader uses a collector that captures only `register_memory_provider(...)`. As a result, commands registered by this plugin with `ctx.register_command(...)` never reach the central plugin command registry before Discord/native slash-command sync. |
| 12 | + |
| 13 | +Symptoms: |
| 14 | + |
| 15 | +- `hermes memory status` shows `Provider: basic-memory` and `Status: available`. |
| 16 | +- Agent tools such as `bm_search`, `bm_read`, and `bm_recent` work. |
| 17 | +- Native slash commands such as `/bm-search`, `/bm-read`, and `/bm-context` are missing after `hermes gateway restart`. |
| 18 | + |
| 19 | +## Target behavior |
| 20 | + |
| 21 | +When Hermes builds its plugin command list, it should also load the configured active memory provider once, allowing that provider to register commands and skills into the same central registries used by ordinary plugins. |
| 22 | + |
| 23 | +After the patch, `get_plugin_commands()` should include commands such as: |
| 24 | + |
| 25 | +```text |
| 26 | +bm-context |
| 27 | +bm-project |
| 28 | +bm-read |
| 29 | +bm-recent |
| 30 | +bm-remember |
| 31 | +bm-search |
| 32 | +bm-status |
| 33 | +bm-workspace |
| 34 | +``` |
| 35 | + |
| 36 | +## Files to patch in Hermes Agent |
| 37 | + |
| 38 | +Patch these files in the Hermes Agent repository, not in this plugin repository: |
| 39 | + |
| 40 | +```text |
| 41 | +hermes_cli/plugins.py |
| 42 | +plugins/memory/__init__.py |
| 43 | +``` |
| 44 | + |
| 45 | +Recommended tests to add/update: |
| 46 | + |
| 47 | +```text |
| 48 | +tests/hermes_cli/test_plugin_cli_registration.py |
| 49 | +tests/hermes_cli/test_plugins.py |
| 50 | +``` |
| 51 | + |
| 52 | +## Implementation outline |
| 53 | + |
| 54 | +### 1. Load active memory-provider commands from `get_plugin_commands()` |
| 55 | + |
| 56 | +In `hermes_cli/plugins.py`, add module-level idempotency/recursion guards near the global plugin manager: |
| 57 | + |
| 58 | +```python |
| 59 | +_plugin_manager: Optional[PluginManager] = None |
| 60 | +_memory_provider_command_loads: set[str] = set() |
| 61 | +_memory_provider_command_loading = False |
| 62 | +``` |
| 63 | + |
| 64 | +Update `get_plugin_commands()` so it first ensures normal plugin discovery, then best-effort loads the active memory provider before returning the command registry: |
| 65 | + |
| 66 | +```python |
| 67 | +def get_plugin_commands() -> Dict[str, dict]: |
| 68 | + """Return the full plugin commands dict (name → {handler, description, plugin}). |
| 69 | +
|
| 70 | + Triggers idempotent plugin discovery so callers can use plugin commands |
| 71 | + before any explicit discover_plugins() call. Also initializes the active |
| 72 | + memory provider once so exclusive memory-provider plugins can contribute |
| 73 | + gateway slash commands during startup discovery. |
| 74 | + """ |
| 75 | + manager = _ensure_plugins_discovered() |
| 76 | + _ensure_active_memory_provider_commands_loaded() |
| 77 | + return manager._plugin_commands |
| 78 | +``` |
| 79 | + |
| 80 | +Add the helper: |
| 81 | + |
| 82 | +```python |
| 83 | +def _ensure_active_memory_provider_commands_loaded() -> None: |
| 84 | + """Best-effort load of the active memory provider's slash commands.""" |
| 85 | + global _memory_provider_command_loading |
| 86 | + if _memory_provider_command_loading: |
| 87 | + return |
| 88 | + try: |
| 89 | + from plugins import memory as memory_plugins |
| 90 | + |
| 91 | + active = memory_plugins._get_active_memory_provider() |
| 92 | + if not active or active in _memory_provider_command_loads: |
| 93 | + return |
| 94 | + _memory_provider_command_loading = True |
| 95 | + try: |
| 96 | + memory_plugins.load_memory_provider(active) |
| 97 | + _memory_provider_command_loads.add(active) |
| 98 | + finally: |
| 99 | + _memory_provider_command_loading = False |
| 100 | + except Exception as exc: |
| 101 | + logger.debug( |
| 102 | + "Failed to load active memory-provider plugin commands: %s", |
| 103 | + exc, |
| 104 | + exc_info=_PLUGINS_DEBUG, |
| 105 | + ) |
| 106 | +``` |
| 107 | + |
| 108 | +Notes: |
| 109 | + |
| 110 | +- This must be best-effort; command discovery should not break Hermes startup if a memory provider is misconfigured. |
| 111 | +- The recursion guard prevents `load_memory_provider(...)` → provider `register(...)` → `ctx.register_command(...)` → plugin manager access from re-entering endlessly. |
| 112 | +- The load set prevents duplicate provider command registration work. |
| 113 | + |
| 114 | +### 2. Make the memory-provider collector delegate commands and skills |
| 115 | + |
| 116 | +In `plugins/memory/__init__.py`, import `Callable`: |
| 117 | + |
| 118 | +```python |
| 119 | +from typing import Callable, List, Optional, Tuple |
| 120 | +``` |
| 121 | + |
| 122 | +When loading a provider directory, pass the plugin/provider name to the collector: |
| 123 | + |
| 124 | +```python |
| 125 | +collector = _ProviderCollector(plugin_name=name) |
| 126 | +``` |
| 127 | + |
| 128 | +Replace the collector that only captures `register_memory_provider(...)` with a plugin-context shim that also delegates `register_command(...)` and `register_skill(...)` into the central `PluginManager` registries: |
| 129 | + |
| 130 | +```python |
| 131 | +class _ProviderCollector: |
| 132 | + """Plugin-context shim used while loading memory providers. |
| 133 | +
|
| 134 | + Memory providers are exclusive plugins and are loaded by this module |
| 135 | + instead of the general PluginManager. They still need access to the same |
| 136 | + slash-command and skill registries as normal plugins, otherwise active |
| 137 | + memory-provider commands are invisible during gateway startup discovery. |
| 138 | + """ |
| 139 | + |
| 140 | + def __init__(self, plugin_name: str = "memory-provider"): |
| 141 | + self.provider = None |
| 142 | + self.plugin_name = plugin_name |
| 143 | + |
| 144 | + def register_memory_provider(self, provider): |
| 145 | + self.provider = provider |
| 146 | + |
| 147 | + def register_command( |
| 148 | + self, |
| 149 | + name: str, |
| 150 | + handler: Callable, |
| 151 | + description: str = "", |
| 152 | + args_hint: str = "", |
| 153 | + ) -> None: |
| 154 | + """Register a memory-provider slash command with PluginManager.""" |
| 155 | + try: |
| 156 | + from hermes_cli.plugins import _ensure_plugins_discovered |
| 157 | + except Exception: |
| 158 | + return |
| 159 | + |
| 160 | + clean = name.lower().strip().lstrip("/").replace(" ", "-") |
| 161 | + if not clean: |
| 162 | + return |
| 163 | + |
| 164 | + try: |
| 165 | + manager = _ensure_plugins_discovered() |
| 166 | + except Exception: |
| 167 | + return |
| 168 | + |
| 169 | + plugin_commands = getattr(manager, "_plugin_commands", None) |
| 170 | + if plugin_commands is None: |
| 171 | + return |
| 172 | + plugin_commands[clean] = { |
| 173 | + "handler": handler, |
| 174 | + "description": description or "Plugin command", |
| 175 | + "plugin": self.plugin_name, |
| 176 | + "args_hint": (args_hint or "").strip(), |
| 177 | + } |
| 178 | + |
| 179 | + def register_skill( |
| 180 | + self, |
| 181 | + name: str, |
| 182 | + path: Path, |
| 183 | + description: str = "", |
| 184 | + ) -> None: |
| 185 | + """Register a memory-provider skill with PluginManager.""" |
| 186 | + try: |
| 187 | + from agent.skill_utils import _NAMESPACE_RE |
| 188 | + from hermes_cli.plugins import _ensure_plugins_discovered |
| 189 | + except Exception: |
| 190 | + return |
| 191 | + |
| 192 | + if ":" in name or not name or not _NAMESPACE_RE.match(name): |
| 193 | + raise ValueError(f"Invalid skill name '{name}'.") |
| 194 | + if not path.exists(): |
| 195 | + raise FileNotFoundError(f"SKILL.md not found at {path}") |
| 196 | + |
| 197 | + try: |
| 198 | + manager = _ensure_plugins_discovered() |
| 199 | + except Exception: |
| 200 | + return |
| 201 | + |
| 202 | + plugin_skills = getattr(manager, "_plugin_skills", None) |
| 203 | + if plugin_skills is None: |
| 204 | + return |
| 205 | + plugin_skills[f"{self.plugin_name}:{name}"] = { |
| 206 | + "path": path, |
| 207 | + "plugin": self.plugin_name, |
| 208 | + "bare_name": name, |
| 209 | + "description": description, |
| 210 | + } |
| 211 | +``` |
| 212 | + |
| 213 | +Keep existing no-op methods such as `register_tool(...)` and `register_cli_command(...)` as no-ops unless the target Hermes version expects otherwise. |
| 214 | + |
| 215 | +## Verification |
| 216 | + |
| 217 | +From the Hermes Agent repository, run focused compile/tests: |
| 218 | + |
| 219 | +```bash |
| 220 | +python -m py_compile hermes_cli/plugins.py plugins/memory/__init__.py |
| 221 | +python -m pytest \ |
| 222 | + tests/hermes_cli/test_plugins.py::TestPluginCommands::test_get_plugin_commands_loads_active_memory_provider_commands \ |
| 223 | + tests/hermes_cli/test_plugin_cli_registration.py::TestProviderCollectorRegistration \ |
| 224 | + -q -o 'addopts=' |
| 225 | +``` |
| 226 | + |
| 227 | +Then verify the active config sees the Basic Memory commands: |
| 228 | + |
| 229 | +```bash |
| 230 | +python - <<'PY' |
| 231 | +import hermes_cli.plugins as p |
| 232 | +p._plugin_manager = None |
| 233 | +p._memory_provider_command_loads.clear() |
| 234 | +cmds = p.get_plugin_commands() |
| 235 | +print(sorted(k for k in cmds if k.startswith('bm-'))) |
| 236 | +PY |
| 237 | +``` |
| 238 | + |
| 239 | +Expected output: |
| 240 | + |
| 241 | +```text |
| 242 | +['bm-context', 'bm-project', 'bm-read', 'bm-recent', 'bm-remember', 'bm-search', 'bm-status', 'bm-workspace'] |
| 243 | +``` |
| 244 | + |
| 245 | +Finally restart the gateway so native slash commands are synced: |
| 246 | + |
| 247 | +```bash |
| 248 | +hermes gateway restart |
| 249 | +``` |
| 250 | + |
| 251 | +For Discord, global command propagation can lag briefly. If the commands do not show immediately, type `/bm` directly or reload the Discord client. |
0 commit comments