basemind-hermes-plugin 0.21.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. basemind_hermes_plugin-0.21.1/PKG-INFO +85 -0
  2. basemind_hermes_plugin-0.21.1/README.md +60 -0
  3. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/__init__.py +8 -0
  4. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/commands/bm-doctor.md +34 -0
  5. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/commands/bm-init.md +57 -0
  6. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/commands/bm-scan.md +38 -0
  7. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/commands/bm-stats.md +43 -0
  8. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/commands/bm-statusline.md +57 -0
  9. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/commands/bm.md +47 -0
  10. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/hermes.py +246 -0
  11. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/plugin.yaml +6 -0
  12. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind/SKILL.md +188 -0
  13. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind-cli/SKILL.md +162 -0
  14. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind-code-search/SKILL.md +77 -0
  15. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind-comms/SKILL.md +99 -0
  16. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind-doctor/SKILL.md +87 -0
  17. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind-documents/SKILL.md +79 -0
  18. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind-git-history/SKILL.md +62 -0
  19. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind-scan/SKILL.md +58 -0
  20. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin/skills/basemind-stats/SKILL.md +56 -0
  21. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin.egg-info/PKG-INFO +85 -0
  22. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin.egg-info/SOURCES.txt +26 -0
  23. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin.egg-info/dependency_links.txt +1 -0
  24. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin.egg-info/entry_points.txt +2 -0
  25. basemind_hermes_plugin-0.21.1/basemind_hermes_plugin.egg-info/top_level.txt +1 -0
  26. basemind_hermes_plugin-0.21.1/pyproject.toml +42 -0
  27. basemind_hermes_plugin-0.21.1/setup.cfg +4 -0
  28. basemind_hermes_plugin-0.21.1/tests/test_hermes_register.py +155 -0
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: basemind-hermes-plugin
3
+ Version: 0.21.1
4
+ Summary: Hermes Agent plugin for basemind — helper skills, slash commands, and agent-comms notifications over the basemind MCP server.
5
+ Author-email: Na'aman Hirschfeld <nhirschfeld@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://basemind.ai
8
+ Project-URL: Documentation, https://basemind.ai
9
+ Project-URL: Repository, https://github.com/Goldziher/basemind.git
10
+ Project-URL: Issues, https://github.com/Goldziher/basemind/issues
11
+ Keywords: mcp,hermes,agent-context,code-map,plugin
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Environment :: Console
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+
26
+ # basemind-hermes-plugin
27
+
28
+ The [Hermes Agent](https://github.com/NousResearch/hermes) plugin for
29
+ [basemind](https://github.com/Goldziher/basemind). It adds what MCP config alone cannot: basemind's
30
+ helper **skills**, **slash commands**, and agent-comms **notifications** (session-start context +
31
+ per-turn inbox deltas).
32
+
33
+ This package is pure-Python and stdlib-only. It does **not** install the `basemind` binary — install
34
+ that separately (see below). The plugin reaches basemind by shelling out to `basemind` on your
35
+ `PATH`, and every hook is fail-open: with no binary or a down comms broker it degrades to a no-op.
36
+
37
+ ## Prerequisites
38
+
39
+ 1. **The `basemind` binary on your `PATH`** — via any channel:
40
+
41
+ | Channel | Command |
42
+ |---|---|
43
+ | Homebrew | `brew install Goldziher/tap/basemind` |
44
+ | npm | `npm install -g basemind` |
45
+ | cargo | `cargo install basemind --features full --locked` |
46
+ | GitHub releases | [download a binary](https://github.com/Goldziher/basemind/releases) |
47
+
48
+ 2. **The basemind MCP server wired into Hermes** — this is what gives Hermes the 60+ tools. Add to
49
+ `~/.hermes/config.yaml`:
50
+
51
+ ```yaml
52
+ mcp_servers:
53
+ basemind:
54
+ command: basemind
55
+ args: [serve]
56
+ ```
57
+
58
+ ## Install the plugin
59
+
60
+ Install into the **same Python environment Hermes runs in** (Hermes discovers plugins through the
61
+ `hermes_agent.plugins` entry point):
62
+
63
+ ```bash
64
+ pip install basemind-hermes-plugin
65
+ ```
66
+
67
+ Then enable it (general plugins are opt-in):
68
+
69
+ ```bash
70
+ hermes plugins enable basemind
71
+ ```
72
+
73
+ Restart your Hermes session so it re-reads the config and loads the plugin.
74
+
75
+ ## What it registers
76
+
77
+ - **Skills** — `basemind`, `basemind-code-search`, `basemind-git-history`, `basemind-documents`,
78
+ `basemind-comms`, `basemind-cli`, `basemind-doctor`, `basemind-scan`, `basemind-stats`.
79
+ - **Slash commands** — `bm`, `bm-init`, `bm-scan`, `bm-doctor`, `bm-stats`, `bm-statusline`.
80
+ - **Hooks** — `on_session_start` (operating discipline + condensed comms inbox) and `pre_llm_call`
81
+ (per-turn agent-comms deltas). Best-effort; the MCP tools work regardless.
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,60 @@
1
+ # basemind-hermes-plugin
2
+
3
+ The [Hermes Agent](https://github.com/NousResearch/hermes) plugin for
4
+ [basemind](https://github.com/Goldziher/basemind). It adds what MCP config alone cannot: basemind's
5
+ helper **skills**, **slash commands**, and agent-comms **notifications** (session-start context +
6
+ per-turn inbox deltas).
7
+
8
+ This package is pure-Python and stdlib-only. It does **not** install the `basemind` binary — install
9
+ that separately (see below). The plugin reaches basemind by shelling out to `basemind` on your
10
+ `PATH`, and every hook is fail-open: with no binary or a down comms broker it degrades to a no-op.
11
+
12
+ ## Prerequisites
13
+
14
+ 1. **The `basemind` binary on your `PATH`** — via any channel:
15
+
16
+ | Channel | Command |
17
+ |---|---|
18
+ | Homebrew | `brew install Goldziher/tap/basemind` |
19
+ | npm | `npm install -g basemind` |
20
+ | cargo | `cargo install basemind --features full --locked` |
21
+ | GitHub releases | [download a binary](https://github.com/Goldziher/basemind/releases) |
22
+
23
+ 2. **The basemind MCP server wired into Hermes** — this is what gives Hermes the 60+ tools. Add to
24
+ `~/.hermes/config.yaml`:
25
+
26
+ ```yaml
27
+ mcp_servers:
28
+ basemind:
29
+ command: basemind
30
+ args: [serve]
31
+ ```
32
+
33
+ ## Install the plugin
34
+
35
+ Install into the **same Python environment Hermes runs in** (Hermes discovers plugins through the
36
+ `hermes_agent.plugins` entry point):
37
+
38
+ ```bash
39
+ pip install basemind-hermes-plugin
40
+ ```
41
+
42
+ Then enable it (general plugins are opt-in):
43
+
44
+ ```bash
45
+ hermes plugins enable basemind
46
+ ```
47
+
48
+ Restart your Hermes session so it re-reads the config and loads the plugin.
49
+
50
+ ## What it registers
51
+
52
+ - **Skills** — `basemind`, `basemind-code-search`, `basemind-git-history`, `basemind-documents`,
53
+ `basemind-comms`, `basemind-cli`, `basemind-doctor`, `basemind-scan`, `basemind-stats`.
54
+ - **Slash commands** — `bm`, `bm-init`, `bm-scan`, `bm-doctor`, `bm-stats`, `bm-statusline`.
55
+ - **Hooks** — `on_session_start` (operating discipline + condensed comms inbox) and `pre_llm_call`
56
+ (per-turn agent-comms deltas). Best-effort; the MCP tools work regardless.
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,8 @@
1
+ """basemind-hermes-plugin: Hermes Agent plugin for basemind (skills, slash commands, comms hooks)."""
2
+
3
+ __version__ = "0.21.1"
4
+
5
+ try:
6
+ from .hermes import register # noqa: F401
7
+ except Exception: # pragma: no cover - defensive: never break plugin load
8
+ pass
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: bm-doctor
3
+ description: Diagnose and recover basemind when it isn't working (MCP tools missing/erroring, "no index", dead server) — runs CLI checks and gives the client-specific way to reconnect the server.
4
+ ---
5
+
6
+ # bm-doctor — diagnose and recover basemind
7
+
8
+ Diagnose and recover basemind using the CLI (works even with no MCP server running).
9
+
10
+ ## When to use
11
+
12
+ basemind isn't behaving: MCP tools are missing or erroring, the statusline or a tool reports
13
+ "no index" / "no indexed files", results are empty when they shouldn't be, or the `basemind
14
+ serve` MCP server seems dead.
15
+
16
+ ## How to use
17
+
18
+ Invoke `/bm-doctor` (optional free-text detail, e.g. `/bm-doctor tools return no indexed files`).
19
+ It runs the checks below in order:
20
+
21
+ 1. Check the index: `basemind query status`.
22
+ 2. Check for a lock-holding server: `cat .basemind/.lock.meta`.
23
+ 3. Rebuild the index if needed: `basemind scan`.
24
+ 4. Reconnect the MCP server (client-specific — this is the only way to restart it).
25
+
26
+ ## Notes
27
+
28
+ - A stdio MCP server can't be restarted by an agent or by basemind itself; reconnecting it is
29
+ the MCP client's job. The CLI stays usable throughout.
30
+
31
+ ## See also
32
+
33
+ The `basemind-doctor` skill for the full step-by-step diagnostic workflow, lock-holder detection,
34
+ and log-reading guidance.
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: bm-init
3
+ description: Onboard (or refresh) basemind in this repo — write basemind.toml, gitignore the cache, and inject a "prefer basemind over grep/read/git" rules block into CLAUDE.md / AGENTS.md / ai-rulez.
4
+ argument-hint: [capabilities…]
5
+ ---
6
+
7
+ # bm-init — onboard basemind into this repo
8
+
9
+ Run `basemind init` so the repo has a committed `basemind.toml`, a gitignored `.basemind/` cache,
10
+ and a rules block that tells every agent to prefer basemind's MCP tools over grep, file reads, and
11
+ naked `git`. CLI and slash command share ONE implementation — this just drives `basemind init`
12
+ with the right non-interactive flags.
13
+
14
+ ## When to use
15
+
16
+ First time setting up basemind in a repo, or to refresh the rules block after enabling new
17
+ capabilities (documents/RAG, agent-comms, semantic search). Safe to re-run: it's idempotent.
18
+
19
+ ## How to use
20
+
21
+ 1. **Ask which capabilities matter** (one short question). The options are:
22
+ `code-search-navigation`, `code-mapping-architecture`, `git-history`, `agent-comms`,
23
+ `documents-rag`, `semantic-search`. If the user has no preference, enable all.
24
+
25
+ 2. **Run `basemind init` non-interactively** with the matching flags. Enable everything:
26
+
27
+ ```sh
28
+ basemind init --yes
29
+ ```
30
+
31
+ Narrow to a subset with repeatable `--with` (allow-list) or `--without` (subtract):
32
+
33
+ ```sh
34
+ basemind init --yes --with code-search-navigation --with git-history
35
+ ```
36
+
37
+ Steer where the rules land with `--rules-target <auto|claude|agents|ai-rulez|none>` (default
38
+ `auto`). Preview without writing using `--print`.
39
+
40
+ 3. **Report what changed** — which files were written or kept (`basemind.toml`, `.gitignore`, the
41
+ rules file), and whether the delimited block was created or updated in place.
42
+
43
+ ## Notes
44
+
45
+ - Source-of-truth detection (auto): `.ai-rulez/config.toml` present → writes
46
+ `.ai-rulez/rules/basemind-usage.md` (then tell the user to run `ai-rulez generate`; do NOT run
47
+ it for them). Else CLAUDE.md → AGENTS.md → create CLAUDE.md, wrapping the content in an
48
+ idempotent `<!-- BEGIN basemind … -->` … `<!-- END basemind -->` block that is replaced in
49
+ place on re-run, never duplicated. Content outside the markers is never touched.
50
+ - An existing `basemind.toml` is kept verbatim, never clobbered.
51
+ - If `basemind` isn't on `PATH`: use the plugin-managed cache binary or build a dev binary with
52
+ `cargo build --release` and use `./target/release/basemind`.
53
+
54
+ ## See also
55
+
56
+ The `bm-scan` command to build the index next, and the `basemind` skill for the full MCP tool
57
+ surface the rules block advertises.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: bm-scan
3
+ description: Build or refresh the basemind index by running `basemind scan` via the CLI — works without the MCP server (use it when basemind reports "no index" / "no indexed files").
4
+ argument-hint: [path]
5
+ ---
6
+
7
+ # bm-scan — build or refresh the basemind index
8
+
9
+ Run `basemind scan` via the CLI so the code map exists and is current.
10
+
11
+ ## When to use
12
+
13
+ basemind (or its statusline) reports "no index" / "no indexed files", an MCP tool returns empty
14
+ results that shouldn't be empty, or the index is stale after large changes.
15
+
16
+ ## How to use
17
+
18
+ ```sh
19
+ basemind scan ${ARGUMENTS:-}
20
+ ```
21
+
22
+ - No argument → full working-tree scan.
23
+ - A path argument (`/bm-scan src/mcp`) → scope the scan to that path (incremental).
24
+ - If `basemind` isn't on `PATH`: use the plugin-managed cache
25
+ (`${XDG_CACHE_HOME:-~/.cache}/basemind/bin/<version>/basemind`), or build a dev binary with
26
+ `cargo build --release` and use `./target/release/basemind`.
27
+
28
+ ## Notes
29
+
30
+ - Report files scanned / updated / skipped and elapsed time. Non-extractable files are
31
+ **skipped**, not failures.
32
+ - If a `basemind serve` MCP server already holds the store lock for this repo, `scan` errors on
33
+ the lock — use the `rescan` MCP tool instead, or stop the server first.
34
+
35
+ ## See also
36
+
37
+ The `basemind-scan` skill for the full workflow, binary-resolution order, and `extra_roots`
38
+ config for indexing directories outside the repo.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: bm-stats
3
+ description: Show the basemind dashboard — resource footprint (disk + RAM) and activity (tool calls, per-tool histogram, estimated tokens saved). Works with or without the MCP server.
4
+ argument-hint: [today|1h|24h|all]
5
+ ---
6
+
7
+ # bm-stats — basemind dashboard
8
+
9
+ Show a basemind dashboard with two sections: resource footprint (on-disk size + process RAM) and
10
+ activity (tool calls, per-tool histogram, estimated tokens saved).
11
+
12
+ ## When to use
13
+
14
+ The user asks "how much is basemind helping?", "show me basemind stats", or wants to check disk /
15
+ RAM usage.
16
+
17
+ ## How to use
18
+
19
+ Invoke `/bm-stats` (default window `today`) or `/bm-stats <today|1h|24h|all>`. Window: $ARGUMENTS
20
+
21
+ 1. **Resource footprint.** MCP tool `cache_stats`, or CLI `basemind cache stats` (add `--json` to
22
+ parse). Report: `total_bytes` (matches `du`), the per-component breakdown (blobs / views /
23
+ git-history / lance / git-cache / telemetry / other), and process RAM (`rss_bytes` +
24
+ `peak_rss_bytes`). If `blob_accounting_ok` is `false`, note that orphan accounting was skipped
25
+ (stale/unreadable index — re-scan to restore it); the sizes are still accurate.
26
+
27
+ 2. **Activity.** MCP tool `telemetry_summary`, or CLI `basemind telemetry --window <today|1h|24h|all>`
28
+ (add `--json`). Report call count, the per-tool histogram, and estimated tokens saved for the
29
+ window.
30
+
31
+ 3. **Render** both sections as a compact markdown dashboard.
32
+
33
+ ## Notes
34
+
35
+ - Prefer the MCP tools when connected, but don't depend on them — the CLI reads the same data
36
+ with no server. If a step's MCP tool isn't available, run its CLI equivalent instead of giving up.
37
+ - If neither MCP nor CLI is reachable, say so plainly and point at `/bm-doctor`.
38
+ - Always end with a one-sentence disclosure that the savings number is heuristic: tools without a
39
+ realistic baseline (memory, document search, git wrappers) report 0 saved.
40
+
41
+ ## See also
42
+
43
+ The `basemind-stats` skill for the render shape and the `--explain` breakdown.
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: bm-statusline
3
+ description: Enable the basemind status line in your Claude Code user settings (one-time setup).
4
+ ---
5
+
6
+ # bm-statusline — enable the basemind status line
7
+
8
+ Wire the basemind status line into the user's global Claude Code settings, then confirm it
9
+ renders.
10
+
11
+ ## When to use
12
+
13
+ Run this once per machine to enable the basemind status line in Claude Code. Re-run only if the
14
+ bar goes blank after an unusual settings edit.
15
+
16
+ ## How to use
17
+
18
+ Invoke `/bm-statusline`. Use your tools to do this directly — do not ask the user to hand-edit
19
+ any files.
20
+
21
+ 1. **Confirm the plugin is installed.** Check that at least one of these exists:
22
+ - `${CLAUDE_PLUGIN_ROOT}/.claude-plugin/statusline.sh` (if that var is set)
23
+ - `~/.claude/plugins/marketplaces/basemind/.claude-plugin/statusline.sh`
24
+ - a match of
25
+ `~/.claude/plugins/cache/basemind/basemind/*/.claude-plugin/statusline.sh`
26
+
27
+ If none exists, tell the user the basemind plugin isn't installed and stop.
28
+
29
+ 2. **Update `~/.claude/settings.json`** (treat a missing file as `{}`). Set its
30
+ `statusLine` key to this **version-independent resolver** — do NOT hardcode a
31
+ version-pinned path, which breaks on the next basemind update when the old
32
+ version dir is pruned:
33
+
34
+ ```json
35
+ { "type": "command",
36
+ "command": "bash -c 's=$(ls -d \"$HOME\"/.claude/plugins/cache/basemind/basemind/*/.claude-plugin/statusline.sh 2>/dev/null | sort -V | tail -1); [ -f \"$s\" ] || s=\"$HOME/.claude/plugins/marketplaces/basemind/.claude-plugin/statusline.sh\"; [ -f \"$s\" ] && exec bash \"$s\" || printf \"%s\" \"◆ basemind: run /bm-statusline\"'",
37
+ "refreshInterval": 5 }
38
+ ```
39
+
40
+ How it resolves, in order: the **highest-versioned** cached copy (`sort -V`, not
41
+ mtime — an older dir touched more recently must never win; this also makes the bar
42
+ track the newest version the moment `/plugin update` installs it), else the
43
+ marketplace clone, else a one-line hint so the bar is never blank. It re-resolves
44
+ at every render, so version bumps never break it and script improvements land
45
+ automatically. Copy the `command` string
46
+ **verbatim** (it self-resolves — `$HOME` is expanded by the `bash -c` it runs
47
+ under, not the settings field). Preserve every other key and verify the file is
48
+ still valid JSON afterward.
49
+
50
+ 3. **Confirm it renders** by running the resolver once:
51
+
52
+ ```bash
53
+ printf '{"workspace":{"current_dir":"%s"}}' "$PWD" | bash -c 's=$(ls -d "$HOME"/.claude/plugins/cache/basemind/basemind/*/.claude-plugin/statusline.sh 2>/dev/null | sort -V | tail -1); [ -f "$s" ] || s="$HOME/.claude/plugins/marketplaces/basemind/.claude-plugin/statusline.sh"; [ -f "$s" ] && exec bash "$s" || printf "%s" "◆ basemind: run /bm-statusline"'
54
+ ```
55
+
56
+ 4. Tell the user it's enabled, and that any other running sessions need a
57
+ relaunch to pick it up.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: bm
3
+ description: Ask basemind anything about the current codebase — outlines, refs, callers, git history, blame, diffs, docs, memory.
4
+ argument-hint: <question about the codebase>
5
+ ---
6
+
7
+ # bm — ask basemind anything about this codebase
8
+
9
+ Answer the user's question using the basemind MCP server instead of reading files or shelling
10
+ out to grep/git.
11
+
12
+ ## When to use
13
+
14
+ Invoke with a natural-language question about this repo's code, history, or documents —
15
+ `/bm <question>`. Use it instead of manually picking a tool when you just want an answer.
16
+
17
+ ## How to use
18
+
19
+ ```text
20
+ /bm where is MapCache defined?
21
+ /bm what calls process_file?
22
+ /bm who last touched src/scanner.rs?
23
+ ```
24
+
25
+ Route the question to the tool that answers it directly:
26
+
27
+ | Example question | Tool |
28
+ |---|---|
29
+ | "Where is X defined?" | `search_symbols` |
30
+ | "What calls X?" | `find_references` (any name) or `find_callers` (specific def) |
31
+ | "What's the shape of this file?" | `outline` (add `l2: true` for calls + docs) |
32
+ | "What changed recently?" | `recent_changes`, `commits_touching`, `symbol_history` |
33
+ | "Who last touched this?" | `blame_file` / `blame_symbol` |
34
+ | "Where's the churn?" | `hot_files` |
35
+ | "Search PDFs/docs in the repo by meaning?" | `search_documents` |
36
+ | "Recall something remembered earlier?" | `memory_get` / `memory_list` / `memory_search` |
37
+ | "Remember this for later sessions?" | `memory_put` (delete with `memory_delete`) |
38
+ | "Refresh the index after editing code?" | `rescan` (pass `paths: [...]` to limit the scope) |
39
+
40
+ ## Notes
41
+
42
+ - Answer with paths, line numbers, and signatures — read whole files only after a tool has
43
+ located the exact span you need.
44
+
45
+ ## See also
46
+
47
+ The `basemind` skill for the full tool-routing table and context-economy discipline.
@@ -0,0 +1,246 @@
1
+ """Hermes Agent plugin registration for basemind.
2
+
3
+ basemind's tools reach Hermes through an MCP server declared in ``~/.hermes/config.yaml``
4
+ (``mcp_servers.basemind``) — a Hermes plugin cannot declare an MCP server. This module adds
5
+ what MCP config cannot: the basemind helper *skills*, *slash commands*, and agent-comms
6
+ *notifications* (parity with the Gemini/OpenCode plugin surfaces).
7
+
8
+ Design constraints (all load-bearing):
9
+
10
+ * **stdlib-only** — the same package is imported by the ``basemind`` CLI, so this module must
11
+ not pull in Hermes or any third-party dependency at import time.
12
+ * **import-cheap and side-effect-free** at module load.
13
+ * **fail-open** — every registration and every hook is guarded; a missing ``basemind`` binary,
14
+ a down comms broker, or a Hermes build that lacks a given ``ctx`` method must degrade to a
15
+ no-op, never raise. A raising plugin would break both Hermes startup and the CLI.
16
+
17
+ Hermes calls :func:`register` with a ``PluginContext`` once, at load. The comms hooks close over
18
+ that context so they can call ``ctx.inject_message(...)`` when they fire.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import subprocess
26
+ import tempfile
27
+ from pathlib import Path
28
+
29
+ _PKG_DIR = Path(__file__).resolve().parent
30
+ _SKILLS_DIR = _PKG_DIR / "skills"
31
+ _COMMANDS_DIR = _PKG_DIR / "commands"
32
+
33
+ _DISCIPLINE = (
34
+ "basemind is available over MCP in this session — a tree-sitter code map + git context. "
35
+ "Prefer it over grep/read for structural and historical questions: its tools return paths, "
36
+ "line numbers, and signatures, not file bodies, so they cost a fraction of the tokens of "
37
+ "reading source. Default workflow: outline a file before opening it (then read only the span "
38
+ "you need); search_symbols instead of grep for a definition; find_references/find_callers "
39
+ "instead of grepping call sites; workspace_grep instead of shelling out to ripgrep; rescan "
40
+ "after edits instead of reconnecting. Do not re-read a file basemind already mapped."
41
+ )
42
+
43
+ _COMMS_TOOLS = (
44
+ "basemind first, shell/grep/git fallback — prefer basemind over grep, over naked git, and for "
45
+ "docs/RAG/NER, web crawl, and parsing. You are connected to basemind agent-comms — a shared "
46
+ "multi-agent chat. You have auto-joined every room scoped to this workspace. Levers: room_post "
47
+ "{room, subject, body, reply_to?} to send (always give a short subject; the body holds the "
48
+ "detail); room_history {room} and inbox_read to scan messages (these return front-matter only — "
49
+ "subject/from/id — never bodies, to stay token-frugal); message_get {message_id} to read one "
50
+ "body on demand; room_list to see rooms, room_join to join another. Prefer posting a concise "
51
+ "status/question over staying silent when collaborating."
52
+ )
53
+
54
+
55
+ def register(ctx) -> None:
56
+ """Register basemind's skills, slash commands, and comms hooks with Hermes.
57
+
58
+ Each capability group is independently guarded so a failure in one (or a ``ctx`` that
59
+ lacks a given ``register_*`` method) never prevents the others from registering.
60
+ """
61
+ for step in (_register_skills, _register_commands, _register_hooks):
62
+ try:
63
+ step(ctx)
64
+ except Exception: # pragma: no cover - defensive: never break plugin load
65
+ pass
66
+
67
+
68
+ def _register_skills(ctx) -> None:
69
+ reg = getattr(ctx, "register_skill", None)
70
+ if not callable(reg) or not _SKILLS_DIR.is_dir():
71
+ return
72
+ for skill_md in sorted(_SKILLS_DIR.glob("*/SKILL.md")):
73
+ _safe_call(reg, skill_md.parent.name, str(skill_md))
74
+
75
+
76
+ def _register_commands(ctx) -> None:
77
+ reg = getattr(ctx, "register_command", None)
78
+ if not callable(reg) or not _COMMANDS_DIR.is_dir():
79
+ return
80
+ for cmd_md in sorted(_COMMANDS_DIR.glob("*.md")):
81
+ body = _read_text(cmd_md)
82
+ if not body:
83
+ continue
84
+ name = cmd_md.stem
85
+ description = _front_matter_description(body) or f"basemind {name} command"
86
+ _safe_call(reg, name, _make_command_handler(body), description)
87
+
88
+
89
+ def _make_command_handler(body: str):
90
+ """A command handler returns the command's markdown (its agent instructions) verbatim.
91
+
92
+ These ``.md`` files are the same prompt definitions the Claude/Codex plugins expand, so
93
+ returning the body gives Hermes faithful slash-command parity without shelling anything.
94
+ """
95
+
96
+ def handler(_args: str = "") -> str:
97
+ return body
98
+
99
+ return handler
100
+
101
+
102
+ def _register_hooks(ctx) -> None:
103
+ reg = getattr(ctx, "register_hook", None)
104
+ inject = getattr(ctx, "inject_message", None)
105
+ if not callable(reg) or not callable(inject):
106
+ return
107
+
108
+ def _inject(text):
109
+ if not text:
110
+ return
111
+ if not _safe_call(inject, text):
112
+ _safe_call(inject, text, "user")
113
+
114
+ def on_session_start(**_kwargs):
115
+ _inject(_session_start_context(os.getcwd()))
116
+
117
+ def pre_llm_call(**kwargs):
118
+ sid = str(kwargs.get("task_id") or kwargs.get("session_id") or "default")
119
+ _inject(_delta_context(os.getcwd(), sid))
120
+
121
+ _safe_call(reg, "on_session_start", on_session_start)
122
+ _safe_call(reg, "pre_llm_call", pre_llm_call)
123
+
124
+
125
+ def _session_start_context(cwd: str) -> str:
126
+ """Boot context: the operating discipline, plus a condensed comms inbox if the broker responds."""
127
+ messages = _inbox_messages(cwd, 8)
128
+ if messages is None:
129
+ return _DISCIPLINE
130
+ if messages:
131
+ return (
132
+ f"{_DISCIPLINE} {_COMMS_TOOLS}\n"
133
+ "Recent messages (front-matter only; call message_get with an id to read a body):\n"
134
+ f"{_format_lines(messages)}"
135
+ )
136
+ return f"{_DISCIPLINE} {_COMMS_TOOLS} No messages in your rooms yet — post one to kick things off."
137
+
138
+
139
+ def _delta_context(cwd: str, session_id: str) -> str | None:
140
+ """Per-turn delta: inject only messages newer than this session's high-water mark."""
141
+ messages = _inbox_messages(cwd, 30)
142
+ if not messages:
143
+ return None
144
+ max_ts = max((_ts(m) for m in messages), default=0)
145
+ hwm_file = _hwm_path(session_id)
146
+ if not hwm_file.exists():
147
+ _write_text(hwm_file, str(max_ts))
148
+ return None
149
+ hwm = _read_int(hwm_file)
150
+ fresh = [m for m in messages if _ts(m) > hwm]
151
+ if not fresh:
152
+ return None
153
+ _write_text(hwm_file, str(max_ts))
154
+ return (
155
+ "New basemind agent-comms message(s) since your last turn (front-matter only — call "
156
+ "message_get with an id to read a body):\n"
157
+ f"{_format_lines(fresh)}\n"
158
+ "Reply with room_post {room, subject, body, reply_to:<id>} if a response is warranted."
159
+ )
160
+
161
+
162
+ def _inbox_messages(cwd: str, limit: int):
163
+ """Return the inbox message list (possibly empty), or ``None`` when the broker is unavailable."""
164
+ data = _run_basemind_json(["comms", "inbox", "--root", cwd, "--json", "--limit", str(limit)], timeout=6)
165
+ if data is None:
166
+ return None
167
+ messages = data.get("messages")
168
+ return messages if isinstance(messages, list) else []
169
+
170
+
171
+ def _run_basemind_json(args, timeout):
172
+ try:
173
+ proc = subprocess.run( # noqa: S603,S607 - fixed argv, no shell
174
+ ["basemind", *args],
175
+ capture_output=True,
176
+ text=True,
177
+ timeout=timeout,
178
+ )
179
+ except Exception:
180
+ return None
181
+ if proc.returncode != 0 or not proc.stdout.strip():
182
+ return None
183
+ try:
184
+ return json.loads(proc.stdout)
185
+ except Exception:
186
+ return None
187
+
188
+
189
+ def _format_lines(messages) -> str:
190
+ return "\n".join(f" • [{m.get('subject', '')}] from {m.get('from', '')} (id: {m.get('id', '')})" for m in messages)
191
+
192
+
193
+ def _ts(message) -> int:
194
+ try:
195
+ return int(message.get("ts_micros") or 0)
196
+ except (TypeError, ValueError):
197
+ return 0
198
+
199
+
200
+ def _hwm_path(session_id: str) -> Path:
201
+ safe = "".join(c if (c.isalnum() or c in "._-") else "_" for c in session_id) or "default"
202
+ return Path(tempfile.gettempdir()) / f"basemind-comms-hwm-{safe}"
203
+
204
+
205
+ def _front_matter_description(body: str) -> str | None:
206
+ """Pull ``description:`` from a leading ``---`` YAML front-matter block, if present."""
207
+ if not body.startswith("---"):
208
+ return None
209
+ end = body.find("\n---", 3)
210
+ if end == -1:
211
+ return None
212
+ for line in body[3:end].splitlines():
213
+ stripped = line.strip()
214
+ if stripped.startswith("description:"):
215
+ return stripped[len("description:") :].strip().strip("\"'") or None
216
+ return None
217
+
218
+
219
+ def _safe_call(fn, *args) -> bool:
220
+ """Call ``fn(*args)``; return True on success, False on any exception. Never raises."""
221
+ try:
222
+ fn(*args)
223
+ return True
224
+ except Exception:
225
+ return False
226
+
227
+
228
+ def _read_text(path: Path) -> str:
229
+ try:
230
+ return path.read_text(encoding="utf-8", errors="replace")
231
+ except Exception:
232
+ return ""
233
+
234
+
235
+ def _write_text(path: Path, text: str) -> None:
236
+ try:
237
+ path.write_text(text, encoding="utf-8")
238
+ except Exception:
239
+ pass
240
+
241
+
242
+ def _read_int(path: Path) -> int:
243
+ try:
244
+ return int(path.read_text(encoding="utf-8").strip() or "0")
245
+ except Exception:
246
+ return 0