robrain 2.3.3 → 2.3.5
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.
- package/dist/commands/install.js +2 -2
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +0 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/up.test.js +2 -2
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/hermes-plugin.d.ts +28 -0
- package/dist/lib/hermes-plugin.d.ts.map +1 -0
- package/dist/lib/hermes-plugin.js +90 -0
- package/dist/lib/hermes-plugin.js.map +1 -0
- package/dist/lib/hermes-plugin.test.d.ts +2 -0
- package/dist/lib/hermes-plugin.test.d.ts.map +1 -0
- package/dist/lib/hermes-plugin.test.js +54 -0
- package/dist/lib/hermes-plugin.test.js.map +1 -0
- package/dist/lib/mcp-bundle.test.js +2 -2
- package/package.json +11 -10
- package/scripts/vendor-hermes-plugin.mjs +26 -0
- package/scripts/verify-publish-tarball.mjs +22 -1
- package/vendor/hermes-plugin/robrain/README.md +103 -0
- package/vendor/hermes-plugin/robrain/__init__.py +440 -0
- package/vendor/hermes-plugin/robrain/client.py +168 -0
- package/vendor/hermes-plugin/robrain/plugin.yaml +3 -0
- package/vendor/sensing-mcp/dist/index.js +1 -1
- package/vendor/sensing-mcp/package.json +1 -1
package/dist/commands/install.js
CHANGED
|
@@ -202,7 +202,7 @@ export async function installCommand(opts) {
|
|
|
202
202
|
planningKey: provisioned.planningKey,
|
|
203
203
|
embeddingProvider: embeddingProvider ?? 'openai',
|
|
204
204
|
installedAt: new Date().toISOString(),
|
|
205
|
-
version: '2.3.
|
|
205
|
+
version: '2.3.5',
|
|
206
206
|
});
|
|
207
207
|
spinner.succeed('MCP servers configured');
|
|
208
208
|
// ── Done ───────────────────────────────────────────────────
|
|
@@ -352,7 +352,7 @@ async function installSelfHosted(opts) {
|
|
|
352
352
|
...(perceptionKey ? { perceptionKey } : {}),
|
|
353
353
|
embeddingProvider: provider,
|
|
354
354
|
installedAt: new Date().toISOString(),
|
|
355
|
-
version: '2.3.
|
|
355
|
+
version: '2.3.5',
|
|
356
356
|
selfHosted: true,
|
|
357
357
|
});
|
|
358
358
|
spinner.succeed('MCP servers configured for self-hosted mode');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAsEnD;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAsEnD;AAMD,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,GAAG,CAAC,EAAK,MAAM,CAAA;IACf,IAAI,CAAC,EAAI,OAAO,CAAA;IAChB,IAAI,CAAC,EAAI,MAAM,CAAA;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwFhB;AAKD,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAMnD"}
|
package/dist/commands/status.js
CHANGED
|
@@ -74,7 +74,6 @@ export async function statusCommand() {
|
|
|
74
74
|
// ─────────────────────────────────────────────────────────────
|
|
75
75
|
// robrain rule --add TEXT [--type always_include|always_exclude|preference]
|
|
76
76
|
// --list
|
|
77
|
-
// --remove ID
|
|
78
77
|
export async function ruleCommand(opts) {
|
|
79
78
|
console.log();
|
|
80
79
|
if (!isAuthenticated()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/up.test.js
CHANGED
|
@@ -2,9 +2,9 @@ import { describe, it } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { ensureStackEnvContent, readEnvValue, renderStackCompose, DEFAULT_IMAGE_REPO } from './up.js';
|
|
4
4
|
describe('renderStackCompose', () => {
|
|
5
|
-
const compose = renderStackCompose(`${DEFAULT_IMAGE_REPO}:2.3.
|
|
5
|
+
const compose = renderStackCompose(`${DEFAULT_IMAGE_REPO}:2.3.5`);
|
|
6
6
|
it('runs Perception from the published image with an env override hook', () => {
|
|
7
|
-
assert.match(compose, /image: \$\{PERCEPTION_IMAGE:-ghcr\.io\/adelinamart\/robrain-perception:2\.3\.
|
|
7
|
+
assert.match(compose, /image: \$\{PERCEPTION_IMAGE:-ghcr\.io\/adelinamart\/robrain-perception:2\.3\.5\}/);
|
|
8
8
|
assert.doesNotMatch(compose, /build:/);
|
|
9
9
|
});
|
|
10
10
|
it('mounts the stack-local schema and env file', () => {
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// init-project Warm-start memory from existing codebase
|
|
10
10
|
// status Show auth status + service health
|
|
11
11
|
// doctor Diagnose install, editor wiring, keys, and service health
|
|
12
|
-
// rule Add / list
|
|
12
|
+
// rule Add / list explicit Planning rules
|
|
13
13
|
// logout Clear local credentials
|
|
14
14
|
// ─────────────────────────────────────────────────────────────
|
|
15
15
|
import { program } from 'commander';
|
|
@@ -27,7 +27,8 @@ import { outcomesScanCommand, outcomesRecordCommand } from './commands/outcomes.
|
|
|
27
27
|
import { explainCommand } from './commands/explain.js';
|
|
28
28
|
import { projectsListCommand, projectsMergeCommand } from './commands/projects.js';
|
|
29
29
|
import { upCommand, downCommand, DEFAULT_IMAGE_REPO } from './commands/up.js';
|
|
30
|
-
|
|
30
|
+
import { installHermesPlugin, resolveHermesHome } from './lib/hermes-plugin.js';
|
|
31
|
+
const VERSION = '2.3.5';
|
|
31
32
|
program
|
|
32
33
|
.name('robrain')
|
|
33
34
|
.description('Institutional memory for AI coding agents — by Rory Plans')
|
|
@@ -150,10 +151,26 @@ program
|
|
|
150
151
|
.option('-t, --token <token>', 'Rory Plans API token (or set RORY_TOKEN env var)')
|
|
151
152
|
.option('-e, --editor <editor>', 'Target editor: claude-code | cursor | copilot | codex')
|
|
152
153
|
.option('--self-hosted', 'Self-hosted mode — skip Rory Plans auth')
|
|
154
|
+
.option('--hermes', 'Install the Hermes memory-provider plugin into $HERMES_HOME/plugins')
|
|
153
155
|
.option('--perception-url <url>', 'Perception URL for self-hosted mode (default: http://localhost:3001)')
|
|
154
156
|
.option('--repo-root <path>', 'Path to your robrain git clone (links built sensing-mcp into ~/.robrain/mcp). Or set ROBRAIN_REPO.')
|
|
155
157
|
.option('--skip-init-project', 'Do not run init-project in the current directory after install')
|
|
156
158
|
.action(async (opts) => {
|
|
159
|
+
if (opts.hermes) {
|
|
160
|
+
const { dest } = installHermesPlugin();
|
|
161
|
+
console.log(`✔ Hermes memory-provider plugin installed → ${dest}`);
|
|
162
|
+
console.log('\nNext steps:');
|
|
163
|
+
console.log(' 1. hermes memory setup # select "robrain"');
|
|
164
|
+
console.log(' 2. PERCEPTION_API_KEY lives in ~/.robrain/stack/.env (after `robrain up`)');
|
|
165
|
+
console.log(' or your repo .env (clone path)');
|
|
166
|
+
console.log(`\nHermes home: ${resolveHermesHome()} (override with HERMES_HOME)`);
|
|
167
|
+
// --hermes alone is a complete install; editor wiring only runs when
|
|
168
|
+
// another install intent is present.
|
|
169
|
+
if (!opts.token && !opts.selfHosted && !opts.editor && !opts.repoRoot && !process.env.RORY_TOKEN) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
157
174
|
await installCommand({
|
|
158
175
|
token: opts.token ?? process.env.RORY_TOKEN,
|
|
159
176
|
editor: opts.editor,
|
|
@@ -209,7 +226,6 @@ program
|
|
|
209
226
|
.description('Manage explicit Planning rules (Rory Plans cloud Planning API — not OSS Perception)')
|
|
210
227
|
.option('--add <text>', 'Add a new rule in plain language')
|
|
211
228
|
.option('--list', 'List active rules for this project')
|
|
212
|
-
.option('--remove <id>', 'Remove a rule by ID')
|
|
213
229
|
.option('--type <type>', 'Rule type: always_include | always_exclude | preference (default: preference)')
|
|
214
230
|
.action(async (opts) => {
|
|
215
231
|
await ruleCommand(opts);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,eAAe;AACf,gEAAgE;AAChE,4BAA4B;AAC5B,kCAAkC;AAClC,EAAE;AACF,YAAY;AACZ,qEAAqE;AACrE,4DAA4D;AAC5D,qDAAqD;AACrD,6EAA6E;AAC7E
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare class HermesPluginError extends Error {
|
|
2
|
+
name: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Hermes home resolution — mirrors hermes_constants.get_hermes_home():
|
|
6
|
+
* HERMES_HOME env var, else the platform-native default (~/.hermes on
|
|
7
|
+
* macOS/Linux, %LOCALAPPDATA%\hermes on Windows).
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveHermesHome(env?: NodeJS.ProcessEnv): string;
|
|
10
|
+
export declare function hermesPluginComplete(dir: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Locate the plugin source shipped with this CLI.
|
|
13
|
+
* Published tarballs carry it under vendor/hermes-plugin/robrain;
|
|
14
|
+
* monorepo dev resolves integrations/hermes/robrain from the repo root.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveHermesPluginSourceDir(): string | undefined;
|
|
17
|
+
export interface HermesInstallResult {
|
|
18
|
+
dest: string;
|
|
19
|
+
hermesHome: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Copy the plugin into `<hermesHome>/plugins/robrain`, replacing whatever is
|
|
23
|
+
* there (including dangling symlinks and half-finished copies). Throws
|
|
24
|
+
* HermesPluginError when no complete source ships with this CLI build.
|
|
25
|
+
* Exposed with an explicit `sourceDir` for tests.
|
|
26
|
+
*/
|
|
27
|
+
export declare function installHermesPlugin(hermesHome?: string, sourceDir?: string | undefined): HermesInstallResult;
|
|
28
|
+
//# sourceMappingURL=hermes-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hermes-plugin.d.ts","sourceRoot":"","sources":["../../src/lib/hermes-plugin.ts"],"names":[],"mappings":"AAgBA,qBAAa,iBAAkB,SAAQ,KAAK;IACjC,IAAI,SAAsB;CACpC;AAiBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAQ9E;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,GAAG,SAAS,CASjE;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,GAAE,MAA4B,EACxC,SAAS,GAAE,MAAM,GAAG,SAA0C,GAC7D,mBAAmB,CAoBrB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// packages/cli/src/lib/hermes-plugin.ts
|
|
2
|
+
// Materialize the Hermes memory-provider plugin into $HERMES_HOME/plugins.
|
|
3
|
+
//
|
|
4
|
+
// hermes-agent's contribution policy keeps memory providers out of their
|
|
5
|
+
// tree — providers install as standalone plugins under ~/.hermes/plugins/,
|
|
6
|
+
// where Hermes' discovery loads them with the same lifecycle hooks as
|
|
7
|
+
// bundled ones. `robrain install --hermes` performs that copy so no repo
|
|
8
|
+
// clone is needed: published tarballs carry the plugin under
|
|
9
|
+
// vendor/hermes-plugin (same self-containment pattern as the vendored
|
|
10
|
+
// sensing-mcp bundle); monorepo dev resolves integrations/hermes.
|
|
11
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, rmSync, unlinkSync } from 'fs';
|
|
12
|
+
import { homedir, platform } from 'os';
|
|
13
|
+
import { basename, dirname, join } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
export class HermesPluginError extends Error {
|
|
16
|
+
name = 'HermesPluginError';
|
|
17
|
+
}
|
|
18
|
+
/** Files that must exist for a plugin source dir to be considered complete. */
|
|
19
|
+
const REQUIRED_FILES = ['__init__.py', 'client.py', 'plugin.yaml'];
|
|
20
|
+
/** Remove whatever sits at `dest`, including a dangling symlink (see mcp-bundle.ts). */
|
|
21
|
+
function removeDest(dest) {
|
|
22
|
+
let stats;
|
|
23
|
+
try {
|
|
24
|
+
stats = lstatSync(dest);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (stats.isSymbolicLink())
|
|
30
|
+
unlinkSync(dest);
|
|
31
|
+
else
|
|
32
|
+
rmSync(dest, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Hermes home resolution — mirrors hermes_constants.get_hermes_home():
|
|
36
|
+
* HERMES_HOME env var, else the platform-native default (~/.hermes on
|
|
37
|
+
* macOS/Linux, %LOCALAPPDATA%\hermes on Windows).
|
|
38
|
+
*/
|
|
39
|
+
export function resolveHermesHome(env = process.env) {
|
|
40
|
+
const fromEnv = env.HERMES_HOME?.trim();
|
|
41
|
+
if (fromEnv)
|
|
42
|
+
return fromEnv;
|
|
43
|
+
if (platform() === 'win32') {
|
|
44
|
+
const localAppData = env.LOCALAPPDATA?.trim();
|
|
45
|
+
return join(localAppData || join(homedir(), 'AppData', 'Local'), 'hermes');
|
|
46
|
+
}
|
|
47
|
+
return join(homedir(), '.hermes');
|
|
48
|
+
}
|
|
49
|
+
export function hermesPluginComplete(dir) {
|
|
50
|
+
return REQUIRED_FILES.every(f => existsSync(join(dir, f)));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Locate the plugin source shipped with this CLI.
|
|
54
|
+
* Published tarballs carry it under vendor/hermes-plugin/robrain;
|
|
55
|
+
* monorepo dev resolves integrations/hermes/robrain from the repo root.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveHermesPluginSourceDir() {
|
|
58
|
+
// Compiled location: <cli-root>/dist/lib/hermes-plugin.js — cli root is two up.
|
|
59
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
60
|
+
const cliRoot = join(here, '..', '..');
|
|
61
|
+
const candidates = [
|
|
62
|
+
join(cliRoot, 'vendor', 'hermes-plugin', 'robrain'), // published tarball
|
|
63
|
+
join(cliRoot, '..', '..', 'integrations', 'hermes', 'robrain'), // monorepo dev
|
|
64
|
+
];
|
|
65
|
+
return candidates.find(hermesPluginComplete);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Copy the plugin into `<hermesHome>/plugins/robrain`, replacing whatever is
|
|
69
|
+
* there (including dangling symlinks and half-finished copies). Throws
|
|
70
|
+
* HermesPluginError when no complete source ships with this CLI build.
|
|
71
|
+
* Exposed with an explicit `sourceDir` for tests.
|
|
72
|
+
*/
|
|
73
|
+
export function installHermesPlugin(hermesHome = resolveHermesHome(), sourceDir = resolveHermesPluginSourceDir()) {
|
|
74
|
+
if (!sourceDir || !hermesPluginComplete(sourceDir)) {
|
|
75
|
+
throw new HermesPluginError('Hermes plugin source not found in this CLI build — expected vendor/hermes-plugin (published) or integrations/hermes (repo clone).');
|
|
76
|
+
}
|
|
77
|
+
const pluginsDir = join(hermesHome, 'plugins');
|
|
78
|
+
const dest = join(pluginsDir, 'robrain');
|
|
79
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
80
|
+
removeDest(dest);
|
|
81
|
+
cpSync(sourceDir, dest, {
|
|
82
|
+
recursive: true,
|
|
83
|
+
filter: src => basename(src) !== '__pycache__' && !src.endsWith('.pyc'),
|
|
84
|
+
});
|
|
85
|
+
if (!hermesPluginComplete(dest)) {
|
|
86
|
+
throw new HermesPluginError(`Hermes plugin copy to ${dest} is incomplete — re-run the install.`);
|
|
87
|
+
}
|
|
88
|
+
return { dest, hermesHome };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=hermes-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hermes-plugin.js","sourceRoot":"","sources":["../../src/lib/hermes-plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hermes-plugin.test.d.ts","sourceRoot":"","sources":["../../src/lib/hermes-plugin.test.ts"],"names":[],"mappings":""}
|
|
@@ -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.
|
|
56
|
+
writeFakePackage(pkgDir, '2.3.5', '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.
|
|
60
|
+
assert.equal(marker.version, '2.3.5');
|
|
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.
|
|
3
|
+
"version": "2.3.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "RoBrain CLI for installation, setup, and memory workflows",
|
|
6
6
|
"author": "Rory Plans, Inc.",
|
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"prepublishOnly": "pnpm build && node scripts/vendor-sensing-mcp.mjs && node scripts/vendor-hermes-plugin.mjs",
|
|
46
|
+
"dev": "tsc --watch",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
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
|
+
"pack:verify": "node scripts/verify-publish-tarball.mjs",
|
|
50
|
+
"postinstall": "node scripts/chmod-bin.cjs"
|
|
51
|
+
},
|
|
43
52
|
"dependencies": {
|
|
44
53
|
"dotenv": "^16.4.5",
|
|
45
54
|
"chalk": "^5.3.0",
|
|
@@ -51,13 +60,5 @@
|
|
|
51
60
|
"typescript": "^5.4.0",
|
|
52
61
|
"@types/node": "^20.0.0",
|
|
53
62
|
"@types/prompts": "^2.4.9"
|
|
54
|
-
},
|
|
55
|
-
"scripts": {
|
|
56
|
-
"build": "tsc",
|
|
57
|
-
"dev": "tsc --watch",
|
|
58
|
-
"typecheck": "tsc --noEmit",
|
|
59
|
-
"test": "pnpm build && node --test dist/lib/editor.codex.test.js dist/lib/mcp-bundle.test.js dist/lib/claude-plugin.test.js dist/commands/outcomes.test.js dist/commands/export-interchange.test.js dist/commands/up.test.js",
|
|
60
|
-
"pack:verify": "node scripts/verify-publish-tarball.mjs",
|
|
61
|
-
"postinstall": "node scripts/chmod-bin.cjs"
|
|
62
63
|
}
|
|
63
|
-
}
|
|
64
|
+
}
|
|
@@ -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
|
-
|
|
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.)
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""RoBrain memory plugin — decision memory with structured rejected alternatives.
|
|
2
|
+
|
|
3
|
+
RoBrain (https://github.com/adelinamart/robrain) is self-hosted decision
|
|
4
|
+
memory for agents: it passively captures what got decided in a session AND
|
|
5
|
+
the alternatives that were explicitly ruled out, storing each veto as a
|
|
6
|
+
structured ``rejected[]`` field (option + reason) in the user's own
|
|
7
|
+
Postgres. The point is preventing an agent from re-proposing an approach
|
|
8
|
+
the user already rejected — the veto arrives in context as an explicit
|
|
9
|
+
warning, with the original reason.
|
|
10
|
+
|
|
11
|
+
Integration shape:
|
|
12
|
+
|
|
13
|
+
system prompt ← the project's always-on summary (session-start recall)
|
|
14
|
+
prefetch ← composite-scored semantic retrieval; rejected
|
|
15
|
+
alternatives rendered as REJECTED warnings
|
|
16
|
+
sync_turn ← raw turns shipped to Perception, which runs its own
|
|
17
|
+
server-side decision extraction (no local LLM needed)
|
|
18
|
+
robrain_search ← on-demand tool for decision + veto lookup
|
|
19
|
+
|
|
20
|
+
Config chain:
|
|
21
|
+
1. $HERMES_HOME/robrain/config.json (base_url, project_id, scope)
|
|
22
|
+
2. Environment: PERCEPTION_API_KEY (secret), ROBRAIN_BASE_URL,
|
|
23
|
+
ROBRAIN_PROJECT_ID
|
|
24
|
+
|
|
25
|
+
Fail-open by design: if the Perception API is down, recall degrades to
|
|
26
|
+
nothing and writes are dropped with a warning — the agent never blocks.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
import os
|
|
34
|
+
import queue
|
|
35
|
+
import threading
|
|
36
|
+
import time
|
|
37
|
+
from datetime import datetime, timezone
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any, Dict, List, Optional
|
|
40
|
+
|
|
41
|
+
from agent.memory_manager import sanitize_context
|
|
42
|
+
from agent.memory_provider import MemoryProvider
|
|
43
|
+
from tools.registry import tool_error
|
|
44
|
+
|
|
45
|
+
from .client import RoBrainClient, RoBrainClientError
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
_CONFIG_DIRNAME = "robrain"
|
|
50
|
+
_CONFIG_FILENAME = "config.json"
|
|
51
|
+
|
|
52
|
+
# Turns shorter than this carry no decision signal — don't ship them.
|
|
53
|
+
_MIN_TURN_CHARS = 20
|
|
54
|
+
# Cap what we send per turn; Perception enforces its own MAX_TURN_TEXT too.
|
|
55
|
+
_MAX_TURN_CHARS = 20_000
|
|
56
|
+
# Shutdown flush window — one POST /signals rides a server-side LLM
|
|
57
|
+
# extraction, so the last turn of a short session needs real time to land.
|
|
58
|
+
_SHUTDOWN_DRAIN_S = 25.0
|
|
59
|
+
|
|
60
|
+
SEARCH_SCHEMA = {
|
|
61
|
+
"name": "robrain_search",
|
|
62
|
+
"description": (
|
|
63
|
+
"Search the team's RoBrain decision memory. Returns prior decisions "
|
|
64
|
+
"ranked by relevance, each with its rationale and — critically — the "
|
|
65
|
+
"alternatives that were explicitly REJECTED and why. Use before "
|
|
66
|
+
"proposing a tool, library, architecture, or approach, to avoid "
|
|
67
|
+
"re-suggesting something the user already ruled out."
|
|
68
|
+
),
|
|
69
|
+
"parameters": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"query": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "What you are about to decide or propose (natural language).",
|
|
75
|
+
},
|
|
76
|
+
"limit": {
|
|
77
|
+
"type": "integer",
|
|
78
|
+
"description": "Max decisions to return (default 5, max 20).",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"required": ["query"],
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _utc_now_iso() -> str:
|
|
87
|
+
return datetime.now(timezone.utc).isoformat()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _format_decisions(rows: List[Dict[str, Any]]) -> str:
|
|
91
|
+
"""Render decisions the way RoBrain's own inject does — vetoes as warnings."""
|
|
92
|
+
lines: List[str] = []
|
|
93
|
+
for row in rows:
|
|
94
|
+
decision = str(row.get("decision") or "").strip()
|
|
95
|
+
if not decision:
|
|
96
|
+
continue
|
|
97
|
+
rationale = str(row.get("rationale") or "").strip()
|
|
98
|
+
lines.append(f"- {decision}" + (f" — {rationale}" if rationale else ""))
|
|
99
|
+
for veto in row.get("rejected") or []:
|
|
100
|
+
option = str(veto.get("option") or "").strip()
|
|
101
|
+
reason = str(veto.get("reason") or "").strip()
|
|
102
|
+
if option:
|
|
103
|
+
lines.append(f" - REJECTED: {option}" + (f" — {reason}" if reason else ""))
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RoBrainProvider(MemoryProvider):
|
|
108
|
+
"""MemoryProvider backed by a self-hosted RoBrain Perception API."""
|
|
109
|
+
|
|
110
|
+
def __init__(self) -> None:
|
|
111
|
+
self._client: Any = None
|
|
112
|
+
self._config: Dict[str, Any] = {}
|
|
113
|
+
self._hermes_home: str = ""
|
|
114
|
+
self._session_id: str = ""
|
|
115
|
+
self._writes_enabled = True
|
|
116
|
+
self._degraded = False
|
|
117
|
+
|
|
118
|
+
self._summary: str = ""
|
|
119
|
+
self._summary_lock = threading.Lock()
|
|
120
|
+
|
|
121
|
+
self._prefetch_cache: Dict[str, str] = {}
|
|
122
|
+
self._prefetch_lock = threading.Lock()
|
|
123
|
+
|
|
124
|
+
self._sequence = 0
|
|
125
|
+
self._sequence_lock = threading.Lock()
|
|
126
|
+
|
|
127
|
+
self._write_queue: "queue.Queue[Optional[Dict[str, Any]]]" = queue.Queue(maxsize=200)
|
|
128
|
+
self._writer: Optional[threading.Thread] = None
|
|
129
|
+
self._writer_busy = False
|
|
130
|
+
|
|
131
|
+
# -- config ---------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def name(self) -> str:
|
|
135
|
+
return "robrain"
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _config_path(hermes_home: str) -> Path:
|
|
139
|
+
return Path(hermes_home) / _CONFIG_DIRNAME / _CONFIG_FILENAME
|
|
140
|
+
|
|
141
|
+
def _load_config(self, hermes_home: str) -> Dict[str, Any]:
|
|
142
|
+
config: Dict[str, Any] = {}
|
|
143
|
+
path = self._config_path(hermes_home) if hermes_home else None
|
|
144
|
+
if path and path.is_file():
|
|
145
|
+
try:
|
|
146
|
+
config = json.loads(path.read_text(encoding="utf-8")) or {}
|
|
147
|
+
except Exception:
|
|
148
|
+
logger.warning("robrain: unreadable config at %s — using env/defaults", path)
|
|
149
|
+
config.setdefault("base_url", os.environ.get("ROBRAIN_BASE_URL", ""))
|
|
150
|
+
config.setdefault("project_id", os.environ.get("ROBRAIN_PROJECT_ID", ""))
|
|
151
|
+
config.setdefault("scope", "team")
|
|
152
|
+
config["api_key"] = os.environ.get("PERCEPTION_API_KEY", "")
|
|
153
|
+
return config
|
|
154
|
+
|
|
155
|
+
def is_available(self) -> bool:
|
|
156
|
+
"""Configured = the config file exists or the API key env var is set.
|
|
157
|
+
|
|
158
|
+
No network calls here (contract) — reachability is probed in
|
|
159
|
+
initialize(), which fails open.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
from hermes_constants import get_hermes_home
|
|
163
|
+
if self._config_path(str(get_hermes_home())).is_file():
|
|
164
|
+
return True
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
return bool(os.environ.get("PERCEPTION_API_KEY"))
|
|
168
|
+
|
|
169
|
+
# -- lifecycle -------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def initialize(self, session_id: str, **kwargs) -> None:
|
|
172
|
+
self._session_id = session_id
|
|
173
|
+
self._hermes_home = str(kwargs.get("hermes_home") or "")
|
|
174
|
+
# Cron/subagent/flush contexts must not write into the decision corpus.
|
|
175
|
+
self._writes_enabled = kwargs.get("agent_context", "primary") == "primary"
|
|
176
|
+
|
|
177
|
+
self._config = self._load_config(self._hermes_home)
|
|
178
|
+
self._client = RoBrainClient(
|
|
179
|
+
base_url=self._config.get("base_url") or "",
|
|
180
|
+
api_key=self._config.get("api_key") or "",
|
|
181
|
+
project_id=self._config.get("project_id") or "",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self._writer = threading.Thread(
|
|
185
|
+
target=self._writer_loop, name="robrain-writer", daemon=True,
|
|
186
|
+
)
|
|
187
|
+
self._writer.start()
|
|
188
|
+
|
|
189
|
+
threading.Thread(
|
|
190
|
+
target=self._fetch_summary, name="robrain-summary", daemon=True,
|
|
191
|
+
).start()
|
|
192
|
+
|
|
193
|
+
def _fetch_summary(self) -> None:
|
|
194
|
+
try:
|
|
195
|
+
payload = self._client.get_summary()
|
|
196
|
+
except RoBrainClientError as exc:
|
|
197
|
+
self._degraded = True
|
|
198
|
+
logger.warning("robrain: Perception unreachable at startup (fail-open): %s", exc)
|
|
199
|
+
return
|
|
200
|
+
summary = str(payload.get("always_on_summary") or "").strip()
|
|
201
|
+
with self._summary_lock:
|
|
202
|
+
self._summary = summary
|
|
203
|
+
self._degraded = False
|
|
204
|
+
|
|
205
|
+
def shutdown(self) -> None:
|
|
206
|
+
"""Flush pending turn captures, then stop the writer.
|
|
207
|
+
|
|
208
|
+
A single POST /signals waits on Perception's server-side LLM
|
|
209
|
+
extraction (typically 5–20s), and short-lived sessions (``hermes
|
|
210
|
+
-z``) reach shutdown with the final turn still in flight — a
|
|
211
|
+
too-short drain here silently loses exactly the turn that made
|
|
212
|
+
the session worth remembering. Bounded by _SHUTDOWN_DRAIN_S so a
|
|
213
|
+
wedged server still can't block exit; the writer is a daemon
|
|
214
|
+
thread, so anything past the deadline dies with the interpreter.
|
|
215
|
+
"""
|
|
216
|
+
deadline = time.monotonic() + _SHUTDOWN_DRAIN_S
|
|
217
|
+
while not self._write_queue.empty() or self._writer_busy:
|
|
218
|
+
if time.monotonic() >= deadline:
|
|
219
|
+
logger.warning(
|
|
220
|
+
"robrain: shutdown drain timed out with %d turn(s) unflushed",
|
|
221
|
+
self._write_queue.qsize(),
|
|
222
|
+
)
|
|
223
|
+
break
|
|
224
|
+
time.sleep(0.2)
|
|
225
|
+
try:
|
|
226
|
+
self._write_queue.put_nowait(None)
|
|
227
|
+
except queue.Full:
|
|
228
|
+
pass
|
|
229
|
+
if self._writer is not None:
|
|
230
|
+
self._writer.join(timeout=max(0.0, deadline - time.monotonic()) + 1.0)
|
|
231
|
+
|
|
232
|
+
# -- recall ----------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def system_prompt_block(self) -> str:
|
|
235
|
+
with self._summary_lock:
|
|
236
|
+
summary = self._summary
|
|
237
|
+
lines = [
|
|
238
|
+
"RoBrain decision memory is active. Prior team decisions — including "
|
|
239
|
+
"explicitly REJECTED alternatives — are injected as context. Never "
|
|
240
|
+
"re-propose a REJECTED option without flagging the recorded reason "
|
|
241
|
+
"and asking the user to confirm the reversal. Use robrain_search "
|
|
242
|
+
"before committing to a tool, library, or architectural approach.",
|
|
243
|
+
]
|
|
244
|
+
if summary:
|
|
245
|
+
lines.append("")
|
|
246
|
+
lines.append("Project decisions (always-on summary):")
|
|
247
|
+
lines.append(summary)
|
|
248
|
+
return "\n".join(lines)
|
|
249
|
+
|
|
250
|
+
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
|
|
251
|
+
query = (query or "").strip()
|
|
252
|
+
if len(query) < 12 or self._client is None:
|
|
253
|
+
return
|
|
254
|
+
threading.Thread(
|
|
255
|
+
target=self._background_search,
|
|
256
|
+
args=(query, session_id or self._session_id),
|
|
257
|
+
name="robrain-prefetch",
|
|
258
|
+
daemon=True,
|
|
259
|
+
).start()
|
|
260
|
+
|
|
261
|
+
def _background_search(self, query: str, session_id: str) -> None:
|
|
262
|
+
try:
|
|
263
|
+
rows = self._client.search_decisions(query, limit=5)
|
|
264
|
+
except RoBrainClientError as exc:
|
|
265
|
+
logger.debug("robrain: prefetch search failed (fail-open): %s", exc)
|
|
266
|
+
return
|
|
267
|
+
formatted = _format_decisions(rows)
|
|
268
|
+
with self._prefetch_lock:
|
|
269
|
+
self._prefetch_cache[session_id] = (
|
|
270
|
+
f"Relevant prior decisions (RoBrain):\n{formatted}" if formatted else ""
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def prefetch(self, query: str, *, session_id: str = "") -> str:
|
|
274
|
+
with self._prefetch_lock:
|
|
275
|
+
return self._prefetch_cache.get(session_id or self._session_id, "")
|
|
276
|
+
|
|
277
|
+
# -- capture -----------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
def sync_turn(
|
|
280
|
+
self,
|
|
281
|
+
user_content: str,
|
|
282
|
+
assistant_content: str,
|
|
283
|
+
*,
|
|
284
|
+
session_id: str = "",
|
|
285
|
+
messages: Optional[List[Dict[str, Any]]] = None,
|
|
286
|
+
) -> None:
|
|
287
|
+
if not self._writes_enabled or self._client is None:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Strip previously injected memory context so recall never re-enters
|
|
291
|
+
# the corpus as a fresh "decision" (feedback loop).
|
|
292
|
+
user_clean = sanitize_context(user_content or "").strip()[:_MAX_TURN_CHARS]
|
|
293
|
+
assistant_clean = sanitize_context(assistant_content or "").strip()[:_MAX_TURN_CHARS]
|
|
294
|
+
if len(user_clean) + len(assistant_clean) < _MIN_TURN_CHARS:
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
with self._sequence_lock:
|
|
298
|
+
self._sequence += 1
|
|
299
|
+
sequence = self._sequence
|
|
300
|
+
|
|
301
|
+
item = {
|
|
302
|
+
"session_id": session_id or self._session_id,
|
|
303
|
+
"sequence": sequence,
|
|
304
|
+
"user_message": user_clean,
|
|
305
|
+
"assistant_message": assistant_clean,
|
|
306
|
+
"timestamp": _utc_now_iso(),
|
|
307
|
+
"scope": str(self._config.get("scope") or "team"),
|
|
308
|
+
}
|
|
309
|
+
try:
|
|
310
|
+
self._write_queue.put_nowait(item)
|
|
311
|
+
except queue.Full:
|
|
312
|
+
logger.warning("robrain: write queue full — dropping turn %s", sequence)
|
|
313
|
+
|
|
314
|
+
def _writer_loop(self) -> None:
|
|
315
|
+
while True:
|
|
316
|
+
item = self._write_queue.get()
|
|
317
|
+
if item is None:
|
|
318
|
+
return
|
|
319
|
+
self._writer_busy = True
|
|
320
|
+
try:
|
|
321
|
+
self._client.post_turn_signal(**item)
|
|
322
|
+
except RoBrainClientError as exc:
|
|
323
|
+
logger.debug("robrain: turn capture failed (fail-open): %s", exc)
|
|
324
|
+
finally:
|
|
325
|
+
self._writer_busy = False
|
|
326
|
+
|
|
327
|
+
# -- session lifecycle -------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
def on_session_switch(
|
|
330
|
+
self,
|
|
331
|
+
new_session_id: str,
|
|
332
|
+
*,
|
|
333
|
+
parent_session_id: str = "",
|
|
334
|
+
reset: bool = False,
|
|
335
|
+
rewound: bool = False,
|
|
336
|
+
**kwargs,
|
|
337
|
+
) -> None:
|
|
338
|
+
self._session_id = new_session_id
|
|
339
|
+
if reset:
|
|
340
|
+
with self._sequence_lock:
|
|
341
|
+
self._sequence = 0
|
|
342
|
+
with self._prefetch_lock:
|
|
343
|
+
self._prefetch_cache.clear()
|
|
344
|
+
|
|
345
|
+
# -- tools ---------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
348
|
+
return [SEARCH_SCHEMA]
|
|
349
|
+
|
|
350
|
+
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
|
|
351
|
+
if tool_name != "robrain_search":
|
|
352
|
+
return tool_error(f"Unknown tool: {tool_name}")
|
|
353
|
+
if self._client is None:
|
|
354
|
+
return tool_error("RoBrain provider is not initialized")
|
|
355
|
+
|
|
356
|
+
query = str(args.get("query") or "").strip()
|
|
357
|
+
if not query:
|
|
358
|
+
return tool_error("query is required")
|
|
359
|
+
limit = args.get("limit") or 5
|
|
360
|
+
try:
|
|
361
|
+
limit = max(1, min(int(limit), 20))
|
|
362
|
+
except (TypeError, ValueError):
|
|
363
|
+
limit = 5
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
rows = self._client.search_decisions(query, limit=limit)
|
|
367
|
+
except RoBrainClientError as exc:
|
|
368
|
+
return tool_error(f"RoBrain search failed: {exc}")
|
|
369
|
+
|
|
370
|
+
decisions = [
|
|
371
|
+
{
|
|
372
|
+
"decision": row.get("decision"),
|
|
373
|
+
"rationale": row.get("rationale"),
|
|
374
|
+
"rejected": row.get("rejected") or [],
|
|
375
|
+
"score": row.get("planning_score"),
|
|
376
|
+
"created_at": row.get("created_at"),
|
|
377
|
+
"reviewed": bool(row.get("reviewed_at")),
|
|
378
|
+
}
|
|
379
|
+
for row in rows
|
|
380
|
+
]
|
|
381
|
+
return json.dumps({"decisions": decisions, "count": len(decisions)})
|
|
382
|
+
|
|
383
|
+
# -- setup ------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def get_config_schema(self) -> List[Dict[str, Any]]:
|
|
386
|
+
return [
|
|
387
|
+
{
|
|
388
|
+
"key": "base_url",
|
|
389
|
+
"description": "Perception API URL of your RoBrain deployment",
|
|
390
|
+
"default": "http://localhost:3001",
|
|
391
|
+
"required": True,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"key": "project_id",
|
|
395
|
+
"description": "RoBrain project id (see `npx robrain review` or the projects table)",
|
|
396
|
+
"default": "default",
|
|
397
|
+
"required": True,
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
"key": "scope",
|
|
401
|
+
"description": "Scope recorded on captured decisions",
|
|
402
|
+
"default": "team",
|
|
403
|
+
"choices": ["user", "local", "team", "global"],
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"key": "api_key",
|
|
407
|
+
"description": "PERCEPTION_API_KEY from your RoBrain .env",
|
|
408
|
+
"secret": True,
|
|
409
|
+
"required": True,
|
|
410
|
+
"env_var": "PERCEPTION_API_KEY",
|
|
411
|
+
"url": "https://github.com/adelinamart/robrain#security",
|
|
412
|
+
},
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
def save_config(self, values: Dict[str, Any], hermes_home: str) -> None:
|
|
416
|
+
path = self._config_path(hermes_home)
|
|
417
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
418
|
+
# Defensive: an interactive-setup mispaste can land the API key in a
|
|
419
|
+
# text field. A base_url that isn't a URL would fail silently later
|
|
420
|
+
# (fail-open hides it), so fall back to the default instead — and
|
|
421
|
+
# never persist something that looks like a pasted secret.
|
|
422
|
+
base_url = str(values.get("base_url") or "").strip()
|
|
423
|
+
if not base_url.startswith(("http://", "https://")):
|
|
424
|
+
if base_url:
|
|
425
|
+
logger.warning(
|
|
426
|
+
"robrain: ignoring non-URL base_url from setup (%r…) — using default",
|
|
427
|
+
base_url[:8],
|
|
428
|
+
)
|
|
429
|
+
base_url = "http://localhost:3001"
|
|
430
|
+
config = {
|
|
431
|
+
"base_url": base_url,
|
|
432
|
+
"project_id": values.get("project_id") or "default",
|
|
433
|
+
"scope": values.get("scope") or "team",
|
|
434
|
+
}
|
|
435
|
+
path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def register_memory_provider() -> MemoryProvider:
|
|
439
|
+
"""Entry point used by plugins/memory discovery."""
|
|
440
|
+
return RoBrainProvider()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""HTTP client for RoBrain's self-hosted Perception API.
|
|
2
|
+
|
|
3
|
+
RoBrain (https://github.com/adelinamart/robrain) is a self-hosted decision
|
|
4
|
+
memory: it records what a team decided AND the alternatives it explicitly
|
|
5
|
+
rejected (a structured ``rejected[]`` field per decision), stores both in
|
|
6
|
+
the user's own Postgres, and serves them back ranked by a 5-signal
|
|
7
|
+
composite score. Nothing leaves the user's machine.
|
|
8
|
+
|
|
9
|
+
This client covers the three Perception endpoints the Hermes provider
|
|
10
|
+
needs:
|
|
11
|
+
|
|
12
|
+
GET /projects/:id/summary — the always-on summary (session-start recall)
|
|
13
|
+
GET /decisions?query=... — composite-scored semantic retrieval
|
|
14
|
+
POST /signals — raw-turn capture; Perception extracts the
|
|
15
|
+
decision server-side (needs_classification)
|
|
16
|
+
|
|
17
|
+
Auth is a static bearer token (``PERCEPTION_API_KEY`` from the RoBrain
|
|
18
|
+
deployment's .env). All calls carry short timeouts and raise
|
|
19
|
+
``RoBrainClientError`` — callers are expected to fail open: memory being
|
|
20
|
+
down should degrade recall, never block the agent.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
import requests
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
DEFAULT_BASE_URL = "http://localhost:3001"
|
|
33
|
+
DEFAULT_PROJECT_ID = "default"
|
|
34
|
+
|
|
35
|
+
_READ_TIMEOUT = 6 # summary / search — user-visible latency path
|
|
36
|
+
_WRITE_TIMEOUT = 45 # /signals runs LLM extraction + embedding server-side
|
|
37
|
+
# before responding; writes happen on a background
|
|
38
|
+
# thread, so a generous timeout costs no turn latency
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RoBrainClientError(Exception):
|
|
42
|
+
"""Raised for any transport or non-2xx failure against Perception."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RoBrainClient:
|
|
46
|
+
"""Thin, dependency-light wrapper over the Perception HTTP API."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
51
|
+
api_key: str = "",
|
|
52
|
+
project_id: str = DEFAULT_PROJECT_ID,
|
|
53
|
+
) -> None:
|
|
54
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
55
|
+
self.api_key = api_key
|
|
56
|
+
self.project_id = project_id or DEFAULT_PROJECT_ID
|
|
57
|
+
self._session = requests.Session()
|
|
58
|
+
|
|
59
|
+
# -- internals ----------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def _headers(self, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
|
62
|
+
headers = {"Content-Type": "application/json"}
|
|
63
|
+
if self.api_key:
|
|
64
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
65
|
+
if extra:
|
|
66
|
+
headers.update(extra)
|
|
67
|
+
return headers
|
|
68
|
+
|
|
69
|
+
def _request(self, method: str, path: str, *, timeout: float, **kwargs) -> Any:
|
|
70
|
+
url = f"{self.base_url}{path}"
|
|
71
|
+
try:
|
|
72
|
+
resp = self._session.request(
|
|
73
|
+
method, url, headers=self._headers(kwargs.pop("extra_headers", None)),
|
|
74
|
+
timeout=timeout, **kwargs,
|
|
75
|
+
)
|
|
76
|
+
except requests.RequestException as exc:
|
|
77
|
+
raise RoBrainClientError(f"{method} {path}: {exc}") from exc
|
|
78
|
+
if resp.status_code >= 400:
|
|
79
|
+
raise RoBrainClientError(
|
|
80
|
+
f"{method} {path}: HTTP {resp.status_code} — {resp.text[:200]}"
|
|
81
|
+
)
|
|
82
|
+
try:
|
|
83
|
+
return resp.json()
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
raise RoBrainClientError(f"{method} {path}: non-JSON response") from exc
|
|
86
|
+
|
|
87
|
+
# -- API surface ---------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def health(self) -> bool:
|
|
90
|
+
"""True when Perception answers /health (unauthenticated by design)."""
|
|
91
|
+
try:
|
|
92
|
+
self._request("GET", "/health", timeout=3)
|
|
93
|
+
return True
|
|
94
|
+
except RoBrainClientError:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
98
|
+
"""Fetch the project's always-on summary.
|
|
99
|
+
|
|
100
|
+
Returns ``{"always_on_summary": str | None, "mission": str | None}``.
|
|
101
|
+
404 (project not registered) raises like any other error — the
|
|
102
|
+
provider treats it as "no summary yet", not a hard failure.
|
|
103
|
+
"""
|
|
104
|
+
return self._request(
|
|
105
|
+
"GET", f"/projects/{self.project_id}/summary", timeout=_READ_TIMEOUT,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def search_decisions(
|
|
109
|
+
self,
|
|
110
|
+
query: str,
|
|
111
|
+
*,
|
|
112
|
+
limit: int = 5,
|
|
113
|
+
boost_files: Optional[List[str]] = None,
|
|
114
|
+
) -> List[Dict[str, Any]]:
|
|
115
|
+
"""Composite-scored semantic retrieval over the decision corpus.
|
|
116
|
+
|
|
117
|
+
Each row carries ``decision``, ``rationale``, ``rejected`` (list of
|
|
118
|
+
``{option, reason}``), ``planning_score``, lifecycle fields, etc.
|
|
119
|
+
"""
|
|
120
|
+
params: Dict[str, str] = {
|
|
121
|
+
"project_id": self.project_id,
|
|
122
|
+
"query": query,
|
|
123
|
+
"limit": str(limit),
|
|
124
|
+
}
|
|
125
|
+
if boost_files:
|
|
126
|
+
params["boost_files"] = ",".join(boost_files)
|
|
127
|
+
payload = self._request("GET", "/decisions", timeout=_READ_TIMEOUT, params=params)
|
|
128
|
+
rows = payload.get("decisions") if isinstance(payload, dict) else payload
|
|
129
|
+
return rows if isinstance(rows, list) else []
|
|
130
|
+
|
|
131
|
+
def post_turn_signal(
|
|
132
|
+
self,
|
|
133
|
+
*,
|
|
134
|
+
session_id: str,
|
|
135
|
+
sequence: int,
|
|
136
|
+
user_message: str,
|
|
137
|
+
assistant_message: str,
|
|
138
|
+
timestamp: str,
|
|
139
|
+
scope: str = "team",
|
|
140
|
+
) -> Dict[str, Any]:
|
|
141
|
+
"""Ship a raw turn; Perception extracts the decision server-side.
|
|
142
|
+
|
|
143
|
+
``needs_classification: true`` bypasses the confidence gate and makes
|
|
144
|
+
Perception run its own extraction (the flush-on-close path), so this
|
|
145
|
+
client needs no LLM of its own. Perception also re-redacts secrets
|
|
146
|
+
before anything is embedded or stored.
|
|
147
|
+
"""
|
|
148
|
+
body = {
|
|
149
|
+
"signal": {
|
|
150
|
+
"turn": {
|
|
151
|
+
"session_id": session_id,
|
|
152
|
+
"sequence": sequence,
|
|
153
|
+
"user_message": user_message,
|
|
154
|
+
"claude_reply": assistant_message,
|
|
155
|
+
"files_touched": [],
|
|
156
|
+
"timestamp": timestamp,
|
|
157
|
+
},
|
|
158
|
+
"decision_type": "unclassified",
|
|
159
|
+
"confidence": 0,
|
|
160
|
+
"files_affected": [],
|
|
161
|
+
"scope": scope,
|
|
162
|
+
"needs_classification": True,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return self._request(
|
|
166
|
+
"POST", "/signals", timeout=_WRITE_TIMEOUT, json=body,
|
|
167
|
+
extra_headers={"X-Project-Id": self.project_id},
|
|
168
|
+
)
|
|
@@ -23037,7 +23037,7 @@ function resolveSessionId(raw) {
|
|
|
23037
23037
|
function buildServer() {
|
|
23038
23038
|
const server = new McpServer({
|
|
23039
23039
|
name: "sensing-mcp",
|
|
23040
|
-
version: "2.3.
|
|
23040
|
+
version: "2.3.5"
|
|
23041
23041
|
});
|
|
23042
23042
|
server.tool(
|
|
23043
23043
|
"sensing_start_session",
|