robrain 2.3.2 → 2.3.4

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 (38) hide show
  1. package/dist/commands/init-project.d.ts +2 -0
  2. package/dist/commands/init-project.d.ts.map +1 -1
  3. package/dist/commands/init-project.js +16 -0
  4. package/dist/commands/init-project.js.map +1 -1
  5. package/dist/commands/install.js +2 -2
  6. package/dist/commands/status.d.ts +0 -1
  7. package/dist/commands/status.d.ts.map +1 -1
  8. package/dist/commands/status.js +0 -1
  9. package/dist/commands/status.js.map +1 -1
  10. package/dist/commands/up.test.js +2 -2
  11. package/dist/index.js +21 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/claude-plugin.d.ts +17 -0
  14. package/dist/lib/claude-plugin.d.ts.map +1 -0
  15. package/dist/lib/claude-plugin.js +83 -0
  16. package/dist/lib/claude-plugin.js.map +1 -0
  17. package/dist/lib/claude-plugin.test.d.ts +2 -0
  18. package/dist/lib/claude-plugin.test.d.ts.map +1 -0
  19. package/dist/lib/claude-plugin.test.js +48 -0
  20. package/dist/lib/claude-plugin.test.js.map +1 -0
  21. package/dist/lib/hermes-plugin.d.ts +28 -0
  22. package/dist/lib/hermes-plugin.d.ts.map +1 -0
  23. package/dist/lib/hermes-plugin.js +90 -0
  24. package/dist/lib/hermes-plugin.js.map +1 -0
  25. package/dist/lib/hermes-plugin.test.d.ts +2 -0
  26. package/dist/lib/hermes-plugin.test.d.ts.map +1 -0
  27. package/dist/lib/hermes-plugin.test.js +54 -0
  28. package/dist/lib/hermes-plugin.test.js.map +1 -0
  29. package/dist/lib/mcp-bundle.test.js +2 -2
  30. package/package.json +3 -3
  31. package/scripts/vendor-hermes-plugin.mjs +26 -0
  32. package/scripts/verify-publish-tarball.mjs +22 -1
  33. package/vendor/hermes-plugin/robrain/README.md +103 -0
  34. package/vendor/hermes-plugin/robrain/__init__.py +428 -0
  35. package/vendor/hermes-plugin/robrain/client.py +168 -0
  36. package/vendor/hermes-plugin/robrain/plugin.yaml +3 -0
  37. package/vendor/sensing-mcp/dist/index.js +1 -1
  38. package/vendor/sensing-mcp/package.json +1 -1
@@ -0,0 +1,54 @@
1
+ // packages/cli/src/lib/hermes-plugin.test.ts
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, mkdtempSync, readdirSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { test } from 'node:test';
7
+ import { HermesPluginError, hermesPluginComplete, installHermesPlugin, resolveHermesHome, } from './hermes-plugin.js';
8
+ function makePluginSource(root) {
9
+ const src = join(root, 'src-plugin');
10
+ mkdirSync(src, { recursive: true });
11
+ writeFileSync(join(src, '__init__.py'), '# provider\n');
12
+ writeFileSync(join(src, 'client.py'), '# client\n');
13
+ writeFileSync(join(src, 'plugin.yaml'), 'name: robrain\n');
14
+ writeFileSync(join(src, 'README.md'), '# docs\n');
15
+ mkdirSync(join(src, '__pycache__'));
16
+ writeFileSync(join(src, '__pycache__', 'junk.pyc'), '');
17
+ return src;
18
+ }
19
+ test('resolveHermesHome prefers HERMES_HOME env', () => {
20
+ assert.equal(resolveHermesHome({ HERMES_HOME: '/custom/home' }), '/custom/home');
21
+ });
22
+ test('resolveHermesHome defaults under the user home when env unset', () => {
23
+ const resolved = resolveHermesHome({});
24
+ assert.ok(resolved.includes('hermes'));
25
+ });
26
+ test('installs plugin into <home>/plugins/robrain, excluding __pycache__', () => {
27
+ const root = mkdtempSync(join(tmpdir(), 'hermes-plugin-'));
28
+ const src = makePluginSource(root);
29
+ const home = join(root, 'hermes-home');
30
+ const { dest } = installHermesPlugin(home, src);
31
+ assert.equal(dest, join(home, 'plugins', 'robrain'));
32
+ assert.ok(hermesPluginComplete(dest));
33
+ assert.ok(!readdirSync(dest).includes('__pycache__'));
34
+ });
35
+ test('replaces an existing install, including a dangling symlink', () => {
36
+ const root = mkdtempSync(join(tmpdir(), 'hermes-plugin-'));
37
+ const src = makePluginSource(root);
38
+ const home = join(root, 'hermes-home');
39
+ mkdirSync(join(home, 'plugins'), { recursive: true });
40
+ symlinkSync(join(root, 'nowhere'), join(home, 'plugins', 'robrain'));
41
+ const { dest } = installHermesPlugin(home, src);
42
+ assert.ok(hermesPluginComplete(dest));
43
+ });
44
+ test('throws when the source is missing or incomplete', () => {
45
+ const root = mkdtempSync(join(tmpdir(), 'hermes-plugin-'));
46
+ const incomplete = join(root, 'incomplete');
47
+ mkdirSync(incomplete);
48
+ writeFileSync(join(incomplete, '__init__.py'), '');
49
+ assert.throws(() => installHermesPlugin(join(root, 'home'), incomplete), HermesPluginError);
50
+ // '' = "resolution found nothing" — passing undefined would re-trigger the
51
+ // default resolver, which legitimately finds the real plugin in a monorepo.
52
+ assert.throws(() => installHermesPlugin(join(root, 'home'), ''), HermesPluginError);
53
+ });
54
+ //# sourceMappingURL=hermes-plugin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-plugin.test.js","sourceRoot":"","sources":["../../src/lib/hermes-plugin.test.ts"],"names":[],"mappings":"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"}
@@ -53,11 +53,11 @@ describe('materializeSensingBundle', () => {
53
53
  it('re-copies when the package version changed', () => {
54
54
  writeFakePackage(pkgDir, '2.2.0', 'old');
55
55
  assert.equal(materializeSensingBundle(pkgDir, mcpDir), true);
56
- writeFakePackage(pkgDir, '2.3.2', 'new');
56
+ writeFakePackage(pkgDir, '2.3.4', 'new');
57
57
  assert.equal(materializeSensingBundle(pkgDir, mcpDir), true);
58
58
  assert.equal(readFileSync(join(mcpDir, 'sensing', 'dist', 'index.js'), 'utf8'), 'new');
59
59
  const marker = JSON.parse(readFileSync(join(mcpDir, 'sensing', 'package.json'), 'utf8'));
60
- assert.equal(marker.version, '2.3.2');
60
+ assert.equal(marker.version, '2.3.4');
61
61
  });
62
62
  it('replaces a symlinked destination from a previous --repo-root install', () => {
63
63
  // Old dev install: ~/.robrain/mcp/sensing -> clone/packages/sensing-mcp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robrain",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "type": "module",
5
5
  "description": "RoBrain CLI for installation, setup, and memory workflows",
6
6
  "author": "Rory Plans, Inc.",
@@ -42,10 +42,10 @@
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsc",
45
- "prepublishOnly": "pnpm build && node scripts/vendor-sensing-mcp.mjs",
45
+ "prepublishOnly": "pnpm build && node scripts/vendor-sensing-mcp.mjs && node scripts/vendor-hermes-plugin.mjs",
46
46
  "dev": "tsc --watch",
47
47
  "typecheck": "tsc --noEmit",
48
- "test": "pnpm build && node --test dist/lib/editor.codex.test.js dist/lib/mcp-bundle.test.js dist/commands/outcomes.test.js dist/commands/export-interchange.test.js dist/commands/up.test.js",
48
+ "test": "pnpm build && node --test dist/lib/editor.codex.test.js dist/lib/mcp-bundle.test.js dist/lib/claude-plugin.test.js dist/lib/hermes-plugin.test.js dist/commands/outcomes.test.js dist/commands/export-interchange.test.js dist/commands/up.test.js",
49
49
  "pack:verify": "node scripts/verify-publish-tarball.mjs",
50
50
  "postinstall": "node scripts/chmod-bin.cjs"
51
51
  },
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // Copy the Hermes memory-provider plugin into vendor/ so the published
3
+ // robrain tarball can materialize it via `robrain install --hermes`
4
+ // without a repo clone — same self-containment pattern as vendor-sensing-mcp.
5
+ import { cpSync, existsSync, mkdirSync, rmSync } from 'fs'
6
+ import { basename, dirname, join } from 'path'
7
+ import { fileURLToPath } from 'url'
8
+
9
+ const cliRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
10
+ const src = join(cliRoot, '..', '..', 'integrations', 'hermes', 'robrain')
11
+ const dest = join(cliRoot, 'vendor', 'hermes-plugin', 'robrain')
12
+
13
+ for (const required of ['__init__.py', 'client.py', 'plugin.yaml']) {
14
+ if (!existsSync(join(src, required))) {
15
+ console.error('vendor-hermes-plugin: missing', join(src, required))
16
+ process.exit(1)
17
+ }
18
+ }
19
+
20
+ rmSync(dest, { recursive: true, force: true })
21
+ mkdirSync(dirname(dest), { recursive: true })
22
+ cpSync(src, dest, {
23
+ recursive: true,
24
+ filter: p => basename(p) !== '__pycache__' && !p.endsWith('.pyc'),
25
+ })
26
+ console.log('Vendored Hermes plugin → packages/cli/vendor/hermes-plugin/robrain')
@@ -19,6 +19,7 @@ mkdirSync(work, { recursive: true })
19
19
 
20
20
  try {
21
21
  execFileSync('node', ['scripts/vendor-sensing-mcp.mjs'], { cwd: cliRoot, stdio: 'inherit' })
22
+ execFileSync('node', ['scripts/vendor-hermes-plugin.mjs'], { cwd: cliRoot, stdio: 'inherit' })
22
23
 
23
24
  execFileSync('pnpm', ['pack', '--pack-destination', work], { cwd: cliRoot, stdio: 'inherit' })
24
25
 
@@ -49,7 +50,27 @@ try {
49
50
  fail(`resolved dir has no dist/index.js: ${dir}`)
50
51
  }
51
52
 
52
- console.log(`pack:verify ok — vendor resolves from ${dir}`)
53
+ const hermesVendorEntry = join(extracted, 'vendor', 'hermes-plugin', 'robrain', '__init__.py')
54
+ if (!existsSync(hermesVendorEntry)) {
55
+ fail('vendor/hermes-plugin/robrain/__init__.py missing from tarball')
56
+ }
57
+
58
+ const hermesUrl = pathToFileURL(join(extracted, 'dist', 'lib', 'hermes-plugin.js')).href
59
+ const { installHermesPlugin, resolveHermesPluginSourceDir } = await import(hermesUrl)
60
+ const hermesSrc = resolveHermesPluginSourceDir()
61
+ // realpath both sides — on macOS the tmpdir is reached via the /var →
62
+ // /private/var symlink, so raw prefix comparison false-negatives.
63
+ const { realpathSync } = await import('fs')
64
+ if (!hermesSrc || !realpathSync(hermesSrc).startsWith(realpathSync(extracted))) {
65
+ fail(`resolveHermesPluginSourceDir() did not resolve inside the tarball: ${hermesSrc}`)
66
+ }
67
+ const hermesHome = join(work, 'hermes-home')
68
+ const { dest } = installHermesPlugin(hermesHome)
69
+ if (!existsSync(join(dest, 'plugin.yaml'))) {
70
+ fail(`installHermesPlugin() copy incomplete at ${dest}`)
71
+ }
72
+
73
+ console.log(`pack:verify ok — sensing vendor resolves from ${dir}; hermes plugin installs from ${hermesSrc}`)
53
74
  } finally {
54
75
  rmSync(work, { recursive: true, force: true })
55
76
  }
@@ -0,0 +1,103 @@
1
+ # RoBrain memory provider
2
+
3
+ [RoBrain](https://github.com/adelinamart/robrain) is self-hosted decision
4
+ memory: it passively captures what got decided in your sessions **and the
5
+ alternatives you explicitly ruled out**, storing each veto as a structured
6
+ `rejected[]` field (option + reason) in your own Postgres. Recall is ranked
7
+ by a 5-signal composite score (semantic similarity, file overlap, recency,
8
+ historical relevance, approval state). Nothing leaves your machine.
9
+
10
+ The differentiator over fact-style memory: a rejection survives as data, not
11
+ prose. When a task drifts toward something you already declined, the veto
12
+ arrives in context as an explicit warning with the original reason —
13
+ `REJECTED: Redis — second datastore to operate; ops budget is one datastore`.
14
+
15
+ ## What the provider does
16
+
17
+ | Hermes hook | RoBrain behavior |
18
+ |---|---|
19
+ | system prompt | Injects the project's always-on summary (top decisions + vetoes) at session start |
20
+ | prefetch | Background semantic retrieval per turn; rejected alternatives rendered as `REJECTED:` warnings |
21
+ | sync_turn | Ships each turn to Perception, which runs its own server-side decision extraction — no local LLM, no extra API key |
22
+ | `robrain_search` tool | On-demand decision + veto lookup before the agent commits to an approach |
23
+
24
+ Fail-open: if the Perception API is down, recall degrades to nothing and
25
+ writes are dropped with a log warning — the agent never blocks. Writes are
26
+ skipped in cron/subagent contexts so system prompts never pollute the
27
+ decision corpus. Previously injected memory context is stripped before
28
+ capture, so recall can't re-enter the corpus as a fresh "decision".
29
+
30
+ ## Setup
31
+
32
+ This is a **standalone Hermes plugin** — hermes-agent's contribution policy
33
+ asks memory providers to ship outside their tree, installed into
34
+ `$HERMES_HOME/plugins/` where Hermes' plugin discovery picks them up with
35
+ the same lifecycle hooks as bundled providers.
36
+
37
+ 1. Run RoBrain (~2 minutes — [install guide](https://github.com/adelinamart/robrain#install)):
38
+
39
+ ```bash
40
+ # No clone:
41
+ npx robrain@latest up
42
+ # Or from a clone:
43
+ git clone https://github.com/adelinamart/robrain && cd robrain
44
+ pnpm install && pnpm build && pnpm docker:up
45
+ ```
46
+
47
+ Add `ANTHROPIC_API_KEY` and your embedding key to `~/.robrain/stack/.env` (after `robrain up`) or the repo `.env` (clone path), then ensure Perception is healthy: `curl -sf http://localhost:3001/health`.
48
+
49
+ 2. Install the plugin — no clone needed; the published CLI carries it:
50
+
51
+ ```bash
52
+ npx robrain@latest install --hermes
53
+ ```
54
+
55
+ (`$HERMES_HOME` defaults to `~/.hermes` on macOS/Linux; the command
56
+ respects the env var. From a repo clone, `cp -r
57
+ integrations/hermes/robrain "${HERMES_HOME:-$HOME/.hermes}/plugins/robrain"`
58
+ does the same thing.)
59
+
60
+ 3. Point Hermes at it:
61
+
62
+ ```bash
63
+ hermes memory setup # select "robrain"
64
+ ```
65
+
66
+ You'll be asked for:
67
+ - **base_url** — Perception's URL (default `http://localhost:3001`)
68
+ - **project_id** — your RoBrain project (default `default`)
69
+ - **scope** — recorded on captured decisions (default `team`)
70
+ - **PERCEPTION_API_KEY** — from `~/.robrain/stack/.env` (after `robrain up`) or the repo `.env` (clone path)
71
+
72
+ Config lands in `$HERMES_HOME/robrain/config.json` (non-secrets) and the
73
+ profile env store (the key). Env fallbacks for headless setups:
74
+ `ROBRAIN_BASE_URL`, `ROBRAIN_PROJECT_ID`, `PERCEPTION_API_KEY`.
75
+
76
+ ## Notes
77
+
78
+ - Decisions captured from Hermes sessions land in the same Postgres store
79
+ RoBrain fills from Claude Code, Cursor, Copilot, and Codex CLI — one
80
+ shared decision memory across your tools. Review captures with
81
+ `npx robrain review`; batch judgment (contradictions, drift) with
82
+ `npx robrain synth`.
83
+ - Perception re-redacts secrets server-side before anything is embedded or
84
+ stored, and RoBrain supports a fully-local mode (OpenAI-compatible local
85
+ server for extraction + embeddings) if you want zero external LLM calls.
86
+ - Requires a reachable RoBrain deployment; there is no hosted default.
87
+
88
+ ## Running the tests
89
+
90
+ The test suite (`../test_robrain_provider.py`) runs against a hermes-agent
91
+ checkout, since it exercises the real `MemoryProvider` ABC:
92
+
93
+ ```bash
94
+ git clone https://github.com/NousResearch/hermes-agent
95
+ cp -r integrations/hermes/robrain hermes-agent/plugins/memory/robrain
96
+ cp integrations/hermes/test_robrain_provider.py hermes-agent/tests/plugins/memory/
97
+ cd hermes-agent && python -m pytest tests/plugins/memory/test_robrain_provider.py
98
+ ```
99
+
100
+ (21 tests, no network — the HTTP client is stubbed. Verified end-to-end
101
+ against a live RoBrain deployment on 2026-07-08: raw Hermes turn →
102
+ server-side extraction → decision with structured veto → recall through
103
+ both prefetch and the robrain_search tool.)