Skip to content
This repository was archived by the owner on May 31, 2026. It is now read-only.

feat: add /bm-* slash commands and bm_recent tool - #3

Merged
groksrc merged 4 commits into
mainfrom
feat/bm-slash-commands
May 11, 2026
Merged

feat: add /bm-* slash commands and bm_recent tool#3
groksrc merged 4 commits into
mainfrom
feat/bm-slash-commands

Conversation

@groksrc

@groksrc groksrc commented May 11, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds 8 plugin-owned slash commands for direct, in-session BM operations: /bm-search, /bm-read, /bm-context, /bm-recent, /bm-status, /bm-remember, /bm-project, /bm-workspace. Registered via ctx.register_command(...) with a hasattr guard so older Hermes installs silently skip the surface.
  • Adds bm_recent tool (curated wrapper around BM's recent_activity) so the agent can also surface recently-updated notes by timeframe.
  • Adds remember_folder config key (default bm-remember) — manual captures via /bm-remember land here, kept separate from auto session transcripts in capture_folder. Captures are tagged manual-capture; titles derived from the first non-empty line (≤80 chars), with a UTC timestamp fallback.

Read-only by design: /bm-project and /bm-workspace list but don't switch. Mid-session project/workspace switching is deferred to a follow-up to avoid auto-capture landing in the wrong place.

Closes #2

Test plan

  • Full unit suite passes (uv run --with pytest pytest) — 213 passed, 12 skipped (integration, gated on BM_INTEGRATION=1).
  • New tests/test_commands.py covers: registration via register_command, hasattr fallback for old Hermes, registration errors logged without breaking memory provider registration, per-handler usage strings, uninitialized state, happy path for each command, exception → plain-text error.
  • Existing schema-count assertions updated for 8 tools.
  • Smoke-test against a live Hermes session: install plugin, run each /bm-* command, confirm output renders verbatim and /bm-status reflects live state. (Manual — depends on a Hermes install.)
  • Integration tests (BM_INTEGRATION=1) — unchanged from prior; new slash handlers are exercised by unit suite against a fake MCP session.

🤖 Generated with Claude Code

groksrc and others added 3 commits May 11, 2026 00:16
Surface recently-updated notes through the curated Hermes tool map so
the agent can answer "what's been touched lately?" without a specific
search query. Maps to BM's recent_activity MCP tool; exposes a
trimmed parameter set (timeframe, limit, type).

Refs #2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugin-owned slash commands for in-session Basic Memory operations:
search, read, context, recent, status, remember, project, workspace.
Handlers close over the provider for live actor + config access, catch
their own exceptions so users see specific messages rather than the
generic "Plugin command error" line Hermes substitutes.

/bm-remember writes to a separate remember_folder (default
"bm-remember") so manual captures don't intermix with auto-generated
session transcripts in capture_folder. Tag "manual-capture"
disambiguates further. Title derives from the first non-empty line,
trimmed to 80 chars, with a UTC timestamp fallback.

/bm-workspace short-circuits in local mode with an explanatory line —
workspaces are a BM Cloud concept and the listing call would otherwise
return a generic "no workspaces" message.

Registration is gated on hasattr(ctx, "register_command") for parity
with the register_skill pattern; Hermes < v0.11.0 simply doesn't get
the slash-command surface.

Closes #2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CHANGELOG entry summarizes the slash-command surface, bm_recent tool,
and remember_folder config. README adds a Slash commands section and
the new config key. Plugin manifest + __version__ → 0.2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

groksrc commented May 11, 2026

Copy link
Copy Markdown
Member Author

Review notes from Codex:

  1. P1: /bm-* commands likely never register in real Hermes memory-provider loading.

    In __init__.py, command registration is guarded by hasattr(ctx, "register_command"), but this plugin is detected as an exclusive memory provider and loaded through Hermes's memory-provider collector rather than the general PluginContext. That collector currently only captures register_memory_provider and no-ops other registration methods, so the new branch silently skips in real installs. The unit tests use MagicMock, which masks this because every attribute exists.

    Suggested fix: either add/verify a Hermes-side collector path that exposes register_command for active memory providers, or move command registration to a supported plugin-loading path and add a test with a collector-shaped context that catches the silent skip.

  2. P2: /bm-recent drops real recent_activity(output_format="json") results.

    Basic Memory's recent_activity returns a bare list[dict] in JSON mode, but the handler only extracts rows from dict keys like results, items, activity, or primary_results. With the real tool shape, data is a list, results remains empty, and the command reports no activity even when there is activity.

    Suggested fix: handle isinstance(data, list) by assigning it directly to results, and add a test matching the real recent_activity JSON return shape.

  3. P3: PR title check is failing.

    semantic-pr-title failed while all Python matrix jobs passed. The current title likely needs a conventional prefix, e.g. feat: add /bm-* slash commands and bm_recent tool.

Verification: I ran uv run --with pytest pytest -q locally on feat/bm-slash-commands: 213 passed, 12 skipped. I did not run the gated BM_INTEGRATION=1 suite.

@groksrc groksrc changed the title Add /bm-* slash commands and bm_recent tool May 11, 2026
Two fixes for the issues raised on PR #3.

P1 — ctx.register_command and ctx.register_skill silently no-op in real
Hermes installs. Memory-provider plugins are loaded through a stripped-
down _ProviderCollector (plugins/memory/__init__.py) that captures only
register_memory_provider; other registration methods are not delegated
to PluginManager. The bundled SKILL.md has been hitting this same path
since 0.1.5 — the call ran but the entry never landed.

Workaround: write directly to PluginManager._plugin_commands and
_plugin_skills from inside register(). Mirror the exact entry shape,
name normalization, and built-in-conflict guard from
PluginContext.register_command (plugins.py:401-453) and
register_skill (plugins.py:622-665). When the upstream collector is
patched, ctx.register_command/register_skill will write identical
entries — the reach-in becomes a redundant idempotent overwrite.

Recursion is safe: PluginManager.discover_and_load is idempotent
(plugins.py:699) and explicitly skips memory-provider plugins at the
manifest-routing stage (plugins.py:792-802), so calling
_ensure_plugins_discovered() from inside register() cannot re-enter.

New tests in tests/test_commands.py use a _ProviderCollector-shaped
ctx (no register_command attribute) and assert the reach-in actually
populates the fake PluginManager. The original tests used MagicMock,
which masked the silent skip by making every attribute exist.

P2 — bm_recent dropped real results. recent_activity(output_format=
"json") returns a bare list[dict] per the BM signature; the handler
only checked dict keys. Added the list branch + regression test
matching the real JSON shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@groksrc

groksrc commented May 11, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review — addressed all three.

P1 (register_command silent skip): confirmed Codex is right. Hermes's _ProviderCollector (plugins/memory/__init__.py:288-305) only captures register_memory_provider; register_command and register_skill aren't delegated to PluginManager. The register_skill call this plugin shipped in 0.1.5 has been silently no-opping in real installs since then too.

Rather than waiting on an upstream patch, this commit reaches into PluginManager._plugin_commands and _plugin_skills directly from inside register(). Mirrors the exact entry shape, name normalization, and built-in-conflict guard from PluginContext.register_command (plugins.py:401-453) and register_skill (plugins.py:622-665) so the reach-in produces byte-identical entries to what the supported path would write.

Verified recursion is safe: PluginManager.discover_and_load is idempotent (plugins.py:699) and explicitly skips memory-provider plugins at the manifest-routing stage (plugins.py:792-802), so _ensure_plugins_discovered() cannot re-enter our register(). Read the actual flow in ~/code/hermes-agent to confirm.

Tests now use a _ProviderCollector-shaped ctx (no register_command attribute, no MagicMock) and assert entries land in a fake PluginManager. Eight new reach-in tests cover: command + skill landing, built-in conflict skip, missing hermes_cli.plugins (graceful degrade), missing internal attrs (forward-compat for refactors), and entry-shape parity with the supported path.

Forward-compat: ctx.register_command / ctx.register_skill calls remain. When the upstream collector is patched (small Hermes-core change, ~15 lines), both paths will write identical entries to the same dicts — idempotent.

P2 (bm-recent dropping results): recent_activity(output_format="json") returns list[dict] per its signature (-> str | list[dict]); my handler only looked for dict keys. Added the list branch + regression test pinning the real JSON shape.

P3 (PR title): renamed to feat: add /bm-* slash commands and bm_recent tool.

Suite: 221 passed, 12 skipped. Still haven't run BM_INTEGRATION=1 — same as before. Plan to file the upstream collector patch as a follow-up against NousResearch/hermes-agent; once it lands and propagates, this PR's reach-in can be removed in a future release.

@groksrc groksrc left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed commit 6513a63. The three items from my earlier pass look addressed:

  • P1: I checked the Hermes clone again: memory providers still load through _ProviderCollector, while slash commands route through PluginManager._plugin_commands. The new fallback writes the same command/skill registry shapes that PluginContext.register_command / register_skill use, and the tests now cover a collector-shaped context instead of relying on MagicMock.
  • P2: /bm-recent now handles Basic Memory's bare list[dict] JSON return shape, with a regression test.
  • P3: the semantic PR title check is now passing.

Verification:

  • Local: uv run --with pytest pytest -> 221 passed, 12 skipped
  • GitHub checks: Python 3.11/3.12/3.13/3.14 unit jobs pass; semantic-pr-title passes

No new blocking findings from this pass. Remaining caveat is expected: the PluginManager reach-in is private and temporary, so it should be removable once Hermes delegates register_command / register_skill from the memory-provider collector.

@groksrc
groksrc merged commit 3b1118c into main May 11, 2026
5 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

1 participant