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 feat: 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add bm-* slash commands for Basic Memory

1 participant