crbro-memory 2.7.2 → 2.9.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/README.md +66 -7
- package/SECURITY.md +36 -0
- package/bin/crbro.mjs +1437 -1366
- package/dist/daemon/endpoint.d.ts.map +1 -1
- package/dist/daemon/endpoint.js +18 -0
- package/dist/daemon/endpoint.js.map +1 -1
- package/dist/engine/brain.d.ts.map +1 -1
- package/dist/engine/brain.js +30 -0
- package/dist/engine/brain.js.map +1 -1
- package/dist/engine/cortex.d.ts +55 -1
- package/dist/engine/cortex.d.ts.map +1 -1
- package/dist/engine/cortex.js +183 -3
- package/dist/engine/cortex.js.map +1 -1
- package/dist/engine/maintenance.d.ts +22 -0
- package/dist/engine/maintenance.d.ts.map +1 -1
- package/dist/engine/maintenance.js +59 -2
- package/dist/engine/maintenance.js.map +1 -1
- package/dist/engine/modinstall.d.ts +215 -0
- package/dist/engine/modinstall.d.ts.map +1 -0
- package/dist/engine/modinstall.js +1342 -0
- package/dist/engine/modinstall.js.map +1 -0
- package/dist/engine/shelf.d.ts +97 -0
- package/dist/engine/shelf.d.ts.map +1 -0
- package/dist/engine/shelf.js +343 -0
- package/dist/engine/shelf.js.map +1 -0
- package/dist/engine/source.d.ts +6 -0
- package/dist/engine/source.d.ts.map +1 -0
- package/dist/engine/source.js +101 -0
- package/dist/engine/source.js.map +1 -0
- package/dist/search/index.d.ts +6 -0
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +46 -1
- package/dist/search/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +226 -33
- package/dist/server.js.map +1 -1
- package/dist/sync/materialize.d.ts +4 -0
- package/dist/sync/materialize.d.ts.map +1 -1
- package/dist/sync/materialize.js +80 -1
- package/dist/sync/materialize.js.map +1 -1
- package/dist/sync/ops.d.ts +29 -2
- package/dist/sync/ops.d.ts.map +1 -1
- package/dist/sync/ops.js.map +1 -1
- package/dist/sync/space.d.ts.map +1 -1
- package/dist/sync/space.js +15 -2
- package/dist/sync/space.js.map +1 -1
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -1
- package/mods/crbro-pending/.claude-plugin/plugin.json +21 -0
- package/mods/crbro-pending/hooks/hooks.json +1 -0
- package/mods/crbro-pending/hooks/register.tsx +576 -0
- package/mods/crbro-pending/hooks/strings.ts +200 -0
- package/mods/crbro-pending/types/index.d.ts +27 -0
- package/package.json +2 -1
package/bin/crbro.mjs
CHANGED
|
@@ -1,1366 +1,1437 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// ─── CRBRO CLI ───────────────────────────────────────────────────
|
|
4
|
-
// Command-line interface for CRBRO memory system
|
|
5
|
-
// Supports: init, status, secret, mine, setup-miner, miner-status, usage, postmortem,
|
|
6
|
-
// remove-miner, and MCP server mode (default)
|
|
7
|
-
|
|
8
|
-
import { platform, homedir } from 'os';
|
|
9
|
-
import { join, dirname } from 'path';
|
|
10
|
-
import { existsSync, readFileSync } from 'fs';
|
|
11
|
-
import { fileURLToPath } from 'url';
|
|
12
|
-
|
|
13
|
-
// The release that is running, read from the package itself. The manifest
|
|
14
|
-
// version stamps the brain format and has not moved since 1.0.0, so showing
|
|
15
|
-
// only that one told everyone they were on 1.0.0 forever.
|
|
16
|
-
function pkgVersion() {
|
|
17
|
-
try {
|
|
18
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version;
|
|
20
|
-
} catch {
|
|
21
|
-
return 'unknown';
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// The Claude Code SessionStart command install-boot writes. Kept in one place
|
|
26
|
-
// because `install-hooks --compact` has to recognise it to replace it.
|
|
27
|
-
function bootCommandPosix(aviso) {
|
|
28
|
-
return `printf '%s\\n' ${JSON.stringify(aviso)}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** An install-boot entry, this release's or an older one with other wording. */
|
|
32
|
-
function isInstallBootHook(h) {
|
|
33
|
-
return !!h && typeof h.command === 'string' && h.command.startsWith("printf '%s\\n' ") && h.command.includes('crbro_boot');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const args = process.argv.slice(2);
|
|
37
|
-
const command = args[0];
|
|
38
|
-
|
|
39
|
-
// ─── IDE Detection ──────────────────────────────────────────────
|
|
40
|
-
const IDE_CONFIGS = [
|
|
41
|
-
{
|
|
42
|
-
name: 'Antigravity (Google Gemini)',
|
|
43
|
-
id: 'antigravity',
|
|
44
|
-
configPath: () => join(homedir(), '.gemini', 'antigravity', 'mcp_config.json'),
|
|
45
|
-
configFormat: 'mcpServers',
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'Cursor',
|
|
49
|
-
id: 'cursor',
|
|
50
|
-
configPath: () => join(homedir(), '.cursor', 'mcp.json'),
|
|
51
|
-
configFormat: 'mcpServers',
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: 'Windsurf',
|
|
55
|
-
id: 'windsurf',
|
|
56
|
-
configPath: () => join(homedir(), '.windsurf', 'mcp.json'),
|
|
57
|
-
configFormat: 'mcpServers',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
name: 'Claude Desktop',
|
|
61
|
-
id: 'claude-desktop',
|
|
62
|
-
configPath: () => {
|
|
63
|
-
if (platform() === 'win32') {
|
|
64
|
-
return join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
|
|
65
|
-
}
|
|
66
|
-
return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
67
|
-
},
|
|
68
|
-
configFormat: 'mcpServers',
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: 'Claude Code (user scope)',
|
|
72
|
-
id: 'claude-code',
|
|
73
|
-
configPath: () => join(homedir(), '.claude.json'),
|
|
74
|
-
configFormat: 'mcpServers',
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'VS Code + Continue',
|
|
78
|
-
id: 'continue',
|
|
79
|
-
configPath: () => join(homedir(), '.continue', 'config.json'),
|
|
80
|
-
configFormat: 'mcpServers',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: 'ChatGPT Desktop',
|
|
84
|
-
id: 'chatgpt',
|
|
85
|
-
configPath: () => {
|
|
86
|
-
if (platform() === 'win32') {
|
|
87
|
-
return join(process.env.APPDATA || '', 'ChatGPT', 'mcp_config.json');
|
|
88
|
-
}
|
|
89
|
-
return join(homedir(), 'Library', 'Application Support', 'ChatGPT', 'mcp_config.json');
|
|
90
|
-
},
|
|
91
|
-
configFormat: 'mcpServers',
|
|
92
|
-
},
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
function detectIDEs() {
|
|
96
|
-
const detected = [];
|
|
97
|
-
for (const ide of IDE_CONFIGS) {
|
|
98
|
-
try {
|
|
99
|
-
const configPath = ide.configPath();
|
|
100
|
-
if (existsSync(configPath)) {
|
|
101
|
-
detected.push({ ...ide, configPath: configPath, exists: true });
|
|
102
|
-
} else {
|
|
103
|
-
// Check if the parent directory exists (IDE installed but no config yet)
|
|
104
|
-
const parentDir = configPath.split(/[/\\]/).slice(0, -1).join(platform() === 'win32' ? '\\' : '/');
|
|
105
|
-
if (existsSync(parentDir)) {
|
|
106
|
-
detected.push({ ...ide, configPath: configPath, exists: false });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch { /* skip */ }
|
|
110
|
-
}
|
|
111
|
-
return detected;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function generateMCPSnippet(envVars) {
|
|
115
|
-
return JSON.stringify({
|
|
116
|
-
"crbro": {
|
|
117
|
-
"command": "npx",
|
|
118
|
-
"args": ["-y", "crbro-memory"],
|
|
119
|
-
...(envVars ? { "env": envVars } : {})
|
|
120
|
-
}
|
|
121
|
-
}, null, 2);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ─── Commands ───────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
// ─── Semantic recall: install, download, embed ──────────────────────
|
|
127
|
-
// Installed by `init` since 1.16 (skip with --no-semantic, turn off with
|
|
128
|
-
// CRBRO_SEMANTIC=0). The runtime (~380 MB) and the model (~118 MB) live in
|
|
129
|
-
// ~/.crbro/.semantic once per machine, outside the package.
|
|
130
|
-
async function semanticInstall(sem) {
|
|
131
|
-
const { spawnSync } = await import('child_process');
|
|
132
|
-
const fs = await import('fs');
|
|
133
|
-
const os = await import('os');
|
|
134
|
-
const home = sem.semanticHome();
|
|
135
|
-
fs.mkdirSync(home, { recursive: true });
|
|
136
|
-
const pkg = join(home, 'package.json');
|
|
137
|
-
if (!fs.existsSync(pkg)) {
|
|
138
|
-
fs.writeFileSync(pkg, JSON.stringify({ name: 'crbro-semantic', private: true }, null, 2));
|
|
139
|
-
}
|
|
140
|
-
if (!sem.resolveRuntime()) {
|
|
141
|
-
console.log(` ⬇️ Installing transformers.js into ${home} (~380 MB with onnxruntime)...`);
|
|
142
|
-
const r = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error', '@huggingface/transformers@3'],
|
|
143
|
-
{ cwd: home, stdio: 'inherit', shell: true });
|
|
144
|
-
if (r.status !== 0) return false;
|
|
145
|
-
}
|
|
146
|
-
const st = sem.semanticStatus();
|
|
147
|
-
if (!st.model_downloaded) {
|
|
148
|
-
console.log(` ⬇️ Downloading the model (${st.model}, ~118 MB)...`);
|
|
149
|
-
const idx = new sem.SemanticIndex(fs.mkdtempSync(join(os.tmpdir(), 'crbro-warm-')));
|
|
150
|
-
await idx.upsert([{ id: 'warm', text: 'hello' }]); // the first use downloads and caches it
|
|
151
|
-
}
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function semanticBuild() {
|
|
156
|
-
process.env.CRBRO_SEMANTIC = '1';
|
|
157
|
-
const [{ Brain }, { SearchEngine }] = await Promise.all([
|
|
158
|
-
import('../dist/engine/brain.js'),
|
|
159
|
-
import('../dist/search/index.js'),
|
|
160
|
-
]);
|
|
161
|
-
const brain = new Brain();
|
|
162
|
-
const engine = new SearchEngine(brain);
|
|
163
|
-
const started = Date.now();
|
|
164
|
-
await engine.init(); // loads the stored vectors: unchanged lines are not embedded twice
|
|
165
|
-
const chunks = await engine.rebuild();
|
|
166
|
-
await engine.awaitEmbeddings();
|
|
167
|
-
await engine.persist();
|
|
168
|
-
return { chunks, vectors: engine.semanticCount(), seconds: ((Date.now() - started) / 1000).toFixed(1) };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (command === 'init') {
|
|
172
|
-
// ─── Initialize brain + IDE detection ──────────────────────────
|
|
173
|
-
import('../dist/engine/brain.js').then(async ({ Brain }) => {
|
|
174
|
-
const brain = new Brain();
|
|
175
|
-
const manifest = await brain.initialize();
|
|
176
|
-
|
|
177
|
-
console.log('');
|
|
178
|
-
console.log(' 🧠 CRBRO brain initialized!');
|
|
179
|
-
console.log(` Path: ${manifest.brain_path}`);
|
|
180
|
-
console.log('');
|
|
181
|
-
|
|
182
|
-
// Detect IDEs
|
|
183
|
-
const ides = detectIDEs();
|
|
184
|
-
|
|
185
|
-
if (ides.length > 0) {
|
|
186
|
-
console.log(' 📡 Detected IDEs:');
|
|
187
|
-
console.log('');
|
|
188
|
-
for (const ide of ides) {
|
|
189
|
-
const status = ide.exists ? '✅ config exists' : '📝 needs config';
|
|
190
|
-
console.log(` ${ide.name}: ${status}`);
|
|
191
|
-
console.log(` → ${ide.configPath}`);
|
|
192
|
-
}
|
|
193
|
-
console.log('');
|
|
194
|
-
console.log(' Add this to your MCP config (inside "mcpServers"):');
|
|
195
|
-
} else {
|
|
196
|
-
console.log(' ⚠️ No IDE detected. Add this to your MCP config manually:');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
console.log('');
|
|
200
|
-
console.log(' ' + generateMCPSnippet().split('\n').join('\n '));
|
|
201
|
-
// Semantic recall, installed by default since 1.16: once per machine,
|
|
202
|
-
// about 500 MB on disk, ~0.5 GB of RAM while a server runs.
|
|
203
|
-
const skipSemantic = args.includes('--no-semantic')
|
|
204
|
-
|| ['0', 'off', 'false'].includes(String(process.env.CRBRO_SEMANTIC || '').toLowerCase());
|
|
205
|
-
console.log('');
|
|
206
|
-
if (skipSemantic) {
|
|
207
|
-
console.log(' npx crbro-memory semantic status Semantic recall (installed by init; --no-semantic skips it): status | install | build');
|
|
208
|
-
} else {
|
|
209
|
-
console.log(' 🧭 Semantic recall: installing (once per machine, ~500 MB)...');
|
|
210
|
-
try {
|
|
211
|
-
const sem = await import('../dist/search/semantic.js');
|
|
212
|
-
const ok = await semanticInstall(sem);
|
|
213
|
-
if (ok) {
|
|
214
|
-
const r = await semanticBuild();
|
|
215
|
-
console.log(` ✅ Semantic recall ready · ${r.vectors} lines embedded in ${r.seconds}s · CRBRO_SEMANTIC=0 turns it off`);
|
|
216
|
-
} else {
|
|
217
|
-
console.log(' ⚠️ Could not install it; recall stays keyword-only. Retry: npx crbro-memory semantic install');
|
|
218
|
-
}
|
|
219
|
-
} catch (err) {
|
|
220
|
-
console.log(` ⚠️ Semantic recall not installed (${err instanceof Error ? err.message : err}); recall stays keyword-only.`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
console.log('');
|
|
224
|
-
console.log(' Next steps:');
|
|
225
|
-
console.log(' 1. Add the config above to your IDE\'s MCP settings');
|
|
226
|
-
console.log(' 2. (Optional) npx crbro-memory setup-miner');
|
|
227
|
-
console.log(' 3. Restart your IDE — CRBRO boots automatically!');
|
|
228
|
-
console.log('');
|
|
229
|
-
}).catch(console.error);
|
|
230
|
-
|
|
231
|
-
} else if (command === 'status') {
|
|
232
|
-
// ─── Show brain status ─────────────────────────────────────────
|
|
233
|
-
import('../dist/engine/brain.js').then(async ({ Brain }) => {
|
|
234
|
-
const brain = new Brain();
|
|
235
|
-
try {
|
|
236
|
-
const manifest = await brain.getManifest();
|
|
237
|
-
console.log('');
|
|
238
|
-
console.log(' 🧠 CRBRO Brain Status');
|
|
239
|
-
console.log(' ─────────────────────');
|
|
240
|
-
console.log(` CRBRO: ${pkgVersion()}`);
|
|
241
|
-
console.log(` Brain format: ${manifest.version}`);
|
|
242
|
-
console.log(` Path: ${manifest.brain_path}`);
|
|
243
|
-
console.log(` Neurons: ${manifest.total_neurons}`);
|
|
244
|
-
console.log(` Synapses: ${manifest.total_synapses}`);
|
|
245
|
-
console.log(` Sessions: ${manifest.total_sessions}`);
|
|
246
|
-
console.log(` Last Boot: ${manifest.last_boot || 'never'}`);
|
|
247
|
-
console.log(` Last Consolidate: ${manifest.last_consolidation || 'never'}`);
|
|
248
|
-
console.log('');
|
|
249
|
-
|
|
250
|
-
// Show detected IDEs
|
|
251
|
-
const ides = detectIDEs();
|
|
252
|
-
if (ides.length > 0) {
|
|
253
|
-
console.log(' 📡 Connected IDEs:');
|
|
254
|
-
for (const ide of ides) {
|
|
255
|
-
console.log(` ${ide.exists ? '✅' : '⚠️ '} ${ide.name}`);
|
|
256
|
-
}
|
|
257
|
-
console.log('');
|
|
258
|
-
}
|
|
259
|
-
} catch {
|
|
260
|
-
console.log('');
|
|
261
|
-
console.log(' 🧠 CRBRO brain not initialized.');
|
|
262
|
-
console.log(' Run: npx crbro-memory init');
|
|
263
|
-
console.log('');
|
|
264
|
-
}
|
|
265
|
-
}).catch(console.error);
|
|
266
|
-
|
|
267
|
-
} else if (command === 'activate') {
|
|
268
|
-
// ─── Legacy command (pre-1.4.0) — CRBRO is now fully free ──────
|
|
269
|
-
console.log('');
|
|
270
|
-
console.log(' ✅ Good news: since v1.4.0 CRBRO is fully free.');
|
|
271
|
-
console.log(' All 15 tools are available — no license key needed.');
|
|
272
|
-
console.log('');
|
|
273
|
-
|
|
274
|
-
} else if (command === 'mine') {
|
|
275
|
-
// ─── One-shot mining ───────────────────────────────────────────
|
|
276
|
-
const targetDir = args[1];
|
|
277
|
-
|
|
278
|
-
import('../dist/miner/index.js').then(async ({ Miner }) => {
|
|
279
|
-
console.log('');
|
|
280
|
-
console.log(' ⛏️ CRBRO Miner — Scanning for knowledge...');
|
|
281
|
-
console.log('');
|
|
282
|
-
|
|
283
|
-
const miner = new Miner();
|
|
284
|
-
const result = await miner.mine(targetDir);
|
|
285
|
-
|
|
286
|
-
console.log(' ────────────────────────────────');
|
|
287
|
-
console.log(` Files scanned: ${result.scanned}`);
|
|
288
|
-
console.log(` New files mined: ${result.new_files}`);
|
|
289
|
-
console.log(` Neurons created: ${result.neurons_created}`);
|
|
290
|
-
console.log(` Neurons updated: ${result.neurons_updated}`);
|
|
291
|
-
console.log(` Facts added: ${result.facts_added}`);
|
|
292
|
-
console.log(` Decisions found: ${result.decisions_added}`);
|
|
293
|
-
|
|
294
|
-
if (result.technologies_found.length > 0) {
|
|
295
|
-
console.log(` Technologies: ${result.technologies_found.slice(0, 10).join(', ')}`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (result.errors.length > 0) {
|
|
299
|
-
console.log('');
|
|
300
|
-
console.log(' ⚠️ Errors:');
|
|
301
|
-
for (const err of result.errors.slice(0, 5)) {
|
|
302
|
-
console.log(` ${err}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
console.log('');
|
|
307
|
-
}).catch(console.error);
|
|
308
|
-
|
|
309
|
-
} else if (command === 'setup-miner') {
|
|
310
|
-
// ─── Setup automatic mining ────────────────────────────────────
|
|
311
|
-
import('../dist/miner/scheduler.js').then(async ({ setupScheduler }) => {
|
|
312
|
-
console.log('');
|
|
313
|
-
console.log(' ⏰ Setting up CRBRO Auto-Miner...');
|
|
314
|
-
console.log('');
|
|
315
|
-
|
|
316
|
-
const result = await setupScheduler();
|
|
317
|
-
console.log(result.message);
|
|
318
|
-
console.log('');
|
|
319
|
-
}).catch(console.error);
|
|
320
|
-
|
|
321
|
-
} else if (command === 'miner-status') {
|
|
322
|
-
// ─── Check miner status ────────────────────────────────────────
|
|
323
|
-
Promise.all([
|
|
324
|
-
import('../dist/miner/scheduler.js'),
|
|
325
|
-
import('../dist/miner/index.js'),
|
|
326
|
-
]).then(async ([{ getSchedulerStatus }, { Miner }]) => {
|
|
327
|
-
console.log('');
|
|
328
|
-
console.log(' ⛏️ CRBRO Miner Status');
|
|
329
|
-
console.log(' ─────────────────────');
|
|
330
|
-
|
|
331
|
-
// Scheduler status
|
|
332
|
-
const schedStatus = await getSchedulerStatus();
|
|
333
|
-
console.log(` Scheduler: ${schedStatus.installed ? '✅ Installed' : '❌ Not installed'}`);
|
|
334
|
-
console.log(` Platform: ${schedStatus.platform}`);
|
|
335
|
-
if (schedStatus.details) {
|
|
336
|
-
console.log(` Details: ${schedStatus.details}`);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Miner state
|
|
340
|
-
const miner = new Miner();
|
|
341
|
-
const status = await miner.getStatus();
|
|
342
|
-
console.log('');
|
|
343
|
-
console.log(` Last run: ${status.state.last_run || 'never'}`);
|
|
344
|
-
console.log(` Total mined: ${status.state.total_mined} files`);
|
|
345
|
-
console.log(` Tracked: ${Object.keys(status.state.mined_files).length} files`);
|
|
346
|
-
console.log('');
|
|
347
|
-
|
|
348
|
-
if (status.detected_dirs.length > 0) {
|
|
349
|
-
console.log(' 📂 Scan directories:');
|
|
350
|
-
for (const dir of status.detected_dirs) {
|
|
351
|
-
console.log(` ${dir}`);
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
console.log(' ⚠️ No IDE directories detected.');
|
|
355
|
-
}
|
|
356
|
-
console.log('');
|
|
357
|
-
}).catch(console.error);
|
|
358
|
-
|
|
359
|
-
} else if (command === 'remove-miner') {
|
|
360
|
-
// ─── Remove automatic mining ───────────────────────────────────
|
|
361
|
-
import('../dist/miner/scheduler.js').then(async ({ removeScheduler }) => {
|
|
362
|
-
const result = await removeScheduler();
|
|
363
|
-
console.log('');
|
|
364
|
-
console.log(result.success ? ` ✅ ${result.message}` : ` ❌ ${result.message}`);
|
|
365
|
-
console.log('');
|
|
366
|
-
}).catch(console.error);
|
|
367
|
-
|
|
368
|
-
} else if (command === 'reindex') {
|
|
369
|
-
// ─── Rebuild the search index from the cortex ──────────────────
|
|
370
|
-
Promise.all([
|
|
371
|
-
import('../dist/engine/brain.js'),
|
|
372
|
-
import('../dist/search/index.js'),
|
|
373
|
-
]).then(async ([{ Brain }, { SearchEngine }]) => {
|
|
374
|
-
const brain = new Brain();
|
|
375
|
-
const engine = new SearchEngine(brain);
|
|
376
|
-
|
|
377
|
-
console.log('');
|
|
378
|
-
console.log(' 🔁 Rebuilding the CRBRO search index...');
|
|
379
|
-
const started = Date.now();
|
|
380
|
-
const indexed = await engine.rebuild();
|
|
381
|
-
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
382
|
-
|
|
383
|
-
console.log('');
|
|
384
|
-
console.log(` ✅ ${indexed} chunks indexed in ${seconds}s`);
|
|
385
|
-
console.log(' Every fact, decision and pattern is now searchable on its own,');
|
|
386
|
-
console.log(' so a big neuron is no longer buried by short ones.');
|
|
387
|
-
console.log('');
|
|
388
|
-
}).catch(console.error);
|
|
389
|
-
|
|
390
|
-
} else if (command === 'semantic') {
|
|
391
|
-
// ─── Semantic recall: status | install | build ─────────────────
|
|
392
|
-
const sub = args[1];
|
|
393
|
-
import('../dist/search/semantic.js').then(async (sem) => {
|
|
394
|
-
if (sub === 'install') {
|
|
395
|
-
console.log('');
|
|
396
|
-
const ok = await semanticInstall(sem);
|
|
397
|
-
if (!ok) {
|
|
398
|
-
console.log(' ❌ npm install failed. Nothing else changed.');
|
|
399
|
-
process.exit(1);
|
|
400
|
-
}
|
|
401
|
-
const r = await semanticBuild();
|
|
402
|
-
console.log(` ✅ Semantic recall ready · ${r.chunks} chunks indexed · ${r.vectors} vectors · ${r.seconds}s`);
|
|
403
|
-
console.log(' It is on whenever this runtime is present; CRBRO_SEMANTIC=0 turns it off.');
|
|
404
|
-
console.log('');
|
|
405
|
-
} else if (sub === 'build') {
|
|
406
|
-
const st = sem.semanticStatus();
|
|
407
|
-
if (!st.installed) {
|
|
408
|
-
console.log('');
|
|
409
|
-
console.log(' ❌ Runtime not installed. Run: npx crbro-memory semantic install');
|
|
410
|
-
console.log('');
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
console.log('');
|
|
414
|
-
console.log(' 🧭 Embedding the brain...');
|
|
415
|
-
const r = await semanticBuild();
|
|
416
|
-
console.log(` ✅ ${r.chunks} chunks indexed · ${r.vectors} vectors stored · ${r.seconds}s`);
|
|
417
|
-
console.log(' From now on each new line is embedded when it is saved.');
|
|
418
|
-
console.log('');
|
|
419
|
-
} else {
|
|
420
|
-
const st = sem.semanticStatus();
|
|
421
|
-
const enabled = st.enabled
|
|
422
|
-
? (st.mode === 'forced' ? '✅ on (CRBRO_SEMANTIC=1)' : '✅ on (installed; CRBRO_SEMANTIC=0 turns it off)')
|
|
423
|
-
: (st.mode === 'disabled' ? '⚪ off (CRBRO_SEMANTIC=0)' : '⚪ off (not installed)');
|
|
424
|
-
console.log('');
|
|
425
|
-
console.log(' 🧭 CRBRO semantic recall');
|
|
426
|
-
console.log(' ────────────────────────');
|
|
427
|
-
console.log(` Runtime: ${st.installed ? '✅ installed' : '❌ not installed → npx crbro-memory init (or: semantic install)'}`);
|
|
428
|
-
console.log(` Model: ${st.model}${st.model_downloaded ? '' : ' (downloads on first use)'}`);
|
|
429
|
-
console.log(` Enabled: ${enabled}`);
|
|
430
|
-
console.log(` Home: ${st.home}`);
|
|
431
|
-
console.log('');
|
|
432
|
-
}
|
|
433
|
-
}).catch(console.error);
|
|
434
|
-
|
|
435
|
-
} else if (command === 'eval') {
|
|
436
|
-
// ─── Measure retrieval quality against a query set ─────────────
|
|
437
|
-
//
|
|
438
|
-
// Without a number you cannot tell a fix from a feeling. The file is
|
|
439
|
-
// .crbro/.eval/queries.json — a list of { query, expect_neuron } and
|
|
440
|
-
// optionally expect_contains, the substring the matched fact should carry.
|
|
441
|
-
Promise.all([
|
|
442
|
-
import('../dist/engine/brain.js'),
|
|
443
|
-
import('../dist/search/index.js'),
|
|
444
|
-
import('fs/promises'),
|
|
445
|
-
]).then(async ([{ Brain }, { SearchEngine }, fsp]) => {
|
|
446
|
-
const brain = new Brain();
|
|
447
|
-
const evalPath = join(brain.paths.root, '.eval', 'queries.json');
|
|
448
|
-
|
|
449
|
-
let queries;
|
|
450
|
-
try {
|
|
451
|
-
queries = JSON.parse(await fsp.readFile(evalPath, 'utf-8'));
|
|
452
|
-
} catch {
|
|
453
|
-
console.log('');
|
|
454
|
-
console.log(` No query set found at ${evalPath}`);
|
|
455
|
-
console.log(' Create it as a JSON array, for example:');
|
|
456
|
-
console.log('');
|
|
457
|
-
console.log(' [');
|
|
458
|
-
console.log(' { "query": "how we deploy the api", "expect_neuron": "project_octochat",');
|
|
459
|
-
console.log(' "expect_contains": "Cloud Run" }');
|
|
460
|
-
console.log(' ]');
|
|
461
|
-
console.log('');
|
|
462
|
-
console.log(' Build it from facts you already saved: take six or eight words');
|
|
463
|
-
console.log(' out of a real fact and name the neuron that holds it.');
|
|
464
|
-
console.log('');
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const engine = new SearchEngine(brain);
|
|
469
|
-
await engine.init();
|
|
470
|
-
|
|
471
|
-
let atOne = 0, atThree = 0, reciprocal = 0, contentOk = 0;
|
|
472
|
-
const misses = [];
|
|
473
|
-
|
|
474
|
-
for (const q of queries) {
|
|
475
|
-
const results = await engine.search(q.query, { limit: 10 });
|
|
476
|
-
const rank = results.findIndex(r => r.neuron_id === q.expect_neuron);
|
|
477
|
-
|
|
478
|
-
if (rank === 0) atOne++;
|
|
479
|
-
if (rank >= 0 && rank < 3) atThree++;
|
|
480
|
-
if (rank >= 0) reciprocal += 1 / (rank + 1);
|
|
481
|
-
|
|
482
|
-
if (rank === 0 && q.expect_contains) {
|
|
483
|
-
if (results[0].matching_content.includes(q.expect_contains)) contentOk++;
|
|
484
|
-
else misses.push(` ~ "${q.query}" — right neuron, wrong fact returned`);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (rank !== 0) {
|
|
488
|
-
const got = results[0] ? results[0].neuron_id : '(nothing)';
|
|
489
|
-
misses.push(` ✗ "${q.query}" — expected ${q.expect_neuron}, got ${got}` +
|
|
490
|
-
(rank > 0 ? ` (it was #${rank + 1})` : ''));
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const n = queries.length;
|
|
495
|
-
const pct = (x) => `${((x / n) * 100).toFixed(1)}%`;
|
|
496
|
-
|
|
497
|
-
console.log('');
|
|
498
|
-
console.log(' 📊 CRBRO retrieval eval');
|
|
499
|
-
console.log(' ───────────────────────');
|
|
500
|
-
console.log(` Queries: ${n}`);
|
|
501
|
-
console.log(` Right first hit: ${atOne}/${n} (${pct(atOne)})`);
|
|
502
|
-
console.log(` In the top 3: ${atThree}/${n} (${pct(atThree)})`);
|
|
503
|
-
console.log(` MRR: ${(reciprocal / n).toFixed(3)}`);
|
|
504
|
-
if (queries.some(q => q.expect_contains)) {
|
|
505
|
-
const withContent = queries.filter(q => q.expect_contains).length;
|
|
506
|
-
console.log(` Right fact shown: ${contentOk}/${withContent}`);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (misses.length > 0) {
|
|
510
|
-
console.log('');
|
|
511
|
-
console.log(' Misses:');
|
|
512
|
-
for (const m of misses.slice(0, 25)) console.log(m);
|
|
513
|
-
if (misses.length > 25) console.log(` ... and ${misses.length - 25} more`);
|
|
514
|
-
}
|
|
515
|
-
console.log('');
|
|
516
|
-
}).catch(console.error);
|
|
517
|
-
|
|
518
|
-
} else if (command === 'daemon') {
|
|
519
|
-
// ─── One process owns the brain for every client (opt-in) ──────
|
|
520
|
-
//
|
|
521
|
-
// crbro daemon status is it on, who is running, how much it holds
|
|
522
|
-
// crbro daemon on every client of this brain becomes a thin proxy
|
|
523
|
-
// crbro daemon off back to one full server per client, and stop what runs
|
|
524
|
-
// crbro daemon stop stop the running daemon(s); clients start a new one on demand
|
|
525
|
-
//
|
|
526
|
-
// The switch is a flag inside the brain, so every client flips together the
|
|
527
|
-
// next time it starts. Nothing is lost by turning it off, or by the daemon
|
|
528
|
-
// dying: a client that cannot reach one serves itself, as before 2.5.
|
|
529
|
-
(async () => {
|
|
530
|
-
const { resolveBrainDir } = await import('../dist/engine/brain.js');
|
|
531
|
-
const ep = await import('../dist/daemon/endpoint.js');
|
|
532
|
-
const { controlDaemon } = await import('../dist/daemon/proxy.js');
|
|
533
|
-
const fs = await import('fs');
|
|
534
|
-
const root = resolveBrainDir();
|
|
535
|
-
const sub = args[1] || 'status';
|
|
536
|
-
|
|
537
|
-
const running = async () => {
|
|
538
|
-
const out = [];
|
|
539
|
-
for (const st of await ep.listStates(root)) {
|
|
540
|
-
const reply = await controlDaemon(root, 'status', st.build);
|
|
541
|
-
if (reply) { out.push({ st, reply }); continue; }
|
|
542
|
-
// No answer is not proof of death: a daemon in the middle of a rebuild
|
|
543
|
-
// answers late, and deleting its state file would orphan it — alive,
|
|
544
|
-
// holding the brain, and unreachable by every client. Only a pid the
|
|
545
|
-
// system says is gone loses its file.
|
|
546
|
-
let alive = true;
|
|
547
|
-
try { process.kill(st.pid, 0); } catch (e) { alive = e.code === 'EPERM'; }
|
|
548
|
-
if (alive) out.push({ st, reply: null });
|
|
549
|
-
else { try { fs.rmSync(ep.stateFile(root, st.build), { force: true }); } catch { /* stale file, best effort */ } }
|
|
550
|
-
}
|
|
551
|
-
return out;
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
if (sub === 'on' || sub === 'off') {
|
|
555
|
-
await ep.setDaemonEnabled(root, sub === 'on');
|
|
556
|
-
console.log('');
|
|
557
|
-
if (sub === 'on') {
|
|
558
|
-
console.log(' ✅ Daemon mode ON for ' + root);
|
|
559
|
-
console.log(' Restart your MCP clients (Claude Code, Claude Desktop, Codex...). The first one to start');
|
|
560
|
-
console.log(' launches the daemon; the rest attach to it. One index, one model, one writer.');
|
|
561
|
-
console.log(' Clients older than 2.5 keep running their own server — update them to get the benefit.');
|
|
562
|
-
} else {
|
|
563
|
-
for (const { st, reply } of await running()) if (reply) await controlDaemon(root, 'stop', st.build);
|
|
564
|
-
console.log(' ✅ Daemon mode OFF. Clients go back to one full server each the next time they start.');
|
|
565
|
-
}
|
|
566
|
-
console.log('');
|
|
567
|
-
} else if (sub === 'stop') {
|
|
568
|
-
const list = await running();
|
|
569
|
-
for (const { st, reply } of list) if (reply) await controlDaemon(root, 'stop', st.build);
|
|
570
|
-
console.log(list.length ? `\n ✅ Asked ${list.length} daemon(s) to stop. Connected clients carry on: they start a new one on their next call.\n`
|
|
571
|
-
: '\n No daemon is running for this brain.\n');
|
|
572
|
-
} else {
|
|
573
|
-
const list = await running();
|
|
574
|
-
console.log('');
|
|
575
|
-
console.log(' 🧠 CRBRO daemon');
|
|
576
|
-
console.log(' ───────────────');
|
|
577
|
-
console.log(` Brain: ${root}`);
|
|
578
|
-
console.log(` Mode: ${ep.daemonEnabled(root) ? '✅ on' : 'off (npx crbro-memory daemon on)'}`);
|
|
579
|
-
if (list.length === 0) console.log(' Running: none' + (ep.daemonEnabled(root) ? ' — the next client to start launches it' : ''));
|
|
580
|
-
for (const { st, reply } of list) {
|
|
581
|
-
if (!reply) { console.log(` Running: pid ${st.pid} · ${st.version} (${st.build}) · alive but not answering right now (busy, or another user's)`); continue; }
|
|
582
|
-
console.log(` Running: pid ${reply.pid} · ${reply.version} (${reply.build}) · ${reply.connections} conversation(s) · ${reply.rss_mb} MB · up ${Math.round(reply.uptime_s / 60)} min · idle exit ${reply.idle_minutes} min`);
|
|
583
|
-
}
|
|
584
|
-
if (list.length > 1) console.log(' ⚠️ More than one build is serving this brain. Point every client at the same install to share one.');
|
|
585
|
-
console.log(` Log: ${root}${root.includes('\\') ? '\\' : '/'}.daemon${root.includes('\\') ? '\\' : '/'}daemon.log`);
|
|
586
|
-
console.log('');
|
|
587
|
-
}
|
|
588
|
-
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
589
|
-
|
|
590
|
-
} else if (command === 'guard') {
|
|
591
|
-
// ─── What the PreToolUse guard would say for a command ─────────
|
|
592
|
-
//
|
|
593
|
-
// crbro guard "firebase deploy --only hosting"
|
|
594
|
-
// crbro guard --rebuild write the trigger index now
|
|
595
|
-
//
|
|
596
|
-
// Reads <brain>/.search/triggers.json, the lookup crbro_consolidate
|
|
597
|
-
// derives from the error, debt and pattern ledgers. Read-only unless
|
|
598
|
-
// --rebuild is passed.
|
|
599
|
-
(async () => {
|
|
600
|
-
const { Brain } = await import('../dist/engine/brain.js');
|
|
601
|
-
const { writeTriggerIndex, triggerIndexPath, lessonsFor } = await import('../dist/engine/triggers.js');
|
|
602
|
-
const fs = await import('fs');
|
|
603
|
-
const brain = new Brain();
|
|
604
|
-
if (process.argv.includes('--rebuild')) {
|
|
605
|
-
const r = await writeTriggerIndex(brain);
|
|
606
|
-
if ('error' in r) { console.error(` ❌ ${r.error}`); process.exit(1); }
|
|
607
|
-
console.log(` ✅ Trigger index written: ${r.entries} lessons under ${r.keys} commands.`);
|
|
608
|
-
console.log(` ${triggerIndexPath(brain)}`);
|
|
609
|
-
}
|
|
610
|
-
const texto = process.argv.slice(3).filter(a => !a.startsWith('--')).join(' ').trim();
|
|
611
|
-
if (!texto) {
|
|
612
|
-
if (!process.argv.includes('--rebuild')) console.log(' Usage: crbro guard "<command line>" | crbro guard --rebuild');
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
let index;
|
|
616
|
-
try {
|
|
617
|
-
index = JSON.parse(fs.readFileSync(triggerIndexPath(brain), 'utf8'));
|
|
618
|
-
} catch {
|
|
619
|
-
console.log(' No trigger index yet. Run: crbro guard --rebuild (crbro_consolidate writes it too).');
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
const lessons = lessonsFor(index, texto, 10);
|
|
623
|
-
if (lessons.length === 0) { console.log(' Nothing stored mentions that command.'); return; }
|
|
624
|
-
for (const l of lessons) console.log(` • [${l.k}${l.d ? ' · ' + l.d : ''} · ${l.n}] ${l.t}\n`);
|
|
625
|
-
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
626
|
-
|
|
627
|
-
} else if (command === 'usage' || command === 'postmortem') {
|
|
628
|
-
// ─── Token spend and post-mortem, from Claude Code's own logs ──
|
|
629
|
-
//
|
|
630
|
-
// crbro usage [--days N] [--session <id>] [--project <folder>] [--json]
|
|
631
|
-
// crbro postmortem [--days N] [--session <id>] [--project <folder>] [--max N] [--json]
|
|
632
|
-
//
|
|
633
|
-
// Both read ~/.claude/projects/<project>/<session>.jsonl (and, for usage,
|
|
634
|
-
// the subagent logs under <session>/subagents/) through one streaming
|
|
635
|
-
// reader. usage reads message.model and message.usage only; postmortem
|
|
636
|
-
// reads what the person typed and tool names, never a tool's input or
|
|
637
|
-
// result, and redacts everything it prints. Neither writes anything.
|
|
638
|
-
const flag = (name) => { const i = args.indexOf(name); return i !== -1 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : undefined; };
|
|
639
|
-
const intFlag = (name) => {
|
|
640
|
-
const v = flag(name);
|
|
641
|
-
if (v === undefined) return undefined;
|
|
642
|
-
const n = Number(v);
|
|
643
|
-
if (!Number.isInteger(n) || n < 0) { console.error(` ❌ ${name} expects a whole number ≥ 0; got: ${v}`); process.exit(1); }
|
|
644
|
-
return n;
|
|
645
|
-
};
|
|
646
|
-
const session = flag('--session');
|
|
647
|
-
const project = flag('--project');
|
|
648
|
-
// Last 7 days unless told otherwise (--days 0 = all); a named session is looked for everywhere.
|
|
649
|
-
const daysArg = intFlag('--days');
|
|
650
|
-
const days = daysArg !== undefined ? (daysArg === 0 ? undefined : daysArg) : (session ? undefined : 7);
|
|
651
|
-
const max = intFlag('--max');
|
|
652
|
-
const json = args.includes('--json');
|
|
653
|
-
const mod = command === 'usage' ? '../dist/engine/usage.js' : '../dist/engine/postmortem.js';
|
|
654
|
-
import(mod).then(async m => {
|
|
655
|
-
if (command === 'usage') {
|
|
656
|
-
const r = await m.collectUsage({ days, session, project });
|
|
657
|
-
console.log(json ? JSON.stringify(r, null, 2) : m.formatUsage(r));
|
|
658
|
-
} else {
|
|
659
|
-
const r = await m.runPostmortem({ days, session, project, max });
|
|
660
|
-
console.log(json ? JSON.stringify(r, null, 2) : m.formatPostmortem(r));
|
|
661
|
-
}
|
|
662
|
-
}).catch(err => {
|
|
663
|
-
console.error(' ❌ ' + (err && err.code === 'ERR_MODULE_NOT_FOUND' ? 'Build required. Run: npm run build' : err.message));
|
|
664
|
-
process.exit(1);
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
} else if (command === 'install-hooks' && process.argv.includes('--verify')) {
|
|
668
|
-
// ─── Are the installed hooks the ones this package ships? ──────
|
|
669
|
-
// SHA-256 of each copy in ~/.claude/crbro-hooks/ against hooks/ here, and
|
|
670
|
-
// whether every CRBRO hook settings.json runs exists. Read-only. Exit 1 on
|
|
671
|
-
// any difference, so it can gate a script.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
// crbro-
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
const
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
settings
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
console.log(
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
console.log('
|
|
968
|
-
console.log('
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
} else
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
} else
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
};
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
if (
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
console.
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
console.log(
|
|
1267
|
-
console.log(
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const
|
|
1273
|
-
if (
|
|
1274
|
-
console.error(
|
|
1275
|
-
process.exit(1);
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ─── CRBRO CLI ───────────────────────────────────────────────────
|
|
4
|
+
// Command-line interface for CRBRO memory system
|
|
5
|
+
// Supports: init, status, secret, mine, setup-miner, miner-status, usage, postmortem,
|
|
6
|
+
// remove-miner, install-hooks, install-mod, and MCP server mode (default)
|
|
7
|
+
|
|
8
|
+
import { platform, homedir } from 'os';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { existsSync, readFileSync } from 'fs';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
// The release that is running, read from the package itself. The manifest
|
|
14
|
+
// version stamps the brain format and has not moved since 1.0.0, so showing
|
|
15
|
+
// only that one told everyone they were on 1.0.0 forever.
|
|
16
|
+
function pkgVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version;
|
|
20
|
+
} catch {
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The Claude Code SessionStart command install-boot writes. Kept in one place
|
|
26
|
+
// because `install-hooks --compact` has to recognise it to replace it.
|
|
27
|
+
function bootCommandPosix(aviso) {
|
|
28
|
+
return `printf '%s\\n' ${JSON.stringify(aviso)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An install-boot entry, this release's or an older one with other wording. */
|
|
32
|
+
function isInstallBootHook(h) {
|
|
33
|
+
return !!h && typeof h.command === 'string' && h.command.startsWith("printf '%s\\n' ") && h.command.includes('crbro_boot');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
const command = args[0];
|
|
38
|
+
|
|
39
|
+
// ─── IDE Detection ──────────────────────────────────────────────
|
|
40
|
+
const IDE_CONFIGS = [
|
|
41
|
+
{
|
|
42
|
+
name: 'Antigravity (Google Gemini)',
|
|
43
|
+
id: 'antigravity',
|
|
44
|
+
configPath: () => join(homedir(), '.gemini', 'antigravity', 'mcp_config.json'),
|
|
45
|
+
configFormat: 'mcpServers',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Cursor',
|
|
49
|
+
id: 'cursor',
|
|
50
|
+
configPath: () => join(homedir(), '.cursor', 'mcp.json'),
|
|
51
|
+
configFormat: 'mcpServers',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Windsurf',
|
|
55
|
+
id: 'windsurf',
|
|
56
|
+
configPath: () => join(homedir(), '.windsurf', 'mcp.json'),
|
|
57
|
+
configFormat: 'mcpServers',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Claude Desktop',
|
|
61
|
+
id: 'claude-desktop',
|
|
62
|
+
configPath: () => {
|
|
63
|
+
if (platform() === 'win32') {
|
|
64
|
+
return join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
|
|
65
|
+
}
|
|
66
|
+
return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
67
|
+
},
|
|
68
|
+
configFormat: 'mcpServers',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'Claude Code (user scope)',
|
|
72
|
+
id: 'claude-code',
|
|
73
|
+
configPath: () => join(homedir(), '.claude.json'),
|
|
74
|
+
configFormat: 'mcpServers',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'VS Code + Continue',
|
|
78
|
+
id: 'continue',
|
|
79
|
+
configPath: () => join(homedir(), '.continue', 'config.json'),
|
|
80
|
+
configFormat: 'mcpServers',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'ChatGPT Desktop',
|
|
84
|
+
id: 'chatgpt',
|
|
85
|
+
configPath: () => {
|
|
86
|
+
if (platform() === 'win32') {
|
|
87
|
+
return join(process.env.APPDATA || '', 'ChatGPT', 'mcp_config.json');
|
|
88
|
+
}
|
|
89
|
+
return join(homedir(), 'Library', 'Application Support', 'ChatGPT', 'mcp_config.json');
|
|
90
|
+
},
|
|
91
|
+
configFormat: 'mcpServers',
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
function detectIDEs() {
|
|
96
|
+
const detected = [];
|
|
97
|
+
for (const ide of IDE_CONFIGS) {
|
|
98
|
+
try {
|
|
99
|
+
const configPath = ide.configPath();
|
|
100
|
+
if (existsSync(configPath)) {
|
|
101
|
+
detected.push({ ...ide, configPath: configPath, exists: true });
|
|
102
|
+
} else {
|
|
103
|
+
// Check if the parent directory exists (IDE installed but no config yet)
|
|
104
|
+
const parentDir = configPath.split(/[/\\]/).slice(0, -1).join(platform() === 'win32' ? '\\' : '/');
|
|
105
|
+
if (existsSync(parentDir)) {
|
|
106
|
+
detected.push({ ...ide, configPath: configPath, exists: false });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch { /* skip */ }
|
|
110
|
+
}
|
|
111
|
+
return detected;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function generateMCPSnippet(envVars) {
|
|
115
|
+
return JSON.stringify({
|
|
116
|
+
"crbro": {
|
|
117
|
+
"command": "npx",
|
|
118
|
+
"args": ["-y", "crbro-memory"],
|
|
119
|
+
...(envVars ? { "env": envVars } : {})
|
|
120
|
+
}
|
|
121
|
+
}, null, 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Commands ───────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
// ─── Semantic recall: install, download, embed ──────────────────────
|
|
127
|
+
// Installed by `init` since 1.16 (skip with --no-semantic, turn off with
|
|
128
|
+
// CRBRO_SEMANTIC=0). The runtime (~380 MB) and the model (~118 MB) live in
|
|
129
|
+
// ~/.crbro/.semantic once per machine, outside the package.
|
|
130
|
+
async function semanticInstall(sem) {
|
|
131
|
+
const { spawnSync } = await import('child_process');
|
|
132
|
+
const fs = await import('fs');
|
|
133
|
+
const os = await import('os');
|
|
134
|
+
const home = sem.semanticHome();
|
|
135
|
+
fs.mkdirSync(home, { recursive: true });
|
|
136
|
+
const pkg = join(home, 'package.json');
|
|
137
|
+
if (!fs.existsSync(pkg)) {
|
|
138
|
+
fs.writeFileSync(pkg, JSON.stringify({ name: 'crbro-semantic', private: true }, null, 2));
|
|
139
|
+
}
|
|
140
|
+
if (!sem.resolveRuntime()) {
|
|
141
|
+
console.log(` ⬇️ Installing transformers.js into ${home} (~380 MB with onnxruntime)...`);
|
|
142
|
+
const r = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error', '@huggingface/transformers@3'],
|
|
143
|
+
{ cwd: home, stdio: 'inherit', shell: true });
|
|
144
|
+
if (r.status !== 0) return false;
|
|
145
|
+
}
|
|
146
|
+
const st = sem.semanticStatus();
|
|
147
|
+
if (!st.model_downloaded) {
|
|
148
|
+
console.log(` ⬇️ Downloading the model (${st.model}, ~118 MB)...`);
|
|
149
|
+
const idx = new sem.SemanticIndex(fs.mkdtempSync(join(os.tmpdir(), 'crbro-warm-')));
|
|
150
|
+
await idx.upsert([{ id: 'warm', text: 'hello' }]); // the first use downloads and caches it
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function semanticBuild() {
|
|
156
|
+
process.env.CRBRO_SEMANTIC = '1';
|
|
157
|
+
const [{ Brain }, { SearchEngine }] = await Promise.all([
|
|
158
|
+
import('../dist/engine/brain.js'),
|
|
159
|
+
import('../dist/search/index.js'),
|
|
160
|
+
]);
|
|
161
|
+
const brain = new Brain();
|
|
162
|
+
const engine = new SearchEngine(brain);
|
|
163
|
+
const started = Date.now();
|
|
164
|
+
await engine.init(); // loads the stored vectors: unchanged lines are not embedded twice
|
|
165
|
+
const chunks = await engine.rebuild();
|
|
166
|
+
await engine.awaitEmbeddings();
|
|
167
|
+
await engine.persist();
|
|
168
|
+
return { chunks, vectors: engine.semanticCount(), seconds: ((Date.now() - started) / 1000).toFixed(1) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (command === 'init') {
|
|
172
|
+
// ─── Initialize brain + IDE detection ──────────────────────────
|
|
173
|
+
import('../dist/engine/brain.js').then(async ({ Brain }) => {
|
|
174
|
+
const brain = new Brain();
|
|
175
|
+
const manifest = await brain.initialize();
|
|
176
|
+
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log(' 🧠 CRBRO brain initialized!');
|
|
179
|
+
console.log(` Path: ${manifest.brain_path}`);
|
|
180
|
+
console.log('');
|
|
181
|
+
|
|
182
|
+
// Detect IDEs
|
|
183
|
+
const ides = detectIDEs();
|
|
184
|
+
|
|
185
|
+
if (ides.length > 0) {
|
|
186
|
+
console.log(' 📡 Detected IDEs:');
|
|
187
|
+
console.log('');
|
|
188
|
+
for (const ide of ides) {
|
|
189
|
+
const status = ide.exists ? '✅ config exists' : '📝 needs config';
|
|
190
|
+
console.log(` ${ide.name}: ${status}`);
|
|
191
|
+
console.log(` → ${ide.configPath}`);
|
|
192
|
+
}
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(' Add this to your MCP config (inside "mcpServers"):');
|
|
195
|
+
} else {
|
|
196
|
+
console.log(' ⚠️ No IDE detected. Add this to your MCP config manually:');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(' ' + generateMCPSnippet().split('\n').join('\n '));
|
|
201
|
+
// Semantic recall, installed by default since 1.16: once per machine,
|
|
202
|
+
// about 500 MB on disk, ~0.5 GB of RAM while a server runs.
|
|
203
|
+
const skipSemantic = args.includes('--no-semantic')
|
|
204
|
+
|| ['0', 'off', 'false'].includes(String(process.env.CRBRO_SEMANTIC || '').toLowerCase());
|
|
205
|
+
console.log('');
|
|
206
|
+
if (skipSemantic) {
|
|
207
|
+
console.log(' npx crbro-memory semantic status Semantic recall (installed by init; --no-semantic skips it): status | install | build');
|
|
208
|
+
} else {
|
|
209
|
+
console.log(' 🧭 Semantic recall: installing (once per machine, ~500 MB)...');
|
|
210
|
+
try {
|
|
211
|
+
const sem = await import('../dist/search/semantic.js');
|
|
212
|
+
const ok = await semanticInstall(sem);
|
|
213
|
+
if (ok) {
|
|
214
|
+
const r = await semanticBuild();
|
|
215
|
+
console.log(` ✅ Semantic recall ready · ${r.vectors} lines embedded in ${r.seconds}s · CRBRO_SEMANTIC=0 turns it off`);
|
|
216
|
+
} else {
|
|
217
|
+
console.log(' ⚠️ Could not install it; recall stays keyword-only. Retry: npx crbro-memory semantic install');
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.log(` ⚠️ Semantic recall not installed (${err instanceof Error ? err.message : err}); recall stays keyword-only.`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(' Next steps:');
|
|
225
|
+
console.log(' 1. Add the config above to your IDE\'s MCP settings');
|
|
226
|
+
console.log(' 2. (Optional) npx crbro-memory setup-miner');
|
|
227
|
+
console.log(' 3. Restart your IDE — CRBRO boots automatically!');
|
|
228
|
+
console.log('');
|
|
229
|
+
}).catch(console.error);
|
|
230
|
+
|
|
231
|
+
} else if (command === 'status') {
|
|
232
|
+
// ─── Show brain status ─────────────────────────────────────────
|
|
233
|
+
import('../dist/engine/brain.js').then(async ({ Brain }) => {
|
|
234
|
+
const brain = new Brain();
|
|
235
|
+
try {
|
|
236
|
+
const manifest = await brain.getManifest();
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log(' 🧠 CRBRO Brain Status');
|
|
239
|
+
console.log(' ─────────────────────');
|
|
240
|
+
console.log(` CRBRO: ${pkgVersion()}`);
|
|
241
|
+
console.log(` Brain format: ${manifest.version}`);
|
|
242
|
+
console.log(` Path: ${manifest.brain_path}`);
|
|
243
|
+
console.log(` Neurons: ${manifest.total_neurons}`);
|
|
244
|
+
console.log(` Synapses: ${manifest.total_synapses}`);
|
|
245
|
+
console.log(` Sessions: ${manifest.total_sessions}`);
|
|
246
|
+
console.log(` Last Boot: ${manifest.last_boot || 'never'}`);
|
|
247
|
+
console.log(` Last Consolidate: ${manifest.last_consolidation || 'never'}`);
|
|
248
|
+
console.log('');
|
|
249
|
+
|
|
250
|
+
// Show detected IDEs
|
|
251
|
+
const ides = detectIDEs();
|
|
252
|
+
if (ides.length > 0) {
|
|
253
|
+
console.log(' 📡 Connected IDEs:');
|
|
254
|
+
for (const ide of ides) {
|
|
255
|
+
console.log(` ${ide.exists ? '✅' : '⚠️ '} ${ide.name}`);
|
|
256
|
+
}
|
|
257
|
+
console.log('');
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log(' 🧠 CRBRO brain not initialized.');
|
|
262
|
+
console.log(' Run: npx crbro-memory init');
|
|
263
|
+
console.log('');
|
|
264
|
+
}
|
|
265
|
+
}).catch(console.error);
|
|
266
|
+
|
|
267
|
+
} else if (command === 'activate') {
|
|
268
|
+
// ─── Legacy command (pre-1.4.0) — CRBRO is now fully free ──────
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log(' ✅ Good news: since v1.4.0 CRBRO is fully free.');
|
|
271
|
+
console.log(' All 15 tools are available — no license key needed.');
|
|
272
|
+
console.log('');
|
|
273
|
+
|
|
274
|
+
} else if (command === 'mine') {
|
|
275
|
+
// ─── One-shot mining ───────────────────────────────────────────
|
|
276
|
+
const targetDir = args[1];
|
|
277
|
+
|
|
278
|
+
import('../dist/miner/index.js').then(async ({ Miner }) => {
|
|
279
|
+
console.log('');
|
|
280
|
+
console.log(' ⛏️ CRBRO Miner — Scanning for knowledge...');
|
|
281
|
+
console.log('');
|
|
282
|
+
|
|
283
|
+
const miner = new Miner();
|
|
284
|
+
const result = await miner.mine(targetDir);
|
|
285
|
+
|
|
286
|
+
console.log(' ────────────────────────────────');
|
|
287
|
+
console.log(` Files scanned: ${result.scanned}`);
|
|
288
|
+
console.log(` New files mined: ${result.new_files}`);
|
|
289
|
+
console.log(` Neurons created: ${result.neurons_created}`);
|
|
290
|
+
console.log(` Neurons updated: ${result.neurons_updated}`);
|
|
291
|
+
console.log(` Facts added: ${result.facts_added}`);
|
|
292
|
+
console.log(` Decisions found: ${result.decisions_added}`);
|
|
293
|
+
|
|
294
|
+
if (result.technologies_found.length > 0) {
|
|
295
|
+
console.log(` Technologies: ${result.technologies_found.slice(0, 10).join(', ')}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (result.errors.length > 0) {
|
|
299
|
+
console.log('');
|
|
300
|
+
console.log(' ⚠️ Errors:');
|
|
301
|
+
for (const err of result.errors.slice(0, 5)) {
|
|
302
|
+
console.log(` ${err}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log('');
|
|
307
|
+
}).catch(console.error);
|
|
308
|
+
|
|
309
|
+
} else if (command === 'setup-miner') {
|
|
310
|
+
// ─── Setup automatic mining ────────────────────────────────────
|
|
311
|
+
import('../dist/miner/scheduler.js').then(async ({ setupScheduler }) => {
|
|
312
|
+
console.log('');
|
|
313
|
+
console.log(' ⏰ Setting up CRBRO Auto-Miner...');
|
|
314
|
+
console.log('');
|
|
315
|
+
|
|
316
|
+
const result = await setupScheduler();
|
|
317
|
+
console.log(result.message);
|
|
318
|
+
console.log('');
|
|
319
|
+
}).catch(console.error);
|
|
320
|
+
|
|
321
|
+
} else if (command === 'miner-status') {
|
|
322
|
+
// ─── Check miner status ────────────────────────────────────────
|
|
323
|
+
Promise.all([
|
|
324
|
+
import('../dist/miner/scheduler.js'),
|
|
325
|
+
import('../dist/miner/index.js'),
|
|
326
|
+
]).then(async ([{ getSchedulerStatus }, { Miner }]) => {
|
|
327
|
+
console.log('');
|
|
328
|
+
console.log(' ⛏️ CRBRO Miner Status');
|
|
329
|
+
console.log(' ─────────────────────');
|
|
330
|
+
|
|
331
|
+
// Scheduler status
|
|
332
|
+
const schedStatus = await getSchedulerStatus();
|
|
333
|
+
console.log(` Scheduler: ${schedStatus.installed ? '✅ Installed' : '❌ Not installed'}`);
|
|
334
|
+
console.log(` Platform: ${schedStatus.platform}`);
|
|
335
|
+
if (schedStatus.details) {
|
|
336
|
+
console.log(` Details: ${schedStatus.details}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Miner state
|
|
340
|
+
const miner = new Miner();
|
|
341
|
+
const status = await miner.getStatus();
|
|
342
|
+
console.log('');
|
|
343
|
+
console.log(` Last run: ${status.state.last_run || 'never'}`);
|
|
344
|
+
console.log(` Total mined: ${status.state.total_mined} files`);
|
|
345
|
+
console.log(` Tracked: ${Object.keys(status.state.mined_files).length} files`);
|
|
346
|
+
console.log('');
|
|
347
|
+
|
|
348
|
+
if (status.detected_dirs.length > 0) {
|
|
349
|
+
console.log(' 📂 Scan directories:');
|
|
350
|
+
for (const dir of status.detected_dirs) {
|
|
351
|
+
console.log(` ${dir}`);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
console.log(' ⚠️ No IDE directories detected.');
|
|
355
|
+
}
|
|
356
|
+
console.log('');
|
|
357
|
+
}).catch(console.error);
|
|
358
|
+
|
|
359
|
+
} else if (command === 'remove-miner') {
|
|
360
|
+
// ─── Remove automatic mining ───────────────────────────────────
|
|
361
|
+
import('../dist/miner/scheduler.js').then(async ({ removeScheduler }) => {
|
|
362
|
+
const result = await removeScheduler();
|
|
363
|
+
console.log('');
|
|
364
|
+
console.log(result.success ? ` ✅ ${result.message}` : ` ❌ ${result.message}`);
|
|
365
|
+
console.log('');
|
|
366
|
+
}).catch(console.error);
|
|
367
|
+
|
|
368
|
+
} else if (command === 'reindex') {
|
|
369
|
+
// ─── Rebuild the search index from the cortex ──────────────────
|
|
370
|
+
Promise.all([
|
|
371
|
+
import('../dist/engine/brain.js'),
|
|
372
|
+
import('../dist/search/index.js'),
|
|
373
|
+
]).then(async ([{ Brain }, { SearchEngine }]) => {
|
|
374
|
+
const brain = new Brain();
|
|
375
|
+
const engine = new SearchEngine(brain);
|
|
376
|
+
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log(' 🔁 Rebuilding the CRBRO search index...');
|
|
379
|
+
const started = Date.now();
|
|
380
|
+
const indexed = await engine.rebuild();
|
|
381
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
382
|
+
|
|
383
|
+
console.log('');
|
|
384
|
+
console.log(` ✅ ${indexed} chunks indexed in ${seconds}s`);
|
|
385
|
+
console.log(' Every fact, decision and pattern is now searchable on its own,');
|
|
386
|
+
console.log(' so a big neuron is no longer buried by short ones.');
|
|
387
|
+
console.log('');
|
|
388
|
+
}).catch(console.error);
|
|
389
|
+
|
|
390
|
+
} else if (command === 'semantic') {
|
|
391
|
+
// ─── Semantic recall: status | install | build ─────────────────
|
|
392
|
+
const sub = args[1];
|
|
393
|
+
import('../dist/search/semantic.js').then(async (sem) => {
|
|
394
|
+
if (sub === 'install') {
|
|
395
|
+
console.log('');
|
|
396
|
+
const ok = await semanticInstall(sem);
|
|
397
|
+
if (!ok) {
|
|
398
|
+
console.log(' ❌ npm install failed. Nothing else changed.');
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
const r = await semanticBuild();
|
|
402
|
+
console.log(` ✅ Semantic recall ready · ${r.chunks} chunks indexed · ${r.vectors} vectors · ${r.seconds}s`);
|
|
403
|
+
console.log(' It is on whenever this runtime is present; CRBRO_SEMANTIC=0 turns it off.');
|
|
404
|
+
console.log('');
|
|
405
|
+
} else if (sub === 'build') {
|
|
406
|
+
const st = sem.semanticStatus();
|
|
407
|
+
if (!st.installed) {
|
|
408
|
+
console.log('');
|
|
409
|
+
console.log(' ❌ Runtime not installed. Run: npx crbro-memory semantic install');
|
|
410
|
+
console.log('');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log(' 🧭 Embedding the brain...');
|
|
415
|
+
const r = await semanticBuild();
|
|
416
|
+
console.log(` ✅ ${r.chunks} chunks indexed · ${r.vectors} vectors stored · ${r.seconds}s`);
|
|
417
|
+
console.log(' From now on each new line is embedded when it is saved.');
|
|
418
|
+
console.log('');
|
|
419
|
+
} else {
|
|
420
|
+
const st = sem.semanticStatus();
|
|
421
|
+
const enabled = st.enabled
|
|
422
|
+
? (st.mode === 'forced' ? '✅ on (CRBRO_SEMANTIC=1)' : '✅ on (installed; CRBRO_SEMANTIC=0 turns it off)')
|
|
423
|
+
: (st.mode === 'disabled' ? '⚪ off (CRBRO_SEMANTIC=0)' : '⚪ off (not installed)');
|
|
424
|
+
console.log('');
|
|
425
|
+
console.log(' 🧭 CRBRO semantic recall');
|
|
426
|
+
console.log(' ────────────────────────');
|
|
427
|
+
console.log(` Runtime: ${st.installed ? '✅ installed' : '❌ not installed → npx crbro-memory init (or: semantic install)'}`);
|
|
428
|
+
console.log(` Model: ${st.model}${st.model_downloaded ? '' : ' (downloads on first use)'}`);
|
|
429
|
+
console.log(` Enabled: ${enabled}`);
|
|
430
|
+
console.log(` Home: ${st.home}`);
|
|
431
|
+
console.log('');
|
|
432
|
+
}
|
|
433
|
+
}).catch(console.error);
|
|
434
|
+
|
|
435
|
+
} else if (command === 'eval') {
|
|
436
|
+
// ─── Measure retrieval quality against a query set ─────────────
|
|
437
|
+
//
|
|
438
|
+
// Without a number you cannot tell a fix from a feeling. The file is
|
|
439
|
+
// .crbro/.eval/queries.json — a list of { query, expect_neuron } and
|
|
440
|
+
// optionally expect_contains, the substring the matched fact should carry.
|
|
441
|
+
Promise.all([
|
|
442
|
+
import('../dist/engine/brain.js'),
|
|
443
|
+
import('../dist/search/index.js'),
|
|
444
|
+
import('fs/promises'),
|
|
445
|
+
]).then(async ([{ Brain }, { SearchEngine }, fsp]) => {
|
|
446
|
+
const brain = new Brain();
|
|
447
|
+
const evalPath = join(brain.paths.root, '.eval', 'queries.json');
|
|
448
|
+
|
|
449
|
+
let queries;
|
|
450
|
+
try {
|
|
451
|
+
queries = JSON.parse(await fsp.readFile(evalPath, 'utf-8'));
|
|
452
|
+
} catch {
|
|
453
|
+
console.log('');
|
|
454
|
+
console.log(` No query set found at ${evalPath}`);
|
|
455
|
+
console.log(' Create it as a JSON array, for example:');
|
|
456
|
+
console.log('');
|
|
457
|
+
console.log(' [');
|
|
458
|
+
console.log(' { "query": "how we deploy the api", "expect_neuron": "project_octochat",');
|
|
459
|
+
console.log(' "expect_contains": "Cloud Run" }');
|
|
460
|
+
console.log(' ]');
|
|
461
|
+
console.log('');
|
|
462
|
+
console.log(' Build it from facts you already saved: take six or eight words');
|
|
463
|
+
console.log(' out of a real fact and name the neuron that holds it.');
|
|
464
|
+
console.log('');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const engine = new SearchEngine(brain);
|
|
469
|
+
await engine.init();
|
|
470
|
+
|
|
471
|
+
let atOne = 0, atThree = 0, reciprocal = 0, contentOk = 0;
|
|
472
|
+
const misses = [];
|
|
473
|
+
|
|
474
|
+
for (const q of queries) {
|
|
475
|
+
const results = await engine.search(q.query, { limit: 10 });
|
|
476
|
+
const rank = results.findIndex(r => r.neuron_id === q.expect_neuron);
|
|
477
|
+
|
|
478
|
+
if (rank === 0) atOne++;
|
|
479
|
+
if (rank >= 0 && rank < 3) atThree++;
|
|
480
|
+
if (rank >= 0) reciprocal += 1 / (rank + 1);
|
|
481
|
+
|
|
482
|
+
if (rank === 0 && q.expect_contains) {
|
|
483
|
+
if (results[0].matching_content.includes(q.expect_contains)) contentOk++;
|
|
484
|
+
else misses.push(` ~ "${q.query}" — right neuron, wrong fact returned`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (rank !== 0) {
|
|
488
|
+
const got = results[0] ? results[0].neuron_id : '(nothing)';
|
|
489
|
+
misses.push(` ✗ "${q.query}" — expected ${q.expect_neuron}, got ${got}` +
|
|
490
|
+
(rank > 0 ? ` (it was #${rank + 1})` : ''));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const n = queries.length;
|
|
495
|
+
const pct = (x) => `${((x / n) * 100).toFixed(1)}%`;
|
|
496
|
+
|
|
497
|
+
console.log('');
|
|
498
|
+
console.log(' 📊 CRBRO retrieval eval');
|
|
499
|
+
console.log(' ───────────────────────');
|
|
500
|
+
console.log(` Queries: ${n}`);
|
|
501
|
+
console.log(` Right first hit: ${atOne}/${n} (${pct(atOne)})`);
|
|
502
|
+
console.log(` In the top 3: ${atThree}/${n} (${pct(atThree)})`);
|
|
503
|
+
console.log(` MRR: ${(reciprocal / n).toFixed(3)}`);
|
|
504
|
+
if (queries.some(q => q.expect_contains)) {
|
|
505
|
+
const withContent = queries.filter(q => q.expect_contains).length;
|
|
506
|
+
console.log(` Right fact shown: ${contentOk}/${withContent}`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (misses.length > 0) {
|
|
510
|
+
console.log('');
|
|
511
|
+
console.log(' Misses:');
|
|
512
|
+
for (const m of misses.slice(0, 25)) console.log(m);
|
|
513
|
+
if (misses.length > 25) console.log(` ... and ${misses.length - 25} more`);
|
|
514
|
+
}
|
|
515
|
+
console.log('');
|
|
516
|
+
}).catch(console.error);
|
|
517
|
+
|
|
518
|
+
} else if (command === 'daemon') {
|
|
519
|
+
// ─── One process owns the brain for every client (opt-in) ──────
|
|
520
|
+
//
|
|
521
|
+
// crbro daemon status is it on, who is running, how much it holds
|
|
522
|
+
// crbro daemon on every client of this brain becomes a thin proxy
|
|
523
|
+
// crbro daemon off back to one full server per client, and stop what runs
|
|
524
|
+
// crbro daemon stop stop the running daemon(s); clients start a new one on demand
|
|
525
|
+
//
|
|
526
|
+
// The switch is a flag inside the brain, so every client flips together the
|
|
527
|
+
// next time it starts. Nothing is lost by turning it off, or by the daemon
|
|
528
|
+
// dying: a client that cannot reach one serves itself, as before 2.5.
|
|
529
|
+
(async () => {
|
|
530
|
+
const { resolveBrainDir } = await import('../dist/engine/brain.js');
|
|
531
|
+
const ep = await import('../dist/daemon/endpoint.js');
|
|
532
|
+
const { controlDaemon } = await import('../dist/daemon/proxy.js');
|
|
533
|
+
const fs = await import('fs');
|
|
534
|
+
const root = resolveBrainDir();
|
|
535
|
+
const sub = args[1] || 'status';
|
|
536
|
+
|
|
537
|
+
const running = async () => {
|
|
538
|
+
const out = [];
|
|
539
|
+
for (const st of await ep.listStates(root)) {
|
|
540
|
+
const reply = await controlDaemon(root, 'status', st.build);
|
|
541
|
+
if (reply) { out.push({ st, reply }); continue; }
|
|
542
|
+
// No answer is not proof of death: a daemon in the middle of a rebuild
|
|
543
|
+
// answers late, and deleting its state file would orphan it — alive,
|
|
544
|
+
// holding the brain, and unreachable by every client. Only a pid the
|
|
545
|
+
// system says is gone loses its file.
|
|
546
|
+
let alive = true;
|
|
547
|
+
try { process.kill(st.pid, 0); } catch (e) { alive = e.code === 'EPERM'; }
|
|
548
|
+
if (alive) out.push({ st, reply: null });
|
|
549
|
+
else { try { fs.rmSync(ep.stateFile(root, st.build), { force: true }); } catch { /* stale file, best effort */ } }
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
if (sub === 'on' || sub === 'off') {
|
|
555
|
+
await ep.setDaemonEnabled(root, sub === 'on');
|
|
556
|
+
console.log('');
|
|
557
|
+
if (sub === 'on') {
|
|
558
|
+
console.log(' ✅ Daemon mode ON for ' + root);
|
|
559
|
+
console.log(' Restart your MCP clients (Claude Code, Claude Desktop, Codex...). The first one to start');
|
|
560
|
+
console.log(' launches the daemon; the rest attach to it. One index, one model, one writer.');
|
|
561
|
+
console.log(' Clients older than 2.5 keep running their own server — update them to get the benefit.');
|
|
562
|
+
} else {
|
|
563
|
+
for (const { st, reply } of await running()) if (reply) await controlDaemon(root, 'stop', st.build);
|
|
564
|
+
console.log(' ✅ Daemon mode OFF. Clients go back to one full server each the next time they start.');
|
|
565
|
+
}
|
|
566
|
+
console.log('');
|
|
567
|
+
} else if (sub === 'stop') {
|
|
568
|
+
const list = await running();
|
|
569
|
+
for (const { st, reply } of list) if (reply) await controlDaemon(root, 'stop', st.build);
|
|
570
|
+
console.log(list.length ? `\n ✅ Asked ${list.length} daemon(s) to stop. Connected clients carry on: they start a new one on their next call.\n`
|
|
571
|
+
: '\n No daemon is running for this brain.\n');
|
|
572
|
+
} else {
|
|
573
|
+
const list = await running();
|
|
574
|
+
console.log('');
|
|
575
|
+
console.log(' 🧠 CRBRO daemon');
|
|
576
|
+
console.log(' ───────────────');
|
|
577
|
+
console.log(` Brain: ${root}`);
|
|
578
|
+
console.log(` Mode: ${ep.daemonEnabled(root) ? '✅ on' : 'off (npx crbro-memory daemon on)'}`);
|
|
579
|
+
if (list.length === 0) console.log(' Running: none' + (ep.daemonEnabled(root) ? ' — the next client to start launches it' : ''));
|
|
580
|
+
for (const { st, reply } of list) {
|
|
581
|
+
if (!reply) { console.log(` Running: pid ${st.pid} · ${st.version} (${st.build}) · alive but not answering right now (busy, or another user's)`); continue; }
|
|
582
|
+
console.log(` Running: pid ${reply.pid} · ${reply.version} (${reply.build}) · ${reply.connections} conversation(s) · ${reply.rss_mb} MB · up ${Math.round(reply.uptime_s / 60)} min · idle exit ${reply.idle_minutes} min`);
|
|
583
|
+
}
|
|
584
|
+
if (list.length > 1) console.log(' ⚠️ More than one build is serving this brain. Point every client at the same install to share one.');
|
|
585
|
+
console.log(` Log: ${root}${root.includes('\\') ? '\\' : '/'}.daemon${root.includes('\\') ? '\\' : '/'}daemon.log`);
|
|
586
|
+
console.log('');
|
|
587
|
+
}
|
|
588
|
+
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
589
|
+
|
|
590
|
+
} else if (command === 'guard') {
|
|
591
|
+
// ─── What the PreToolUse guard would say for a command ─────────
|
|
592
|
+
//
|
|
593
|
+
// crbro guard "firebase deploy --only hosting"
|
|
594
|
+
// crbro guard --rebuild write the trigger index now
|
|
595
|
+
//
|
|
596
|
+
// Reads <brain>/.search/triggers.json, the lookup crbro_consolidate
|
|
597
|
+
// derives from the error, debt and pattern ledgers. Read-only unless
|
|
598
|
+
// --rebuild is passed.
|
|
599
|
+
(async () => {
|
|
600
|
+
const { Brain } = await import('../dist/engine/brain.js');
|
|
601
|
+
const { writeTriggerIndex, triggerIndexPath, lessonsFor } = await import('../dist/engine/triggers.js');
|
|
602
|
+
const fs = await import('fs');
|
|
603
|
+
const brain = new Brain();
|
|
604
|
+
if (process.argv.includes('--rebuild')) {
|
|
605
|
+
const r = await writeTriggerIndex(brain);
|
|
606
|
+
if ('error' in r) { console.error(` ❌ ${r.error}`); process.exit(1); }
|
|
607
|
+
console.log(` ✅ Trigger index written: ${r.entries} lessons under ${r.keys} commands.`);
|
|
608
|
+
console.log(` ${triggerIndexPath(brain)}`);
|
|
609
|
+
}
|
|
610
|
+
const texto = process.argv.slice(3).filter(a => !a.startsWith('--')).join(' ').trim();
|
|
611
|
+
if (!texto) {
|
|
612
|
+
if (!process.argv.includes('--rebuild')) console.log(' Usage: crbro guard "<command line>" | crbro guard --rebuild');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
let index;
|
|
616
|
+
try {
|
|
617
|
+
index = JSON.parse(fs.readFileSync(triggerIndexPath(brain), 'utf8'));
|
|
618
|
+
} catch {
|
|
619
|
+
console.log(' No trigger index yet. Run: crbro guard --rebuild (crbro_consolidate writes it too).');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const lessons = lessonsFor(index, texto, 10);
|
|
623
|
+
if (lessons.length === 0) { console.log(' Nothing stored mentions that command.'); return; }
|
|
624
|
+
for (const l of lessons) console.log(` • [${l.k}${l.d ? ' · ' + l.d : ''} · ${l.n}] ${l.t}\n`);
|
|
625
|
+
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
626
|
+
|
|
627
|
+
} else if (command === 'usage' || command === 'postmortem') {
|
|
628
|
+
// ─── Token spend and post-mortem, from Claude Code's own logs ──
|
|
629
|
+
//
|
|
630
|
+
// crbro usage [--days N] [--session <id>] [--project <folder>] [--json]
|
|
631
|
+
// crbro postmortem [--days N] [--session <id>] [--project <folder>] [--max N] [--json]
|
|
632
|
+
//
|
|
633
|
+
// Both read ~/.claude/projects/<project>/<session>.jsonl (and, for usage,
|
|
634
|
+
// the subagent logs under <session>/subagents/) through one streaming
|
|
635
|
+
// reader. usage reads message.model and message.usage only; postmortem
|
|
636
|
+
// reads what the person typed and tool names, never a tool's input or
|
|
637
|
+
// result, and redacts everything it prints. Neither writes anything.
|
|
638
|
+
const flag = (name) => { const i = args.indexOf(name); return i !== -1 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : undefined; };
|
|
639
|
+
const intFlag = (name) => {
|
|
640
|
+
const v = flag(name);
|
|
641
|
+
if (v === undefined) return undefined;
|
|
642
|
+
const n = Number(v);
|
|
643
|
+
if (!Number.isInteger(n) || n < 0) { console.error(` ❌ ${name} expects a whole number ≥ 0; got: ${v}`); process.exit(1); }
|
|
644
|
+
return n;
|
|
645
|
+
};
|
|
646
|
+
const session = flag('--session');
|
|
647
|
+
const project = flag('--project');
|
|
648
|
+
// Last 7 days unless told otherwise (--days 0 = all); a named session is looked for everywhere.
|
|
649
|
+
const daysArg = intFlag('--days');
|
|
650
|
+
const days = daysArg !== undefined ? (daysArg === 0 ? undefined : daysArg) : (session ? undefined : 7);
|
|
651
|
+
const max = intFlag('--max');
|
|
652
|
+
const json = args.includes('--json');
|
|
653
|
+
const mod = command === 'usage' ? '../dist/engine/usage.js' : '../dist/engine/postmortem.js';
|
|
654
|
+
import(mod).then(async m => {
|
|
655
|
+
if (command === 'usage') {
|
|
656
|
+
const r = await m.collectUsage({ days, session, project });
|
|
657
|
+
console.log(json ? JSON.stringify(r, null, 2) : m.formatUsage(r));
|
|
658
|
+
} else {
|
|
659
|
+
const r = await m.runPostmortem({ days, session, project, max });
|
|
660
|
+
console.log(json ? JSON.stringify(r, null, 2) : m.formatPostmortem(r));
|
|
661
|
+
}
|
|
662
|
+
}).catch(err => {
|
|
663
|
+
console.error(' ❌ ' + (err && err.code === 'ERR_MODULE_NOT_FOUND' ? 'Build required. Run: npm run build' : err.message));
|
|
664
|
+
process.exit(1);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
} else if (command === 'install-hooks' && process.argv.includes('--verify')) {
|
|
668
|
+
// ─── Are the installed hooks the ones this package ships? ──────
|
|
669
|
+
// SHA-256 of each copy in ~/.claude/crbro-hooks/ against hooks/ here, and
|
|
670
|
+
// whether every CRBRO hook settings.json runs exists. Read-only. Exit 1 on
|
|
671
|
+
// any difference, so it can gate a script.
|
|
672
|
+
// The Claude Code mod is checked too (install-mod): a copy in the home
|
|
673
|
+
// folder that runs in every session, so the same question applies.
|
|
674
|
+
Promise.all([import('../dist/engine/hookverify.js'), import('../dist/engine/modinstall.js')]).then(([
|
|
675
|
+
{ verifyHooks, formatVerify },
|
|
676
|
+
{ verifyMod, formatModVerify },
|
|
677
|
+
]) => {
|
|
678
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
679
|
+
const r = verifyHooks({
|
|
680
|
+
packageDir: join(here, '..', 'hooks'),
|
|
681
|
+
installedDir: join(homedir(), '.claude', 'crbro-hooks'),
|
|
682
|
+
settingsPath: join(homedir(), '.claude', 'settings.json'),
|
|
683
|
+
});
|
|
684
|
+
const m = verifyMod({ packageDir: join(here, '..', 'mods', 'crbro-pending'), home: homedir() });
|
|
685
|
+
if (args.includes('--json')) console.log(JSON.stringify({ ...r, mod: m }, null, 2));
|
|
686
|
+
else console.log(formatVerify(r) + formatModVerify(m));
|
|
687
|
+
if (!r.ok || !m.ok) process.exitCode = 1;
|
|
688
|
+
}).catch(err => {
|
|
689
|
+
console.error(' ❌ ' + (err && err.code === 'ERR_MODULE_NOT_FOUND' ? 'Build required. Run: npm run build' : err.message));
|
|
690
|
+
process.exit(1);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
} else if ((command === 'install-hooks' || command === 'uninstall-hooks') && process.argv.includes('--compact')) {
|
|
694
|
+
// ─── Compact without losing the thread (opt-in, Claude Code only) ──
|
|
695
|
+
//
|
|
696
|
+
// PreCompact → hooks/crbro-lifecycle.mjs pre-compact: a mechanical,
|
|
697
|
+
// redacted checkpoint of the session in <brain>/checkpoints/, plus the
|
|
698
|
+
// "keep what is not saved yet" reminder.
|
|
699
|
+
// SessionStart → hooks/crbro-lifecycle.mjs session-start: the boot notice,
|
|
700
|
+
// the folder and its git remote, and after a compaction what was being
|
|
701
|
+
// done before it.
|
|
702
|
+
//
|
|
703
|
+
// Only Claude Code has PreCompact, so ~/.codex is never touched. The entry
|
|
704
|
+
// install-boot wrote for Claude Code prints the very notice session-start
|
|
705
|
+
// prints, so it is replaced, and the replaced hook is kept aside in
|
|
706
|
+
// crbro-hooks/ so uninstall puts it back exactly. A hand-written CRBRO hook
|
|
707
|
+
// is never replaced: ours is installed with --no-boot / --no-reminder next
|
|
708
|
+
// to it, so nothing is read twice. Merged, never rewritten; idempotent.
|
|
709
|
+
import('fs').then(async fs => {
|
|
710
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
711
|
+
const hookDir = join(homedir(), '.claude', 'crbro-hooks');
|
|
712
|
+
const hookScript = join(hookDir, 'crbro-lifecycle.mjs');
|
|
713
|
+
const replacedPath = join(hookDir, 'crbro-lifecycle.replaced.json');
|
|
714
|
+
const ours = h => !!h && typeof h.command === 'string' && h.command.includes('crbro-lifecycle');
|
|
715
|
+
const entryIsOurs = e => Array.isArray(e?.hooks) && e.hooks.some(ours);
|
|
716
|
+
|
|
717
|
+
let settings = null;
|
|
718
|
+
try {
|
|
719
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
720
|
+
settings = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
|
|
721
|
+
} catch (e) {
|
|
722
|
+
if (fs.existsSync(settingsPath)) {
|
|
723
|
+
console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
|
|
724
|
+
console.error(` ${e.message}`);
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (settings !== null && (typeof settings !== 'object' || Array.isArray(settings))) {
|
|
729
|
+
console.error(` ❌ ${settingsPath} is not a JSON object — not touching it.`);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
for (const event of ['PreCompact', 'SessionStart']) {
|
|
733
|
+
const v = settings?.hooks?.[event];
|
|
734
|
+
if (v !== undefined && !Array.isArray(v)) {
|
|
735
|
+
console.error(` ❌ hooks.${event} in ${settingsPath} is not a list — not touching it.`);
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const write = obj => {
|
|
740
|
+
const tmp = settingsPath + '.' + process.pid + '.tmp';
|
|
741
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
742
|
+
fs.renameSync(tmp, settingsPath);
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
if (command === 'uninstall-hooks') {
|
|
746
|
+
if (!settings || !settings.hooks) {
|
|
747
|
+
console.log(' ⚪ No Claude Code settings with hooks: nothing to remove.');
|
|
748
|
+
} else {
|
|
749
|
+
let removed = 0;
|
|
750
|
+
for (const event of ['PreCompact', 'SessionStart']) {
|
|
751
|
+
const list = settings.hooks[event];
|
|
752
|
+
if (!Array.isArray(list)) continue;
|
|
753
|
+
const kept = [];
|
|
754
|
+
for (const entry of list) {
|
|
755
|
+
if (!entryIsOurs(entry)) { kept.push(entry); continue; }
|
|
756
|
+
const rest = entry.hooks.filter(h => !ours(h));
|
|
757
|
+
removed += entry.hooks.length - rest.length;
|
|
758
|
+
if (rest.length) kept.push({ ...entry, hooks: rest });
|
|
759
|
+
}
|
|
760
|
+
if (kept.length) settings.hooks[event] = kept; else delete settings.hooks[event];
|
|
761
|
+
}
|
|
762
|
+
// Put back what install replaced, unless an equal hook is already there.
|
|
763
|
+
let restored = 0;
|
|
764
|
+
try {
|
|
765
|
+
const rec = JSON.parse(fs.readFileSync(replacedPath, 'utf8'));
|
|
766
|
+
for (const { matcher, hook } of Array.isArray(rec.SessionStart) ? rec.SessionStart : []) {
|
|
767
|
+
const list = settings.hooks.SessionStart = settings.hooks.SessionStart || [];
|
|
768
|
+
if (JSON.stringify(list).includes(JSON.stringify(hook.command))) continue;
|
|
769
|
+
list.push(matcher !== undefined ? { matcher, hooks: [hook] } : { hooks: [hook] });
|
|
770
|
+
restored++;
|
|
771
|
+
}
|
|
772
|
+
} catch { /* nothing was replaced */ }
|
|
773
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
774
|
+
if (removed || restored) {
|
|
775
|
+
write(settings);
|
|
776
|
+
console.log(` ✅ Compact hooks removed (${removed}).${restored ? ` The install-boot SessionStart entry is back (${restored}).` : ''}`);
|
|
777
|
+
console.log(` ${settingsPath}`);
|
|
778
|
+
} else {
|
|
779
|
+
console.log(' ⚪ Compact hooks were not installed. Nothing changed.');
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
for (const f of [hookScript, replacedPath]) { try { fs.unlinkSync(f); } catch { /* not there */ } }
|
|
783
|
+
console.log(' Checkpoints already written stay in <brain>/checkpoints/ and expire after 7 days.');
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// — install —
|
|
788
|
+
if (settings === null && !fs.existsSync(join(homedir(), '.claude'))) {
|
|
789
|
+
console.log(' ⚪ ~/.claude not found: Claude Code is the only client with PreCompact, so nothing was wired.');
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
793
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
794
|
+
fs.copyFileSync(join(here, '..', 'hooks', 'crbro-lifecycle.mjs'), hookScript);
|
|
795
|
+
settings = settings || {};
|
|
796
|
+
settings.hooks = settings.hooks || {};
|
|
797
|
+
const node = `node "${hookScript.split('\\').join('/')}"`;
|
|
798
|
+
let changed = false;
|
|
799
|
+
|
|
800
|
+
// SessionStart
|
|
801
|
+
const start = settings.hooks.SessionStart = settings.hooks.SessionStart || [];
|
|
802
|
+
if (start.some(entryIsOurs)) {
|
|
803
|
+
console.log(' ✅ SessionStart: already installed. Script refreshed.');
|
|
804
|
+
} else {
|
|
805
|
+
const replaced = [];
|
|
806
|
+
for (let i = start.length - 1; i >= 0; i--) {
|
|
807
|
+
const entry = start[i];
|
|
808
|
+
if (!Array.isArray(entry?.hooks) || !entry.hooks.some(isInstallBootHook)) continue;
|
|
809
|
+
for (const h of entry.hooks.filter(isInstallBootHook)) replaced.unshift({ matcher: entry.matcher, hook: h });
|
|
810
|
+
const rest = entry.hooks.filter(h => !isInstallBootHook(h));
|
|
811
|
+
if (rest.length) start[i] = { ...entry, hooks: rest }; else start.splice(i, 1);
|
|
812
|
+
}
|
|
813
|
+
// A hand-written CRBRO start (cat ~/.claude/crbro-session-start.txt…)
|
|
814
|
+
// stays, and ours stops printing the notice it already prints.
|
|
815
|
+
const handWritten = /crbro/i.test(JSON.stringify(start));
|
|
816
|
+
start.push({
|
|
817
|
+
matcher: 'startup|resume|clear|compact',
|
|
818
|
+
hooks: [{ type: 'command', command: `${node} session-start${handWritten ? ' --no-boot' : ''}`, timeout: 10, statusMessage: 'Loading CRBRO memory...' }],
|
|
819
|
+
});
|
|
820
|
+
if (replaced.length) fs.writeFileSync(replacedPath, JSON.stringify({ SessionStart: replaced }, null, 2), 'utf8');
|
|
821
|
+
changed = true;
|
|
822
|
+
console.log(` ✅ SessionStart: lifecycle hook added${replaced.length ? ', replacing the install-boot entry (same notice, plus the project line)' : ''}${handWritten ? '; your own CRBRO start hook is kept and the notice is not repeated' : ''}.`);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// PreCompact
|
|
826
|
+
const pre = settings.hooks.PreCompact = settings.hooks.PreCompact || [];
|
|
827
|
+
if (pre.some(entryIsOurs)) {
|
|
828
|
+
console.log(' ✅ PreCompact: already installed. Script refreshed.');
|
|
829
|
+
} else {
|
|
830
|
+
const handWritten = /crbro/i.test(JSON.stringify(pre));
|
|
831
|
+
pre.push({
|
|
832
|
+
hooks: [{ type: 'command', command: `${node} pre-compact${handWritten ? ' --no-reminder' : ''}`, timeout: 10, statusMessage: 'Saving a CRBRO checkpoint...' }],
|
|
833
|
+
});
|
|
834
|
+
changed = true;
|
|
835
|
+
console.log(` ✅ PreCompact: checkpoint hook added${handWritten ? '; your own CRBRO reminder is kept and not repeated' : ''}.`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (changed) {
|
|
839
|
+
write(settings);
|
|
840
|
+
console.log(` ${settingsPath}`);
|
|
841
|
+
console.log(' Undo with: npx crbro-memory uninstall-hooks --compact');
|
|
842
|
+
}
|
|
843
|
+
console.log(' Never blocks; checkpoints are redacted and expire after 7 days. New sessions pick it up.');
|
|
844
|
+
}).catch(e => { console.error(e); process.exit(1); });
|
|
845
|
+
|
|
846
|
+
} else if (command === 'uninstall-hooks') {
|
|
847
|
+
console.log(' Usage: npx crbro-memory uninstall-hooks --compact');
|
|
848
|
+
console.log(' (the guard and subagent hooks are removed by deleting their entry from ~/.claude/settings.json)');
|
|
849
|
+
|
|
850
|
+
} else if (command === 'install-hooks' && process.argv.includes('--guard')) {
|
|
851
|
+
// ─── Wire the PreToolUse guard into Claude Code (opt-in) ───────
|
|
852
|
+
//
|
|
853
|
+
// Before a Bash or PowerShell call, hooks/crbro-guard.mjs looks the command
|
|
854
|
+
// up in the trigger index and adds the stored errors, debts and patterns
|
|
855
|
+
// that mention it to the model's context — once per session each, three at
|
|
856
|
+
// most. It never blocks and never asks. Remove the PreToolUse entry from
|
|
857
|
+
// ~/.claude/settings.json to uninstall.
|
|
858
|
+
import('fs').then(async fs => {
|
|
859
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
860
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
861
|
+
const hookDir = join(homedir(), '.claude', 'crbro-hooks');
|
|
862
|
+
const hookScript = join(hookDir, 'crbro-guard.mjs');
|
|
863
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
864
|
+
fs.copyFileSync(join(here, '..', 'hooks', 'crbro-guard.mjs'), hookScript);
|
|
865
|
+
|
|
866
|
+
let settings = {};
|
|
867
|
+
try {
|
|
868
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
869
|
+
settings = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
|
|
870
|
+
} catch (e) {
|
|
871
|
+
if (fs.existsSync(settingsPath)) {
|
|
872
|
+
console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
|
|
873
|
+
console.error(` ${e.message}`);
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
settings.hooks = settings.hooks || {};
|
|
878
|
+
const list = settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
|
|
879
|
+
if (JSON.stringify(list).includes('crbro-guard')) {
|
|
880
|
+
console.log(' ✅ PreToolUse guard already installed. Script refreshed.');
|
|
881
|
+
} else {
|
|
882
|
+
list.push({
|
|
883
|
+
matcher: 'Bash|PowerShell',
|
|
884
|
+
hooks: [{ type: 'command', command: `node "${hookScript.split('\\').join('/')}"`, timeout: 5 }],
|
|
885
|
+
});
|
|
886
|
+
const tmp = settingsPath + '.' + process.pid + '.tmp';
|
|
887
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
|
|
888
|
+
fs.renameSync(tmp, settingsPath);
|
|
889
|
+
console.log(' ✅ PreToolUse guard installed.');
|
|
890
|
+
console.log(` ${settingsPath}`);
|
|
891
|
+
}
|
|
892
|
+
const { Brain } = await import('../dist/engine/brain.js');
|
|
893
|
+
const { writeTriggerIndex } = await import('../dist/engine/triggers.js');
|
|
894
|
+
const r = await writeTriggerIndex(new Brain());
|
|
895
|
+
if (!('error' in r)) console.log(` Trigger index: ${r.entries} lessons under ${r.keys} commands. Try: crbro guard "git push"`);
|
|
896
|
+
console.log(' It adds context, never blocks. New sessions pick it up.');
|
|
897
|
+
}).catch(console.error);
|
|
898
|
+
|
|
899
|
+
} else if (command === 'install-hooks') {
|
|
900
|
+
// ─── Wire the SubagentStart hook into Claude Code ──────────────
|
|
901
|
+
//
|
|
902
|
+
// SessionStart context never reaches Task-spawned subagents, so without
|
|
903
|
+
// this every subagent runs without the behavioral protocols the session
|
|
904
|
+
// was booted with. This registers hooks/crbro-subagent.mjs, which reads
|
|
905
|
+
// the same protocol neurons crbro_boot reads — one source of truth.
|
|
906
|
+
//
|
|
907
|
+
// Merges into ~/.claude/settings.json without touching anything else.
|
|
908
|
+
// Idempotent: running it twice changes nothing the second time.
|
|
909
|
+
import('fs').then(async fs => {
|
|
910
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
911
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
912
|
+
const source = join(here, '..', 'hooks', 'crbro-subagent.mjs');
|
|
913
|
+
|
|
914
|
+
// Copy the hook to a stable location. When CRBRO runs from the npx
|
|
915
|
+
// cache, `here` changes with every release and the stale path would
|
|
916
|
+
// break the hook silently on the next update.
|
|
917
|
+
const hookDir = join(homedir(), '.claude', 'crbro-hooks');
|
|
918
|
+
const hookScript = join(hookDir, 'crbro-subagent.mjs');
|
|
919
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
920
|
+
fs.copyFileSync(source, hookScript);
|
|
921
|
+
// Injection is OPT-IN since 1.12 (pre-registered consequence of three
|
|
922
|
+
// clean-control benchmark runs: no measured benefit in any model, harm
|
|
923
|
+
// and fabricated compliance in small ones — see the benchmarks in the
|
|
924
|
+
// card repo). `install-hooks` alone installs the machinery inert;
|
|
925
|
+
// `install-hooks --inject` enables injection for every subagent.
|
|
926
|
+
const conInyeccion = process.argv.includes('--inject');
|
|
927
|
+
const hookCmd = (conInyeccion ? 'CRBRO_SUBAGENT_INJECT=full ' : '') + `node "${hookScript.split('\\').join('/')}"`;
|
|
928
|
+
|
|
929
|
+
let settings = {};
|
|
930
|
+
try {
|
|
931
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8').replace(/^/, ''));
|
|
932
|
+
} catch (e) {
|
|
933
|
+
if (fs.existsSync(settingsPath)) {
|
|
934
|
+
console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
|
|
935
|
+
console.error(` ${e.message}`);
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
settings.hooks = settings.hooks || {};
|
|
941
|
+
const list = settings.hooks.SubagentStart = settings.hooks.SubagentStart || [];
|
|
942
|
+
const yaEsta = JSON.stringify(list).includes('crbro-subagent');
|
|
943
|
+
if (yaEsta) {
|
|
944
|
+
console.log(' ✅ SubagentStart hook already installed. Script refreshed.');
|
|
945
|
+
if (conInyeccion && !JSON.stringify(list).includes('CRBRO_SUBAGENT_INJECT')) {
|
|
946
|
+
console.log(' ⚠️ The installed entry does NOT enable injection. To enable it,');
|
|
947
|
+
console.log(' remove the SubagentStart entry from settings.json and re-run');
|
|
948
|
+
console.log(' install-hooks --inject.');
|
|
949
|
+
}
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
list.push({
|
|
953
|
+
hooks: [{
|
|
954
|
+
type: 'command',
|
|
955
|
+
command: hookCmd,
|
|
956
|
+
timeout: 5,
|
|
957
|
+
statusMessage: 'Inyectando protocolos CRBRO en el subagente...',
|
|
958
|
+
}],
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
const tmp = settingsPath + '.' + process.pid + '.tmp';
|
|
962
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
|
|
963
|
+
fs.renameSync(tmp, settingsPath);
|
|
964
|
+
console.log(' ✅ SubagentStart hook installed.');
|
|
965
|
+
console.log(` ${settingsPath}`);
|
|
966
|
+
if (conInyeccion) {
|
|
967
|
+
console.log(' Injection ENABLED: every Task-spawned subagent receives the');
|
|
968
|
+
console.log(' protocol block. Scope it with CRBRO_SUBAGENT_MATCHER (regex on');
|
|
969
|
+
console.log(' agent_type) if needed. Measured caveat: on small models the');
|
|
970
|
+
console.log(' block bought no benchmarked benefit and induced fabricated');
|
|
971
|
+
console.log(' compliance in some runs — see the benchmarks before scoping.');
|
|
972
|
+
} else {
|
|
973
|
+
console.log(' Injection is OPT-IN and currently OFF (the measured default:');
|
|
974
|
+
console.log(' three clean benchmark runs found no benefit in any model and');
|
|
975
|
+
console.log(' harm in small ones). Re-run with --inject to enable it.');
|
|
976
|
+
}
|
|
977
|
+
}).catch(console.error);
|
|
978
|
+
|
|
979
|
+
} else if (command === 'install-mod' && process.argv.includes('--verify')) {
|
|
980
|
+
// ─── Is the installed mod the one this package ships? ──────────
|
|
981
|
+
// SHA-256 of every file in ~/.claude/crbro-mods/crbro-pending against
|
|
982
|
+
// mods/crbro-pending here, and whether settings.json lists it. Read-only;
|
|
983
|
+
// exit 1 on any difference.
|
|
984
|
+
import('../dist/engine/modinstall.js').then(({ verifyMod, formatModVerify }) => {
|
|
985
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
986
|
+
const m = verifyMod({ packageDir: join(here, '..', 'mods', 'crbro-pending'), home: homedir() });
|
|
987
|
+
console.log(args.includes('--json') ? JSON.stringify(m, null, 2) : formatModVerify(m));
|
|
988
|
+
if (!m.ok) process.exitCode = 1;
|
|
989
|
+
}).catch(err => {
|
|
990
|
+
console.error(' ❌ ' + (err && err.code === 'ERR_MODULE_NOT_FOUND' ? 'Build required. Run: npm run build' : err.message));
|
|
991
|
+
process.exit(1);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
} else if (command === 'install-mod' || command === 'uninstall-mod') {
|
|
995
|
+
// ─── The open-items band for Claude Code ───────────────────────
|
|
996
|
+
//
|
|
997
|
+
// crbro_boot installs it on its own for whoever has Claude Code, and keeps
|
|
998
|
+
// its files up to date (autoInstallMod in modinstall.ts); these commands are
|
|
999
|
+
// the manual way in and the way out. uninstall-mod leaves a mark in
|
|
1000
|
+
// ~/.claude/crbro-mods/state.json so the boot never puts it back, from any
|
|
1001
|
+
// client, and install-mod lifts that mark. CRBRO_MOD=0 turns the automatic
|
|
1002
|
+
// part off in the client whose env has it.
|
|
1003
|
+
//
|
|
1004
|
+
// mods/crbro-pending is a Claude Code mod: CRBRO's open items above the
|
|
1005
|
+
// prompt and /pending (alias /pendientes) with all of them as cards. It is
|
|
1006
|
+
// copied to ~/.claude/crbro-mods/crbro-pending and that folder is added to
|
|
1007
|
+
// env.CLAUDE_CODE_PLUGIN_DIRS in ~/.claude/settings.json (";" on Windows,
|
|
1008
|
+
// ":" elsewhere), once. Nothing else in settings.json is touched, except the
|
|
1009
|
+
// mod's own language when --lang en|es is given. An earlier local copy
|
|
1010
|
+
// (crbro-pendientes) in that list is replaced, and uninstall-mod puts it
|
|
1011
|
+
// back. Same care as install-hooks:
|
|
1012
|
+
// BOM read, unparseable settings left alone, atomic write, idempotent.
|
|
1013
|
+
import('../dist/engine/modinstall.js').then(({ installMod, uninstallMod }) => {
|
|
1014
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1015
|
+
let lang;
|
|
1016
|
+
const at = args.findIndex(a => a === '--lang' || a.startsWith('--lang='));
|
|
1017
|
+
if (at !== -1) {
|
|
1018
|
+
lang = args[at].includes('=') ? args[at].split('=')[1] : args[at + 1];
|
|
1019
|
+
if (!['auto', 'en', 'es'].includes(lang)) {
|
|
1020
|
+
console.error(' ❌ --lang takes auto, en or es.');
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const r = command === 'uninstall-mod'
|
|
1025
|
+
? uninstallMod({ home: homedir() })
|
|
1026
|
+
: installMod({ packageDir: join(here, '..', 'mods', 'crbro-pending'), home: homedir(), lang });
|
|
1027
|
+
if (!r.ok) {
|
|
1028
|
+
console.error(` ❌ ${r.error}`);
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
for (const line of r.lines) console.log(line);
|
|
1032
|
+
// The mod is out, but the mark that keeps the boot from putting it back is not.
|
|
1033
|
+
if (r.markFailed) process.exitCode = 1;
|
|
1034
|
+
}).catch(err => {
|
|
1035
|
+
console.error(' ❌ ' + (err && err.code === 'ERR_MODULE_NOT_FOUND' ? 'Build required. Run: npm run build' : err.message));
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
} else if (command === 'install-boot') {
|
|
1040
|
+
// ─── The step that made the memory look broken ─────────────────
|
|
1041
|
+
//
|
|
1042
|
+
// Installing the MCP server does not call it. Without something that runs
|
|
1043
|
+
// crbro_boot at the start of a conversation, the brain sits there and the
|
|
1044
|
+
// assistant answers from nothing — the memory looks installed and behaves
|
|
1045
|
+
// like it was never there. Every report of "CRBRO does not remember" so far
|
|
1046
|
+
// has been this, not a bug in the recall.
|
|
1047
|
+
//
|
|
1048
|
+
// So this wires the start itself, per client:
|
|
1049
|
+
//
|
|
1050
|
+
// Claude Code ~/.claude/settings.json — SessionStart runs a command whose
|
|
1051
|
+
// stdout is added to the session, telling the model to call
|
|
1052
|
+
// crbro_boot first. Claude Code has no way to invoke an MCP
|
|
1053
|
+
// tool from a hook, so the instruction is the mechanism.
|
|
1054
|
+
// Codex ~/.codex/hooks.json — SessionStart can call an MCP tool
|
|
1055
|
+
// directly (type "mcp_tool"), so it does, AND keeps the same
|
|
1056
|
+
// printed instruction as a second layer: the hook can fire
|
|
1057
|
+
// before the MCP server has finished starting, and then the
|
|
1058
|
+
// direct call is simply lost.
|
|
1059
|
+
//
|
|
1060
|
+
// Only files that already exist are touched, and each one is merged, never
|
|
1061
|
+
// rewritten. Idempotent: a second run reports and changes nothing.
|
|
1062
|
+
import('fs').then(async fs => {
|
|
1063
|
+
// One copy of the notice: the lifecycle hook prints the same text in its
|
|
1064
|
+
// session-start mode, and `install-hooks --compact` swaps this entry for it.
|
|
1065
|
+
const { BOOT_NOTICE: AVISO } = await import('../hooks/crbro-lifecycle.mjs');
|
|
1066
|
+
|
|
1067
|
+
// printf on a shell, Write-Output on Windows PowerShell. One line, no
|
|
1068
|
+
// external file: a path stored in a hook goes stale on the next update.
|
|
1069
|
+
const cmdPosix = bootCommandPosix(AVISO);
|
|
1070
|
+
const cmdWin = `powershell -NoProfile -Command ${JSON.stringify('Write-Output ' + JSON.stringify(AVISO))}`;
|
|
1071
|
+
const MATCHER = 'startup|resume|clear|compact';
|
|
1072
|
+
|
|
1073
|
+
const leerJson = (p) => {
|
|
1074
|
+
if (!fs.existsSync(p)) return null;
|
|
1075
|
+
try {
|
|
1076
|
+
return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^/, ''));
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
console.error(` ❌ ${p} exists but could not be parsed — not touching it.`);
|
|
1079
|
+
console.error(` ${e.message}`);
|
|
1080
|
+
process.exit(1);
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
const escribirJson = (p, obj) => {
|
|
1084
|
+
const tmp = p + '.' + process.pid + '.tmp';
|
|
1085
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
1086
|
+
fs.renameSync(tmp, p);
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
let tocados = 0, yaEstaban = 0, ausentes = [];
|
|
1090
|
+
|
|
1091
|
+
// — Claude Code —
|
|
1092
|
+
const claudePath = join(homedir(), '.claude', 'settings.json');
|
|
1093
|
+
const claude = leerJson(claudePath) ?? (fs.existsSync(join(homedir(), '.claude')) ? {} : null);
|
|
1094
|
+
if (claude === null) {
|
|
1095
|
+
ausentes.push('Claude Code (~/.claude not found)');
|
|
1096
|
+
} else {
|
|
1097
|
+
claude.hooks = claude.hooks || {};
|
|
1098
|
+
const list = claude.hooks.SessionStart = claude.hooks.SessionStart || [];
|
|
1099
|
+
// Any mention of crbro counts as "already wired". A hand-rolled hook
|
|
1100
|
+
// often points at a file — `cat ~/.claude/crbro-session-start.txt` — so
|
|
1101
|
+
// looking for crbro_boot alone misses it and installs a second entry
|
|
1102
|
+
// that boots the brain twice.
|
|
1103
|
+
if (/crbro/i.test(JSON.stringify(list))) {
|
|
1104
|
+
yaEstaban++;
|
|
1105
|
+
console.log(' ⚪ Claude Code: SessionStart already starts CRBRO. Left alone.');
|
|
1106
|
+
} else {
|
|
1107
|
+
list.push({
|
|
1108
|
+
matcher: MATCHER,
|
|
1109
|
+
hooks: [{ type: 'command', command: cmdPosix, shell: 'bash', timeout: 10, statusMessage: 'Loading CRBRO memory...' }],
|
|
1110
|
+
});
|
|
1111
|
+
escribirJson(claudePath, claude);
|
|
1112
|
+
tocados++;
|
|
1113
|
+
console.log(` ✅ Claude Code: SessionStart hook added.\n ${claudePath}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// — Codex —
|
|
1118
|
+
const codexDir = join(homedir(), '.codex');
|
|
1119
|
+
const codexPath = join(codexDir, 'hooks.json');
|
|
1120
|
+
if (!fs.existsSync(codexDir)) {
|
|
1121
|
+
ausentes.push('Codex (~/.codex not found)');
|
|
1122
|
+
} else {
|
|
1123
|
+
const codex = leerJson(codexPath) ?? {};
|
|
1124
|
+
codex.hooks = codex.hooks || {};
|
|
1125
|
+
const list = codex.hooks.SessionStart = codex.hooks.SessionStart || [];
|
|
1126
|
+
// Any mention of crbro counts as "already wired". A hand-rolled hook
|
|
1127
|
+
// often points at a file — `cat ~/.claude/crbro-session-start.txt` — so
|
|
1128
|
+
// looking for crbro_boot alone misses it and installs a second entry
|
|
1129
|
+
// that boots the brain twice.
|
|
1130
|
+
if (/crbro/i.test(JSON.stringify(list))) {
|
|
1131
|
+
yaEstaban++;
|
|
1132
|
+
console.log(' ⚪ Codex: SessionStart already starts CRBRO. Left alone.');
|
|
1133
|
+
} else {
|
|
1134
|
+
list.push({
|
|
1135
|
+
matcher: MATCHER,
|
|
1136
|
+
hooks: [
|
|
1137
|
+
{ type: 'mcp_tool', server: 'crbro', tool: 'crbro_boot', input: {}, timeout: 30, statusMessage: 'Loading CRBRO memory...' },
|
|
1138
|
+
{ type: 'command', command: cmdPosix, commandWindows: cmdWin, timeout: 10, statusMessage: 'Checking CRBRO start...' },
|
|
1139
|
+
],
|
|
1140
|
+
});
|
|
1141
|
+
escribirJson(codexPath, codex);
|
|
1142
|
+
tocados++;
|
|
1143
|
+
console.log(` ✅ Codex: SessionStart hook added (MCP call + fallback).\n ${codexPath}`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
console.log('');
|
|
1148
|
+
if (!tocados && !yaEstaban) {
|
|
1149
|
+
console.log(' ⚠️ Neither ~/.claude nor ~/.codex was found, so nothing was wired.');
|
|
1150
|
+
}
|
|
1151
|
+
for (const a of ausentes) console.log(` ⚪ Skipped ${a}`);
|
|
1152
|
+
console.log('');
|
|
1153
|
+
console.log(' Other tools (Cursor, Windsurf, Antigravity…) have no session hooks.');
|
|
1154
|
+
console.log(' Put this line in their always-on rules file (.cursorrules, .windsurfrules,');
|
|
1155
|
+
console.log(' User Rules) and it does the same job:');
|
|
1156
|
+
console.log('');
|
|
1157
|
+
console.log(` ${AVISO}`);
|
|
1158
|
+
console.log('');
|
|
1159
|
+
if (tocados) console.log(' Restart the tool, then check a new conversation actually boots CRBRO.');
|
|
1160
|
+
console.log('');
|
|
1161
|
+
}).catch(console.error);
|
|
1162
|
+
|
|
1163
|
+
} else if (command === 'secret') {
|
|
1164
|
+
// ─── Credentials, from the terminal ────────────────────────────
|
|
1165
|
+
//
|
|
1166
|
+
// Until 2.3 the only way to store a credential was the crbro_secret MCP
|
|
1167
|
+
// tool, which means typing it into a conversation with a model. For a
|
|
1168
|
+
// module whose whole point is that a secret never touches the brain, that
|
|
1169
|
+
// was the wrong last mile. These subcommands close it: the value is read
|
|
1170
|
+
// from stdin, never from argv, so it stays out of the shell history and
|
|
1171
|
+
// out of the process table where `ps` and Task Manager can read it.
|
|
1172
|
+
const sub = args[1];
|
|
1173
|
+
const name = args[2];
|
|
1174
|
+
|
|
1175
|
+
const readPiped = () => new Promise((resolve, reject) => {
|
|
1176
|
+
let data = '';
|
|
1177
|
+
process.stdin.setEncoding('utf8');
|
|
1178
|
+
process.stdin.on('data', (d) => { data += d; });
|
|
1179
|
+
process.stdin.on('end', () => resolve(data.replace(/\r?\n$/, '')));
|
|
1180
|
+
process.stdin.on('error', reject);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// Interactive read with the echo off. Raw mode hands us every keystroke,
|
|
1184
|
+
// so nothing is drawn and nothing survives in the terminal scrollback.
|
|
1185
|
+
const readHidden = (promptText) => new Promise((resolve, reject) => {
|
|
1186
|
+
const stdin = process.stdin;
|
|
1187
|
+
let value = '';
|
|
1188
|
+
const cleanup = () => {
|
|
1189
|
+
stdin.removeListener('data', onData);
|
|
1190
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
1191
|
+
stdin.pause();
|
|
1192
|
+
};
|
|
1193
|
+
const onData = (chunk) => {
|
|
1194
|
+
for (const ch of chunk.toString('utf8')) {
|
|
1195
|
+
if (ch === '\r' || ch === '\n') { cleanup(); process.stderr.write('\n'); return resolve(value); }
|
|
1196
|
+
if (ch === '\u0003') { cleanup(); process.stderr.write('\n'); return reject(new Error('Cancelled — nothing was stored.')); }
|
|
1197
|
+
if (ch === '\u007f' || ch === '\b') { value = value.slice(0, -1); continue; }
|
|
1198
|
+
value += ch;
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
process.stderr.write(promptText);
|
|
1202
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
1203
|
+
stdin.resume();
|
|
1204
|
+
stdin.on('data', onData);
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
const flagValue = (flag) => {
|
|
1208
|
+
const i = args.indexOf(flag);
|
|
1209
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : '';
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
import('../dist/engine/keychain.js').then(async (kc) => {
|
|
1213
|
+
try {
|
|
1214
|
+
if (sub === 'status') {
|
|
1215
|
+
const { backend, reason } = kc.detectBackend();
|
|
1216
|
+
console.log('');
|
|
1217
|
+
console.log(' 🔐 CRBRO keychain');
|
|
1218
|
+
console.log(' ─────────────────');
|
|
1219
|
+
console.log(` Backend: ${backend || '❌ none available'}`);
|
|
1220
|
+
if (reason) console.log(` Reason: ${reason}`);
|
|
1221
|
+
if (backend === 'windows-dpapi') {
|
|
1222
|
+
const dir = process.env['CRBRO_KEYS_DIR'] || join(homedir(), '.crbro-keys');
|
|
1223
|
+
console.log(` Store: ${join(dir, 'keys.dpapi')}`);
|
|
1224
|
+
console.log(' Sealed to this Windows account: copied to another');
|
|
1225
|
+
console.log(' machine or lifted from a backup, it is unreadable.');
|
|
1226
|
+
}
|
|
1227
|
+
console.log('');
|
|
1228
|
+
console.log(' The store lives outside the brain. No sync, no team space and no');
|
|
1229
|
+
console.log(' crbro_share can reach it — per machine, on purpose.');
|
|
1230
|
+
console.log('');
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (sub === 'list') {
|
|
1235
|
+
const secrets = kc.listSecrets();
|
|
1236
|
+
console.log('');
|
|
1237
|
+
if (secrets.length === 0) {
|
|
1238
|
+
console.log(' 🔐 No credentials stored on this machine yet.');
|
|
1239
|
+
console.log(' Store one: npx crbro-memory secret set MY_TOKEN');
|
|
1240
|
+
console.log('');
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
console.log(` 🔐 ${secrets.length} credential(s) — names only, values are never listed.`);
|
|
1244
|
+
console.log(' ──────────────────────────────────────────────────────────');
|
|
1245
|
+
for (const s of secrets) {
|
|
1246
|
+
console.log(` ${s.name.padEnd(28)} ${s.updated} ${s.description || ''}`.trimEnd());
|
|
1247
|
+
}
|
|
1248
|
+
console.log('');
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (!name) {
|
|
1253
|
+
console.error(` ❌ Missing name. Usage: npx crbro-memory secret ${sub || '<set|get|list|remove|status>'} NAME`);
|
|
1254
|
+
process.exit(1);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (sub === 'set') {
|
|
1258
|
+
const value = process.stdin.isTTY
|
|
1259
|
+
? await readHidden(` Value for ${name} (input hidden, Enter to finish): `)
|
|
1260
|
+
: await readPiped();
|
|
1261
|
+
if (!value) {
|
|
1262
|
+
console.error(' ❌ Empty value. Nothing was stored.');
|
|
1263
|
+
process.exit(1);
|
|
1264
|
+
}
|
|
1265
|
+
kc.setSecret(name, value, flagValue('--description'));
|
|
1266
|
+
console.log(` ✅ ${name} sealed in the OS keychain. CRBRO keeps no copy.`);
|
|
1267
|
+
console.log(' Record only the NAME in the brain, never the value.');
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (sub === 'get') {
|
|
1272
|
+
const value = kc.getSecret(name);
|
|
1273
|
+
if (value === null) {
|
|
1274
|
+
console.error(` ❌ ${name} not found on this machine.`);
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}
|
|
1277
|
+
if (process.stdout.isTTY) {
|
|
1278
|
+
process.stderr.write(' ⚠️ Printing a credential to the screen. Pipe it instead to keep it out of the scrollback.\n');
|
|
1279
|
+
}
|
|
1280
|
+
process.stdout.write(value + '\n');
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (sub === 'remove') {
|
|
1285
|
+
if (!args.includes('--yes')) {
|
|
1286
|
+
console.error(` ❌ This deletes ${name} from the keychain and cannot be undone.`);
|
|
1287
|
+
console.error(` Re-run to confirm: npx crbro-memory secret remove ${name} --yes`);
|
|
1288
|
+
process.exit(1);
|
|
1289
|
+
}
|
|
1290
|
+
const removed = kc.removeSecret(name);
|
|
1291
|
+
console.log(removed ? ` ✅ ${name} removed.` : ` ⚪ ${name} was not there. Nothing changed.`);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
console.error(' Usage: npx crbro-memory secret <set|get|list|remove|status> [NAME]');
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
console.error(` ❌ ${err instanceof Error ? err.message : err}`);
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
}).catch((err) => {
|
|
1302
|
+
console.error(` ❌ ${err instanceof Error ? err.message : err}`);
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
} else if (command === 'backup') {
|
|
1307
|
+
// ─── Backup: one gzipped file per copy, rotated ────────────────
|
|
1308
|
+
// `backup` makes one · `backup list` · `backup restore FILE [--into DIR]`
|
|
1309
|
+
const sub = args[1] && !args[1].startsWith('--') ? args[1] : 'create';
|
|
1310
|
+
const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
|
|
1311
|
+
Promise.all([import('../dist/engine/brain.js'), import('../dist/engine/backup.js')]).then(async ([{ BrainPaths }, bk]) => {
|
|
1312
|
+
const paths = new BrainPaths();
|
|
1313
|
+
const dir = flag('--dir') || bk.resolveBackupDir(paths.root);
|
|
1314
|
+
const kb = (n) => `${(n / 1024).toFixed(0)} KB`;
|
|
1315
|
+
try {
|
|
1316
|
+
if (sub === 'create') {
|
|
1317
|
+
const keep = flag('--keep') ? Number(flag('--keep')) : undefined;
|
|
1318
|
+
const r = await bk.createBackup(paths, { dir, keep });
|
|
1319
|
+
console.log('');
|
|
1320
|
+
console.log(` ✅ Backup written: ${r.path}`);
|
|
1321
|
+
console.log(` ${r.counts.neurons} neurons · ${r.counts.sessions} sessions · ${r.counts.synapses} synapses · ${kb(r.counts.bytes)} of text → ${kb(r.size_bytes)} on disk`);
|
|
1322
|
+
if (r.rotated_out.length) console.log(` Rotated out: ${r.rotated_out.join(', ')}`);
|
|
1323
|
+
console.log(' Left out on purpose: the quarantine, machine tokens, the search index and the semantic runtime.');
|
|
1324
|
+
if (!process.env.CRBRO_BACKUP_DIR) {
|
|
1325
|
+
console.log('');
|
|
1326
|
+
console.log(' ⚠️ This folder is on the same disk as the brain. It survives a bad write or a wrong');
|
|
1327
|
+
console.log(' forget, not a dead disk. Point CRBRO_BACKUP_DIR at a folder that is already synced');
|
|
1328
|
+
console.log(' (Drive in mirror mode, Dropbox, an external disk) to cover that too.');
|
|
1329
|
+
}
|
|
1330
|
+
console.log('');
|
|
1331
|
+
} else if (sub === 'list') {
|
|
1332
|
+
const all = await bk.listBackups(dir);
|
|
1333
|
+
console.log('');
|
|
1334
|
+
console.log(` Backups in ${dir}`);
|
|
1335
|
+
if (!all.length) console.log(' (none yet — run: npx crbro-memory backup)');
|
|
1336
|
+
for (const b of all) console.log(` ${b.file} ${b.created.slice(0, 16).replace('T', ' ')} UTC ${kb(b.size_bytes)}`);
|
|
1337
|
+
console.log('');
|
|
1338
|
+
} else if (sub === 'restore') {
|
|
1339
|
+
const file = args[2];
|
|
1340
|
+
if (!file || file.startsWith('--')) {
|
|
1341
|
+
console.error(' Usage: npx crbro-memory backup restore FILE [--into DIR]');
|
|
1342
|
+
process.exit(1);
|
|
1343
|
+
}
|
|
1344
|
+
const src = existsSync(file) ? file : join(dir, file);
|
|
1345
|
+
const into = flag('--into') || join(homedir(), `.crbro-restored-${Date.now()}`);
|
|
1346
|
+
const r = await bk.restoreBackup(src, into);
|
|
1347
|
+
console.log('');
|
|
1348
|
+
console.log(` ✅ Restored ${r.files} files into ${r.into}`);
|
|
1349
|
+
console.log(' Nothing was written over your live brain. To switch to the restored copy, close every');
|
|
1350
|
+
console.log(' client using CRBRO, move ~/.crbro aside, rename this folder to ~/.crbro, then run:');
|
|
1351
|
+
console.log(' npx crbro-memory reindex');
|
|
1352
|
+
console.log('');
|
|
1353
|
+
} else {
|
|
1354
|
+
console.error(' Usage: npx crbro-memory backup [list | restore FILE [--into DIR]] [--dir DIR] [--keep N]');
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
console.error(` ❌ ${err.message}`);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
}).catch(err => {
|
|
1362
|
+
console.error(' ❌ Build required. Run: npm run build');
|
|
1363
|
+
console.error(' ', err.message);
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
} else if (command === '--help' || command === '-h') {
|
|
1368
|
+
// ─── Help ──────────────────────────────────────────────────────
|
|
1369
|
+
console.log('');
|
|
1370
|
+
console.log(' 🧠 CRBRO — Persistent Neural Memory for AI');
|
|
1371
|
+
console.log(' ═══════════════════════════════════════════');
|
|
1372
|
+
console.log('');
|
|
1373
|
+
console.log(' Setup:');
|
|
1374
|
+
console.log(' npx crbro-memory init Initialize brain + detect IDEs');
|
|
1375
|
+
console.log(' npx crbro-memory install-boot Make the memory load itself in every conversation');
|
|
1376
|
+
console.log(' npx crbro-memory status Show brain status');
|
|
1377
|
+
console.log('');
|
|
1378
|
+
console.log(' Auto-Mining:');
|
|
1379
|
+
console.log(' npx crbro-memory mine [dir] One-shot mining of artifacts');
|
|
1380
|
+
console.log(' npx crbro-memory setup-miner Install scheduled auto-miner');
|
|
1381
|
+
console.log(' npx crbro-memory miner-status Check auto-miner status');
|
|
1382
|
+
console.log(' npx crbro-memory remove-miner Remove auto-miner');
|
|
1383
|
+
console.log('');
|
|
1384
|
+
console.log(' Backup (one gzipped file per copy; consolidate makes one a day on its own):');
|
|
1385
|
+
console.log(' npx crbro-memory backup Back the brain up now (keeps the newest 7)');
|
|
1386
|
+
console.log(' npx crbro-memory backup list What there is, newest first');
|
|
1387
|
+
console.log(' npx crbro-memory backup restore FILE Unpack into a NEW folder, never over the live brain');
|
|
1388
|
+
console.log(' CRBRO_BACKUP_DIR=<synced folder> Put them somewhere that survives the disk');
|
|
1389
|
+
console.log('');
|
|
1390
|
+
console.log(' Guard (opt-in, Claude Code): stored lessons speak before a shell command runs:');
|
|
1391
|
+
console.log(' npx crbro-memory install-hooks --guard Wire the PreToolUse hook (adds context, never blocks)');
|
|
1392
|
+
console.log(' npx crbro-memory guard "<command>" What it would say for a command · --rebuild writes the index');
|
|
1393
|
+
console.log('');
|
|
1394
|
+
console.log(' Compact without losing the thread (opt-in, Claude Code):');
|
|
1395
|
+
console.log(' npx crbro-memory install-hooks --compact PreCompact checkpoint + SessionStart that picks it back up');
|
|
1396
|
+
console.log(' npx crbro-memory uninstall-hooks --compact Remove both and put back what was replaced');
|
|
1397
|
+
console.log(' npx crbro-memory install-hooks --verify SHA-256 of the installed hooks and mod against this package (read-only)');
|
|
1398
|
+
console.log(' npx crbro-memory install-mod [--lang en|es|auto] Claude Code band + /pending with the open items (Claude Code 2.1.286+)');
|
|
1399
|
+
console.log(' crbro_boot installs and updates it on its own; CRBRO_MOD=0 turns that off');
|
|
1400
|
+
console.log(' npx crbro-memory uninstall-mod Remove the mod and its CLAUDE_CODE_PLUGIN_DIRS entry; never put back on its own');
|
|
1401
|
+
console.log(' npx crbro-memory install-mod --verify SHA-256 of the installed mod against this package (read-only)');
|
|
1402
|
+
console.log('');
|
|
1403
|
+
console.log(' Looking back at Claude Code sessions (read-only; last 7 days unless --days N, 0 = all):');
|
|
1404
|
+
console.log(' npx crbro-memory usage [--session ID] [--project DIR] [--json] Tokens per model and session, subagents apart');
|
|
1405
|
+
console.log(' npx crbro-memory postmortem [--max N] [--json] Candidate lessons: corrections, failing tools, repeats');
|
|
1406
|
+
console.log('');
|
|
1407
|
+
console.log(' Daemon (opt-in): one process owns the brain for every client — one index, one model, one writer:');
|
|
1408
|
+
console.log(' npx crbro-memory daemon on | off Switch every client of this brain, the next time each starts');
|
|
1409
|
+
console.log(' npx crbro-memory daemon status Is it on, who is running, how much it holds');
|
|
1410
|
+
console.log(' npx crbro-memory daemon stop Stop it; clients start a new one on demand');
|
|
1411
|
+
console.log('');
|
|
1412
|
+
console.log(' Search:');
|
|
1413
|
+
console.log(' npx crbro-memory reindex Rebuild the search index');
|
|
1414
|
+
console.log(' npx crbro-memory eval Measure retrieval against .crbro/.eval/queries.json');
|
|
1415
|
+
console.log('');
|
|
1416
|
+
console.log(' Semantic layer (opt-in, ~500 MB on disk, measured in benchmarks/):');
|
|
1417
|
+
console.log(' npx crbro-memory semantic install Install transformers.js into ~/.crbro/.semantic');
|
|
1418
|
+
console.log(' npx crbro-memory semantic build Embed the whole brain once (needs CRBRO_SEMANTIC=1)');
|
|
1419
|
+
console.log(' npx crbro-memory semantic status Runtime, model and whether it is enabled');
|
|
1420
|
+
console.log('');
|
|
1421
|
+
console.log(' Credentials (per machine, sealed in the OS keychain):');
|
|
1422
|
+
console.log(' npx crbro-memory secret set NAME Store one; the value is read from stdin, never argv');
|
|
1423
|
+
console.log(' npx crbro-memory secret list Names only, never values');
|
|
1424
|
+
console.log(' npx crbro-memory secret get NAME Print one; pipe it to keep it off the screen');
|
|
1425
|
+
console.log(' npx crbro-memory secret remove NAME --yes');
|
|
1426
|
+
console.log(' npx crbro-memory secret status Which keychain this machine offers');
|
|
1427
|
+
console.log('');
|
|
1428
|
+
console.log(' Server:');
|
|
1429
|
+
console.log(' npx crbro-memory Start MCP server (stdio)');
|
|
1430
|
+
console.log('');
|
|
1431
|
+
console.log(' Open source (MIT) — https://github.com/Octonove/crbro-memory');
|
|
1432
|
+
console.log('');
|
|
1433
|
+
|
|
1434
|
+
} else {
|
|
1435
|
+
// ─── Default: start MCP server ─────────────────────────────────
|
|
1436
|
+
import('../dist/index.js').catch(console.error);
|
|
1437
|
+
}
|