total-recall-brain 3.36.0 → 3.37.0
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/frontend/dist/assets/index-CpvEw3iA.js +5786 -0
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli/agent.mjs +59 -18
- package/src/cli/plugin/create.mjs +112 -4
- package/src/cli/secret.mjs +28 -1
- package/src/core/agent-manager.mjs +47 -6
- package/src/core/cache-prune.mjs +60 -2
- package/src/core/meta-harness.mjs +93 -6
- package/src/core/plugin-loader.mjs +6 -5
- package/frontend/dist/assets/index-DZ4ypNEl.js +0 -5786
|
@@ -29,8 +29,14 @@ export const HARNESS_SPECS = {
|
|
|
29
29
|
name: 'Claude Code CLI',
|
|
30
30
|
binary: 'claude',
|
|
31
31
|
category: 'code_engineering',
|
|
32
|
-
defaultFlags: ['--output-format', 'json', '--permission-mode', 'bypassPermissions', '
|
|
32
|
+
defaultFlags: ['--output-format', 'json', '--permission-mode', 'bypassPermissions', '-p'],
|
|
33
33
|
execType: 'flag_last',
|
|
34
|
+
// Options the harness accepts for the built-in tool set and settings layers.
|
|
35
|
+
toolsFlag: '--tools',
|
|
36
|
+
settingSourcesFlag: '--setting-sources',
|
|
37
|
+
// Headless subscription auth (`claude setup-token`), read from the secret
|
|
38
|
+
// store when the caller's environment does not already carry it.
|
|
39
|
+
authSecrets: ['CLAUDE_CODE_OAUTH_TOKEN'],
|
|
34
40
|
description: 'Deep codebase editing, refactoring, and Unix command execution.'
|
|
35
41
|
},
|
|
36
42
|
codex: {
|
|
@@ -62,6 +68,79 @@ export const HARNESS_SPECS = {
|
|
|
62
68
|
}
|
|
63
69
|
};
|
|
64
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Normalise a caller's tool selection.
|
|
73
|
+
* undefined | null | 'default' | 'all' -> null (the harness's full built-in set)
|
|
74
|
+
* 'none' | '' -> '' (no tools)
|
|
75
|
+
* 'Bash,Read' | ['Bash', 'Read'] -> 'Bash,Read'
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeToolSelection(tools) {
|
|
78
|
+
if (tools === undefined || tools === null) return null;
|
|
79
|
+
const list = (Array.isArray(tools) ? tools : String(tools).split(','))
|
|
80
|
+
.map((t) => String(t).trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
if (list.length === 0 || (list.length === 1 && list[0] === 'none')) return '';
|
|
83
|
+
if (list.length === 1 && (list[0] === 'default' || list[0] === 'all')) return null;
|
|
84
|
+
return list.join(',');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Argument vector for a harness, before the task prompt.
|
|
89
|
+
* `tools` and `settingSources` are only accepted by harnesses that declare the
|
|
90
|
+
* matching flag; asking another harness for them is an error, not a silent no-op.
|
|
91
|
+
*/
|
|
92
|
+
export function buildHarnessArgs(spec, { tools, settingSources } = {}) {
|
|
93
|
+
const args = [...spec.defaultFlags];
|
|
94
|
+
const extra = [];
|
|
95
|
+
const selection = normalizeToolSelection(tools);
|
|
96
|
+
if (selection !== null) {
|
|
97
|
+
if (!spec.toolsFlag) throw new Error(`Harness "${spec.name}" does not support choosing its tool set.`);
|
|
98
|
+
extra.push(spec.toolsFlag, selection);
|
|
99
|
+
}
|
|
100
|
+
if (settingSources) {
|
|
101
|
+
if (!spec.settingSourcesFlag) throw new Error(`Harness "${spec.name}" does not support --setting-sources.`);
|
|
102
|
+
extra.push(spec.settingSourcesFlag, String(settingSources));
|
|
103
|
+
}
|
|
104
|
+
if (!extra.length) return args;
|
|
105
|
+
// Prompt-mode flags such as `-p` stay last so the task follows them.
|
|
106
|
+
const last = args[args.length - 1];
|
|
107
|
+
return spec.execType === 'flag_last' && last && last.startsWith('-')
|
|
108
|
+
? [...args.slice(0, -1), ...extra, last]
|
|
109
|
+
: [...args, ...extra];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** POSIX single-quote a value for a remote shell command line. */
|
|
113
|
+
export function shellQuote(value) {
|
|
114
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readStoreSecret(key) {
|
|
118
|
+
const { getSecret, defaultBrainDir } = await import('./secrets-store.mjs');
|
|
119
|
+
const res = await getSecret(defaultBrainDir(), key, { actor: 'agent-harness', action: 'harness-auth' });
|
|
120
|
+
return res.found ? res.value : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Environment for a harness process. Headless auth secrets the harness declares
|
|
125
|
+
* (for Claude Code, CLAUDE_CODE_OAUTH_TOKEN) are filled from the secret store
|
|
126
|
+
* when the base environment lacks them, so a session without a login keychain
|
|
127
|
+
* (SSH, launchd, cron) can still authenticate. A store that cannot be opened is
|
|
128
|
+
* not fatal: the harness falls back to its own login. Values are never logged.
|
|
129
|
+
*/
|
|
130
|
+
export async function resolveHarnessEnv(spec, baseEnv = process.env, { readSecret = readStoreSecret } = {}) {
|
|
131
|
+
const env = { ...baseEnv };
|
|
132
|
+
for (const key of spec.authSecrets || []) {
|
|
133
|
+
if (env[key]) continue;
|
|
134
|
+
try {
|
|
135
|
+
const value = await readSecret(key);
|
|
136
|
+
if (value) env[key] = value;
|
|
137
|
+
} catch {
|
|
138
|
+
// Store locked or missing; leave auth to the harness.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return env;
|
|
142
|
+
}
|
|
143
|
+
|
|
65
144
|
/**
|
|
66
145
|
* Detect all installed and operational harnesses on this computer.
|
|
67
146
|
*/
|
|
@@ -94,6 +173,12 @@ export async function dispatchTask(harnessId, taskPrompt, options = {}) {
|
|
|
94
173
|
}
|
|
95
174
|
|
|
96
175
|
const timeoutMs = options.timeoutMs || 180000; // 3 min default
|
|
176
|
+
// A dispatch is a single answer, not a workflow: tool-less and isolated from
|
|
177
|
+
// user/project settings unless the caller asks otherwise.
|
|
178
|
+
const harnessArgs = buildHarnessArgs(spec, {
|
|
179
|
+
tools: options.tools !== undefined ? options.tools : (spec.toolsFlag ? 'none' : undefined),
|
|
180
|
+
settingSources: options.settingSources !== undefined ? options.settingSources : (spec.settingSourcesFlag ? 'local' : undefined),
|
|
181
|
+
});
|
|
97
182
|
|
|
98
183
|
// Remote mesh execution branch
|
|
99
184
|
if (options.node) {
|
|
@@ -111,10 +196,11 @@ export async function dispatchTask(harnessId, taskPrompt, options = {}) {
|
|
|
111
196
|
message: `Dispatching to ${spec.name} remotely on mesh node "${options.node}" (${targetNode.ip})...`
|
|
112
197
|
});
|
|
113
198
|
|
|
114
|
-
const flags =
|
|
199
|
+
const flags = harnessArgs.map(shellQuote).join(' ');
|
|
200
|
+
const cd = options.cwd ? `cd ${shellQuote(options.cwd)} && ` : '';
|
|
115
201
|
const remoteCmd = spec.execType === 'pipe_stdin'
|
|
116
|
-
?
|
|
117
|
-
: `${spec.binary} ${flags} ${
|
|
202
|
+
? `${cd}printf '%s' ${shellQuote(taskPrompt)} | ${spec.binary} ${flags}`
|
|
203
|
+
: `${cd}${spec.binary} ${flags} ${shellQuote(taskPrompt)}`;
|
|
118
204
|
const execResult = await execMeshCommand(options.node, remoteCmd, {
|
|
119
205
|
vaultRoot: options.vaultRoot,
|
|
120
206
|
timeoutMs,
|
|
@@ -140,7 +226,8 @@ export async function dispatchTask(harnessId, taskPrompt, options = {}) {
|
|
|
140
226
|
}
|
|
141
227
|
|
|
142
228
|
const cwd = options.cwd || process.cwd();
|
|
143
|
-
const args = [...
|
|
229
|
+
const args = [...harnessArgs];
|
|
230
|
+
const env = await resolveHarnessEnv(spec, process.env, options);
|
|
144
231
|
|
|
145
232
|
if (spec.execType === 'subcommand' || spec.execType === 'flag_last') {
|
|
146
233
|
args.push(taskPrompt);
|
|
@@ -156,7 +243,7 @@ export async function dispatchTask(harnessId, taskPrompt, options = {}) {
|
|
|
156
243
|
cwd,
|
|
157
244
|
stdio: spec.execType === 'pipe_stdin' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
158
245
|
env: {
|
|
159
|
-
...
|
|
246
|
+
...env,
|
|
160
247
|
TR_HARNESS_DISPATCH: '1'
|
|
161
248
|
}
|
|
162
249
|
});
|
|
@@ -27,7 +27,7 @@ export function globalPluginsDir() {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function bundledPluginsDir() {
|
|
30
|
-
return path.join(PACKAGE_ROOT, 'plugins');
|
|
30
|
+
return process.env._TR_TEST_BUNDLED_PLUGINS_DIR || path.join(PACKAGE_ROOT, 'plugins');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** The vault that owns records for plugins installed in `pluginsDir` (`<brain>/plugins` → `<brain>/memory-vault`). */
|
|
@@ -351,10 +351,11 @@ export function discoverPlugins(projectRoot = process.cwd()) {
|
|
|
351
351
|
export function listBundledPlugins(projectRoot = process.cwd()) {
|
|
352
352
|
const seen = new Set();
|
|
353
353
|
const out = [];
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
// Under test the fixture directory is the only source, so results never
|
|
355
|
+
// depend on plugins installed in the developer's own brain.
|
|
356
|
+
const dirs = process.env._TR_TEST_BUNDLED_PLUGINS_DIR
|
|
357
|
+
? [bundledPluginsDir()]
|
|
358
|
+
: [bundledPluginsDir(), path.join(PACKAGE_ROOT, '.agent', 'skills', 'total-recall', 'plugins')];
|
|
358
359
|
for (const dir of dirs) {
|
|
359
360
|
if (!fs.existsSync(dir)) continue;
|
|
360
361
|
for (const p of readPluginsDir(dir, 'bundled')) {
|