crbro-memory 2.9.1 → 2.11.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/LICENSE +21 -21
- package/README.md +256 -456
- package/SECURITY.md +135 -8
- package/bin/crbro.mjs +555 -299
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +143 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +762 -311
- package/dist/engine/doctor.js +676 -0
- package/dist/engine/evidence.js +171 -0
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +50 -36
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +735 -144
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +126 -69
- package/dist/engine/projects.js +80 -0
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +34 -11
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +810 -176
- package/dist/search/quality.js +31 -0
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +649 -343
- package/dist/sync/git.js +203 -72
- package/dist/sync/materialize.js +33 -8
- package/dist/sync/space.js +286 -101
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +219 -52
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +12 -8
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -670
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -61
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -156
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -323
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -35
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -142
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -116
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -346
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -11
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/bin/crbro.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// remove-miner, install-hooks, install-mod, and MCP server mode (default)
|
|
7
7
|
|
|
8
8
|
import { platform, homedir } from 'os';
|
|
9
|
-
import { join, dirname } from 'path';
|
|
9
|
+
import { join, dirname, basename } from 'path';
|
|
10
10
|
import { existsSync, readFileSync } from 'fs';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
|
|
@@ -36,6 +36,202 @@ function isInstallBootHook(h) {
|
|
|
36
36
|
const args = process.argv.slice(2);
|
|
37
37
|
const command = args[0];
|
|
38
38
|
|
|
39
|
+
// ─── Commands ────────────────────────────────────────────────────
|
|
40
|
+
//
|
|
41
|
+
// Every command the CLI knows, one entry each. Before anything runs, the
|
|
42
|
+
// arguments are checked against its entry: an unknown command or flag is
|
|
43
|
+
// rejected with a short help and exit code 2 (it used to start the MCP
|
|
44
|
+
// server, or — `install-hooks --help` — install), `<command> --help` prints
|
|
45
|
+
// the entry and runs nothing, `--version` prints the version.
|
|
46
|
+
//
|
|
47
|
+
// usage the synopsis, as printed by --help and in errors
|
|
48
|
+
// about one or more lines of description
|
|
49
|
+
// flags switches it accepts; values: flags that take a value (--x v or --x=v)
|
|
50
|
+
// args how many positional arguments it takes (default 0)
|
|
51
|
+
// free its own parser checks the arguments (daemon, guard, backup)
|
|
52
|
+
// run async (args) => void, for commands dispatched from this table;
|
|
53
|
+
// the older ones are dispatched by the if/else chain further down.
|
|
54
|
+
//
|
|
55
|
+
// To add a command: one entry here, with `run` importing its module, e.g.
|
|
56
|
+
// view: { usage: 'view [--port N]', about: ['Local viewer'], values: ['--port'], run: a => import(<its module in dist>).then(m => m.main(a)) },
|
|
57
|
+
// A command that fails sets process.exitCode = 1 (or throws: fail() below does it).
|
|
58
|
+
const COMMANDS = {
|
|
59
|
+
install: { usage: 'install [--clients auto|claude-code,claude-desktop,codex,cursor] [--dry-run] [--engine-dir DIR] [--json]',
|
|
60
|
+
about: ['Install CRBRO for every MCP client found: copies this engine to ~/.crbro-engine (no npx at startup),',
|
|
61
|
+
'registers it in each client (Codex with timeouts), and wires the session start. Prints what it did.'],
|
|
62
|
+
flags: ['--dry-run', '--json'], values: ['--clients', '--engine-dir'] },
|
|
63
|
+
uninstall: { usage: 'uninstall [--clients auto|claude-code,claude-desktop,codex,cursor] [--dry-run] [--engine-dir DIR] [--json]',
|
|
64
|
+
about: ['Remove what CRBRO added: registrations that run it, its hooks and boot wiring, the mod, the engine folder.',
|
|
65
|
+
'Backups are kept and the brain (~/.crbro) is never touched.'],
|
|
66
|
+
flags: ['--dry-run', '--json'], values: ['--clients', '--engine-dir'] },
|
|
67
|
+
init: { usage: 'init [--no-semantic]', about: ['Initialize the brain, detect clients, install semantic recall (skip with --no-semantic)'], flags: ['--no-semantic'] },
|
|
68
|
+
'install-boot': { usage: 'install-boot [--dry-run]', about: ['Make the memory load itself at the start of every conversation (Claude Code, Codex)'], flags: ['--dry-run'] },
|
|
69
|
+
status: { usage: 'status', about: ['Show brain status'] },
|
|
70
|
+
doctor: { usage: 'doctor [--json]', about: ['Read-only local diagnostics: brain, clients, hooks, daemon leftovers, backups. Never initializes or boots'], flags: ['--json'] },
|
|
71
|
+
activate: { usage: 'activate', about: ['Legacy (pre-1.4.0): CRBRO is free, nothing to activate'] },
|
|
72
|
+
mine: { usage: 'mine [dir]', about: ['One-shot mining of editor artifacts'], args: 1 },
|
|
73
|
+
'setup-miner': { usage: 'setup-miner', about: ['Install the scheduled auto-miner'] },
|
|
74
|
+
'miner-status': { usage: 'miner-status', about: ['Check auto-miner status'] },
|
|
75
|
+
'remove-miner': { usage: 'remove-miner', about: ['Remove the auto-miner'] },
|
|
76
|
+
reindex: { usage: 'reindex', about: ['Rebuild the search index'] },
|
|
77
|
+
semantic: { usage: 'semantic [status|install|build]', about: ['Semantic recall (installed by init): status, install the runtime, embed the whole brain'], args: 1, choices: ['status', 'install', 'build'] },
|
|
78
|
+
eval: { usage: 'eval', about: ['Measure retrieval against <brain>/.eval/queries.json'] },
|
|
79
|
+
daemon: { usage: 'daemon [status|on|off|stop]', about: ['One process owns the brain for every client (opt-in)'], free: true },
|
|
80
|
+
guard: { usage: 'guard "<command line>" | guard --rebuild', about: ['What the PreToolUse guard would say for a command; --rebuild writes the trigger index'], free: true },
|
|
81
|
+
usage: { usage: 'usage [--days N] [--session ID] [--project DIR] [--json]', about: ['Tokens per model and session from Claude Code logs (read-only)'], flags: ['--json'], values: ['--days', '--session', '--project'] },
|
|
82
|
+
postmortem: { usage: 'postmortem [--days N] [--session ID] [--project DIR] [--max N] [--json]', about: ['Candidate lessons from Claude Code logs (read-only)'], flags: ['--json'], values: ['--days', '--session', '--project', '--max'] },
|
|
83
|
+
'install-hooks': { usage: 'install-hooks [--inject] | --guard | --compact | --verify [--json]',
|
|
84
|
+
about: ['Claude Code hooks: SubagentStart (alone; --inject enables injection), the PreToolUse guard (--guard),',
|
|
85
|
+
'compaction checkpoints (--compact), or check the installed copies against this package (--verify)'],
|
|
86
|
+
flags: ['--inject', '--guard', '--compact', '--verify', '--json'] },
|
|
87
|
+
'uninstall-hooks': { usage: 'uninstall-hooks --compact', about: ['Remove the compaction hooks and put back what they replaced'], flags: ['--compact'] },
|
|
88
|
+
'install-mod': { usage: 'install-mod [--lang en|es|auto] | install-mod --verify [--json]', about: ['Claude Code band + /pending with the open items'], flags: ['--verify', '--json'], values: ['--lang'] },
|
|
89
|
+
'uninstall-mod': { usage: 'uninstall-mod', about: ['Remove the mod and its CLAUDE_CODE_PLUGIN_DIRS entry; never put back on its own'] },
|
|
90
|
+
secret: { usage: 'secret <set|get|list|remove|status|copies|restore> [NAME] [--description TEXT] [--yes] [--force]', about: ['Credentials, sealed in the OS keychain'], args: 2, flags: ['--yes', '--force'], values: ['--description'] },
|
|
91
|
+
backup: { usage: 'backup [list | restore FILE [--into DIR]] [--dir DIR] [--keep N]', about: ['Back the brain up now, list the copies, or restore one into a NEW folder'], free: true },
|
|
92
|
+
// 2.11 (G): each module parses its own arguments and returns 0, 1 or 2.
|
|
93
|
+
export: { usage: 'export [--out DIR] [--format md|json] [--include-retired]',
|
|
94
|
+
about: ['Write the brain to an Obsidian vault (Markdown) or one JSON file. Read-only, credentials redacted,', 'never inside the brain or into a non-empty folder'],
|
|
95
|
+
free: true, run: a => import('../dist/cli/export.js').then(m => m.main(a)).then(code => { if (code) process.exitCode = code; }) },
|
|
96
|
+
import: { usage: 'import PATH [--apply] [--topic NAME] [--type fact|preference|pattern]',
|
|
97
|
+
about: ['Learn what a CLAUDE.md / AGENTS.md, a Claude Code memory folder or a CRBRO export says.', 'Dry run unless --apply; importing the same thing twice adds nothing'],
|
|
98
|
+
free: true, run: a => import('../dist/cli/import.js').then(m => m.main(a)).then(code => { if (code) process.exitCode = code; }) },
|
|
99
|
+
view: { usage: 'view [--port N] [--no-open]',
|
|
100
|
+
about: ['A local page on 127.0.0.1 with a one-time token: neurons, entries, search, open items, stale entries'],
|
|
101
|
+
free: true, run: a => import('../dist/cli/view.js').then(m => m.main(a)).then(code => process.exit(code ?? 0)) },
|
|
102
|
+
serve: { usage: 'serve --http [--port 8787] [--host 127.0.0.1] [--allow-remote] [--allowed-host H] [--path /mcp] [--new-token] [--allow-secrets]',
|
|
103
|
+
about: ['MCP over Streamable HTTP with a bearer token (CRBRO_HTTP_TOKEN or the keychain). Loopback unless --allow-remote;', 'no TLS, no OAuth. crbro_secret is left out unless --allow-secrets'],
|
|
104
|
+
free: true, run: a => import('../dist/cli/serve.js').then(m => m.main(a)).then(code => process.exit(code ?? 0)) },
|
|
105
|
+
help: { usage: 'help [command]', about: ['This help, or one command\'s'], args: 1 },
|
|
106
|
+
};
|
|
107
|
+
/** Arguments that start the MCP server, as no arguments do. */
|
|
108
|
+
const SERVER_FLAGS = new Set(['--daemon', '--proxy', '--stdio']);
|
|
109
|
+
|
|
110
|
+
function commandHelp(name) {
|
|
111
|
+
const c = COMMANDS[name];
|
|
112
|
+
const lines = ['', ` npx crbro-memory ${c.usage}`, ''];
|
|
113
|
+
for (const l of c.about) lines.push(` ${l}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function shortHelp() {
|
|
119
|
+
return ` Commands: ${Object.keys(COMMANDS).join(', ')}\n npx crbro-memory --help for all of them, npx crbro-memory <command> --help for one.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Exit code 2: the command line itself is wrong. Nothing ran. */
|
|
123
|
+
function usageError(message) {
|
|
124
|
+
console.error(` ❌ ${message}`);
|
|
125
|
+
console.error(shortHelp());
|
|
126
|
+
process.exit(2);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A command that failed: a short message (never a stack trace) and exit code 1. */
|
|
130
|
+
function fail(err) {
|
|
131
|
+
const msg = err && err.code === 'ERR_MODULE_NOT_FOUND' ? 'Build required. Run: npm run build'
|
|
132
|
+
: err instanceof Error ? err.message : String(err);
|
|
133
|
+
console.error(` ❌ ${msg}`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Checks the arguments against the command's entry; exits 2 on anything it does not take. */
|
|
138
|
+
function checkArgs(name, rest) {
|
|
139
|
+
const c = COMMANDS[name];
|
|
140
|
+
if (c.free) return;
|
|
141
|
+
const flags = new Set(c.flags || []);
|
|
142
|
+
const values = new Set(c.values || []);
|
|
143
|
+
const positional = [];
|
|
144
|
+
for (let i = 0; i < rest.length; i++) {
|
|
145
|
+
const a = rest[i];
|
|
146
|
+
if (a.startsWith('-')) {
|
|
147
|
+
const [flag, inline] = a.split(/=(.*)/s);
|
|
148
|
+
if (values.has(flag)) {
|
|
149
|
+
if (inline === undefined) {
|
|
150
|
+
if (rest[i + 1] === undefined || rest[i + 1].startsWith('--')) usageError(`${flag} needs a value. Usage: npx crbro-memory ${c.usage}`);
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
} else if (!flags.has(flag) || inline !== undefined) {
|
|
154
|
+
usageError(`unknown option for ${name}: ${a}. Usage: npx crbro-memory ${c.usage}`);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
positional.push(a);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (positional.length > (c.args || 0)) usageError(`unexpected argument for ${name}: ${positional[c.args || 0]}. Usage: npx crbro-memory ${c.usage}`);
|
|
161
|
+
if (c.choices && positional[0] !== undefined && !c.choices.includes(positional[0])) {
|
|
162
|
+
usageError(`${name} takes ${c.choices.join(', ')}; got: ${positional[0]}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Runs before the if/else chain. True when it handled the command line
|
|
168
|
+
* (version, help, an error, or a command with `run`); false to go on.
|
|
169
|
+
*/
|
|
170
|
+
function preDispatch() {
|
|
171
|
+
if (command === undefined || SERVER_FLAGS.has(command)) return false; // the MCP server
|
|
172
|
+
if (command === '--version' || command === '-v' || command === 'version') { console.log(pkgVersion()); return true; }
|
|
173
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
174
|
+
const topic = args[1];
|
|
175
|
+
if (topic && !topic.startsWith('-')) {
|
|
176
|
+
if (!COMMANDS[topic]) usageError(`unknown command: ${topic}`);
|
|
177
|
+
console.log(commandHelp(topic));
|
|
178
|
+
} else printHelp();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
if (!Object.prototype.hasOwnProperty.call(COMMANDS, command)) {
|
|
182
|
+
usageError(command.startsWith('-') ? `unknown option: ${command}` : `unknown command: ${command}`);
|
|
183
|
+
}
|
|
184
|
+
// `<command> --help` explains and runs nothing — install-hooks --help used to install.
|
|
185
|
+
if (args.slice(1).some(a => a === '--help' || a === '-h')) { console.log(commandHelp(command)); return true; }
|
|
186
|
+
checkArgs(command, args.slice(1));
|
|
187
|
+
const run = COMMANDS[command].run;
|
|
188
|
+
if (run) {
|
|
189
|
+
Promise.resolve().then(() => run(args.slice(1))).catch(fail);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Every install command also brings the hook copies CRBRO installed earlier up
|
|
197
|
+
* to this version when nobody edited them (hookrefresh.ts); an edited copy is
|
|
198
|
+
* named and left alone. A courtesy: it never fails the command.
|
|
199
|
+
*/
|
|
200
|
+
function refreshInstalledHooks() {
|
|
201
|
+
import('../dist/engine/hookrefresh.js').then(hr => {
|
|
202
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
203
|
+
for (const c of hr.refreshHooks({ home: homedir(), env: process.env, pkgHooks: join(here, '..', 'hooks') })) {
|
|
204
|
+
const file = join(c.dir, c.name);
|
|
205
|
+
if (c.refreshed) console.log(` ✅ ${file} refreshed to this version (it was an unmodified older copy).`);
|
|
206
|
+
else if (c.error) console.log(` ⚠️ ${file} could not be refreshed: ${c.error}`);
|
|
207
|
+
else if (c.state === 'modified') console.log(` ⚠️ ${file} was edited by hand: left as it is.`);
|
|
208
|
+
}
|
|
209
|
+
}).catch(() => { /* never fails the command */ });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** What install, uninstall and install-boot need: the installer modules and the context they run in. */
|
|
213
|
+
async function installContext(extra = {}) {
|
|
214
|
+
const [inst, io, { BOOT_NOTICE }] = await Promise.all([
|
|
215
|
+
import('../dist/engine/installer.js'), import('../dist/engine/installio.js'), import('../hooks/crbro-lifecycle.mjs'),
|
|
216
|
+
]);
|
|
217
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
218
|
+
const ctx = {
|
|
219
|
+
home: homedir(), env: process.env, platform: platform(), dryRun: false,
|
|
220
|
+
packageRoot: join(here, '..'), nodePath: process.execPath, bootNotice: BOOT_NOTICE,
|
|
221
|
+
claude: inst.claudeCli(process.env, platform()), ...extra,
|
|
222
|
+
};
|
|
223
|
+
return { ctx, inst, io };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The value of a flag given as `--x v` or `--x=v`; undefined when absent. */
|
|
227
|
+
function flagValue(name) {
|
|
228
|
+
for (let i = 1; i < args.length; i++) {
|
|
229
|
+
if (args[i] === name) return args[i + 1];
|
|
230
|
+
if (args[i].startsWith(name + '=')) return args[i].slice(name.length + 1);
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
39
235
|
// ─── IDE Detection ──────────────────────────────────────────────
|
|
40
236
|
const IDE_CONFIGS = [
|
|
41
237
|
{
|
|
@@ -139,14 +335,14 @@ async function semanticInstall(sem) {
|
|
|
139
335
|
}
|
|
140
336
|
if (!sem.resolveRuntime()) {
|
|
141
337
|
console.log(` ⬇️ Installing transformers.js into ${home} (~380 MB with onnxruntime)...`);
|
|
142
|
-
const r = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error', '
|
|
338
|
+
const r = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error', '--save-exact', sem.SEMANTIC_RUNTIME],
|
|
143
339
|
{ cwd: home, stdio: 'inherit', shell: true });
|
|
144
340
|
if (r.status !== 0) return false;
|
|
145
341
|
}
|
|
146
342
|
const st = sem.semanticStatus();
|
|
147
343
|
if (!st.model_downloaded) {
|
|
148
344
|
console.log(` ⬇️ Downloading the model (${st.model}, ~118 MB)...`);
|
|
149
|
-
const idx = new sem.SemanticIndex(fs.mkdtempSync(join(os.tmpdir(), 'crbro-warm-')));
|
|
345
|
+
const idx = new sem.SemanticIndex(fs.mkdtempSync(join(os.tmpdir(), 'crbro-warm-')), { allowDownload: true });
|
|
150
346
|
await idx.upsert([{ id: 'warm', text: 'hello' }]); // the first use downloads and caches it
|
|
151
347
|
}
|
|
152
348
|
return true;
|
|
@@ -168,7 +364,25 @@ async function semanticBuild() {
|
|
|
168
364
|
return { chunks, vectors: engine.semanticCount(), seconds: ((Date.now() - started) / 1000).toFixed(1) };
|
|
169
365
|
}
|
|
170
366
|
|
|
171
|
-
if (
|
|
367
|
+
if (preDispatch()) {
|
|
368
|
+
// Handled before the chain: --version, --help, an error, or a command with `run`.
|
|
369
|
+
} else if (command === 'doctor') {
|
|
370
|
+
// Observational only: never fall through to initialization or MCP startup.
|
|
371
|
+
const json = args.includes('--json');
|
|
372
|
+
try {
|
|
373
|
+
if (args.slice(1).some(arg => arg !== '--json')) throw new Error('invalid doctor option');
|
|
374
|
+
const { diagnose, formatDoctor } = await import('../dist/engine/doctor.js');
|
|
375
|
+
const report = await diagnose({ env: process.env, cwd: process.cwd() });
|
|
376
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatDoctor(report));
|
|
377
|
+
process.exitCode = report.status === 'error' ? 1 : 0;
|
|
378
|
+
} catch {
|
|
379
|
+
// Do not print exception text: configuration/JSON errors can quote secrets.
|
|
380
|
+
const failure = { schema_version: 1, read_only: true, status: 'error', error: 'doctor_unavailable_or_invalid_options' };
|
|
381
|
+
console.log(json ? JSON.stringify(failure) : 'CRBRO doctor: unavailable or invalid options; use doctor [--json].');
|
|
382
|
+
process.exitCode = 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
} else if (command === 'init') {
|
|
172
386
|
// ─── Initialize brain + IDE detection ──────────────────────────
|
|
173
387
|
import('../dist/engine/brain.js').then(async ({ Brain }) => {
|
|
174
388
|
const brain = new Brain();
|
|
@@ -226,7 +440,7 @@ if (command === 'init') {
|
|
|
226
440
|
console.log(' 2. (Optional) npx crbro-memory setup-miner');
|
|
227
441
|
console.log(' 3. Restart your IDE — CRBRO boots automatically!');
|
|
228
442
|
console.log('');
|
|
229
|
-
}).catch(
|
|
443
|
+
}).catch(fail);
|
|
230
444
|
|
|
231
445
|
} else if (command === 'status') {
|
|
232
446
|
// ─── Show brain status ─────────────────────────────────────────
|
|
@@ -248,12 +462,16 @@ if (command === 'init') {
|
|
|
248
462
|
console.log('');
|
|
249
463
|
|
|
250
464
|
// Show detected IDEs
|
|
465
|
+
// A config file is not a connection: say whether CRBRO is in it.
|
|
251
466
|
const ides = detectIDEs();
|
|
252
467
|
if (ides.length > 0) {
|
|
253
|
-
console.log(' 📡
|
|
468
|
+
console.log(' 📡 Clients found:');
|
|
254
469
|
for (const ide of ides) {
|
|
255
|
-
|
|
470
|
+
let registered = false;
|
|
471
|
+
try { registered = ide.exists && /crbro/i.test(readFileSync(ide.configPath, 'utf8')); } catch { /* unreadable */ }
|
|
472
|
+
console.log(` ${registered ? '✅' : '⚪'} ${ide.name}${registered ? ' — CRBRO registered' : ide.exists ? ' — CRBRO not registered' : ' — no config yet'}`);
|
|
256
473
|
}
|
|
474
|
+
console.log(' npx crbro-memory install registers it everywhere; npx crbro-memory doctor checks each registration.');
|
|
257
475
|
console.log('');
|
|
258
476
|
}
|
|
259
477
|
} catch {
|
|
@@ -261,8 +479,9 @@ if (command === 'init') {
|
|
|
261
479
|
console.log(' 🧠 CRBRO brain not initialized.');
|
|
262
480
|
console.log(' Run: npx crbro-memory init');
|
|
263
481
|
console.log('');
|
|
482
|
+
process.exitCode = 1;
|
|
264
483
|
}
|
|
265
|
-
}).catch(
|
|
484
|
+
}).catch(fail);
|
|
266
485
|
|
|
267
486
|
} else if (command === 'activate') {
|
|
268
487
|
// ─── Legacy command (pre-1.4.0) — CRBRO is now fully free ──────
|
|
@@ -301,10 +520,11 @@ if (command === 'init') {
|
|
|
301
520
|
for (const err of result.errors.slice(0, 5)) {
|
|
302
521
|
console.log(` ${err}`);
|
|
303
522
|
}
|
|
523
|
+
process.exitCode = 1;
|
|
304
524
|
}
|
|
305
525
|
|
|
306
526
|
console.log('');
|
|
307
|
-
}).catch(
|
|
527
|
+
}).catch(fail);
|
|
308
528
|
|
|
309
529
|
} else if (command === 'setup-miner') {
|
|
310
530
|
// ─── Setup automatic mining ────────────────────────────────────
|
|
@@ -315,8 +535,9 @@ if (command === 'init') {
|
|
|
315
535
|
|
|
316
536
|
const result = await setupScheduler();
|
|
317
537
|
console.log(result.message);
|
|
538
|
+
if (!result.success) process.exitCode = 1;
|
|
318
539
|
console.log('');
|
|
319
|
-
}).catch(
|
|
540
|
+
}).catch(fail);
|
|
320
541
|
|
|
321
542
|
} else if (command === 'miner-status') {
|
|
322
543
|
// ─── Check miner status ────────────────────────────────────────
|
|
@@ -354,7 +575,7 @@ if (command === 'init') {
|
|
|
354
575
|
console.log(' ⚠️ No IDE directories detected.');
|
|
355
576
|
}
|
|
356
577
|
console.log('');
|
|
357
|
-
}).catch(
|
|
578
|
+
}).catch(fail);
|
|
358
579
|
|
|
359
580
|
} else if (command === 'remove-miner') {
|
|
360
581
|
// ─── Remove automatic mining ───────────────────────────────────
|
|
@@ -362,8 +583,9 @@ if (command === 'init') {
|
|
|
362
583
|
const result = await removeScheduler();
|
|
363
584
|
console.log('');
|
|
364
585
|
console.log(result.success ? ` ✅ ${result.message}` : ` ❌ ${result.message}`);
|
|
586
|
+
if (!result.success) process.exitCode = 1;
|
|
365
587
|
console.log('');
|
|
366
|
-
}).catch(
|
|
588
|
+
}).catch(fail);
|
|
367
589
|
|
|
368
590
|
} else if (command === 'reindex') {
|
|
369
591
|
// ─── Rebuild the search index from the cortex ──────────────────
|
|
@@ -385,7 +607,7 @@ if (command === 'init') {
|
|
|
385
607
|
console.log(' Every fact, decision and pattern is now searchable on its own,');
|
|
386
608
|
console.log(' so a big neuron is no longer buried by short ones.');
|
|
387
609
|
console.log('');
|
|
388
|
-
}).catch(
|
|
610
|
+
}).catch(fail);
|
|
389
611
|
|
|
390
612
|
} else if (command === 'semantic') {
|
|
391
613
|
// ─── Semantic recall: status | install | build ─────────────────
|
|
@@ -408,6 +630,7 @@ if (command === 'init') {
|
|
|
408
630
|
console.log('');
|
|
409
631
|
console.log(' ❌ Runtime not installed. Run: npx crbro-memory semantic install');
|
|
410
632
|
console.log('');
|
|
633
|
+
process.exitCode = 1;
|
|
411
634
|
return;
|
|
412
635
|
}
|
|
413
636
|
console.log('');
|
|
@@ -430,7 +653,7 @@ if (command === 'init') {
|
|
|
430
653
|
console.log(` Home: ${st.home}`);
|
|
431
654
|
console.log('');
|
|
432
655
|
}
|
|
433
|
-
}).catch(
|
|
656
|
+
}).catch(fail);
|
|
434
657
|
|
|
435
658
|
} else if (command === 'eval') {
|
|
436
659
|
// ─── Measure retrieval quality against a query set ─────────────
|
|
@@ -462,6 +685,7 @@ if (command === 'init') {
|
|
|
462
685
|
console.log(' Build it from facts you already saved: take six or eight words');
|
|
463
686
|
console.log(' out of a real fact and name the neuron that holds it.');
|
|
464
687
|
console.log('');
|
|
688
|
+
process.exitCode = 1;
|
|
465
689
|
return;
|
|
466
690
|
}
|
|
467
691
|
|
|
@@ -513,7 +737,7 @@ if (command === 'init') {
|
|
|
513
737
|
if (misses.length > 25) console.log(` ... and ${misses.length - 25} more`);
|
|
514
738
|
}
|
|
515
739
|
console.log('');
|
|
516
|
-
}).catch(
|
|
740
|
+
}).catch(fail);
|
|
517
741
|
|
|
518
742
|
} else if (command === 'daemon') {
|
|
519
743
|
// ─── One process owns the brain for every client (opt-in) ──────
|
|
@@ -530,24 +754,20 @@ if (command === 'init') {
|
|
|
530
754
|
const { resolveBrainDir } = await import('../dist/engine/brain.js');
|
|
531
755
|
const ep = await import('../dist/daemon/endpoint.js');
|
|
532
756
|
const { controlDaemon } = await import('../dist/daemon/proxy.js');
|
|
533
|
-
const fs = await import('fs');
|
|
534
757
|
const root = resolveBrainDir();
|
|
535
758
|
const sub = args[1] || 'status';
|
|
536
759
|
|
|
760
|
+
let cleaned = 0;
|
|
537
761
|
const running = async () => {
|
|
762
|
+
// Each descriptor is asked at its own endpoint, with its own token — not
|
|
763
|
+
// by pid: Windows hands pids out again, and a descriptor whose pid had
|
|
764
|
+
// become a cmd.exe was listed as alive for ever. Nobody there: the file
|
|
765
|
+
// goes (with stale temporaries and locks). No verdict in time (a daemon
|
|
766
|
+
// busy rebuilding): it stays, and is listed as not answering.
|
|
767
|
+
const swept = await ep.pruneDaemonDir(root);
|
|
768
|
+
cleaned += swept.removed.length;
|
|
538
769
|
const out = [];
|
|
539
|
-
for (const st of
|
|
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
|
-
}
|
|
770
|
+
for (const st of [...swept.alive, ...swept.unknown]) out.push({ st, reply: await controlDaemon(root, 'status', st.build) });
|
|
551
771
|
return out;
|
|
552
772
|
};
|
|
553
773
|
|
|
@@ -582,6 +802,7 @@ if (command === 'init') {
|
|
|
582
802
|
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
803
|
}
|
|
584
804
|
if (list.length > 1) console.log(' ⚠️ More than one build is serving this brain. Point every client at the same install to share one.');
|
|
805
|
+
if (cleaned > 0) console.log(` Cleaned: ${cleaned} file(s) left in .daemon by daemons that are gone`);
|
|
585
806
|
console.log(` Log: ${root}${root.includes('\\') ? '\\' : '/'}.daemon${root.includes('\\') ? '\\' : '/'}daemon.log`);
|
|
586
807
|
console.log('');
|
|
587
808
|
}
|
|
@@ -609,19 +830,22 @@ if (command === 'init') {
|
|
|
609
830
|
}
|
|
610
831
|
const texto = process.argv.slice(3).filter(a => !a.startsWith('--')).join(' ').trim();
|
|
611
832
|
if (!texto) {
|
|
612
|
-
if (!process.argv.includes('--rebuild')) console.log(' Usage: crbro guard "<command line>" | crbro guard --rebuild');
|
|
833
|
+
if (!process.argv.includes('--rebuild')) console.log(' Usage: npx crbro-memory guard "<command line>" | npx crbro-memory guard --rebuild');
|
|
613
834
|
return;
|
|
614
835
|
}
|
|
615
836
|
let index;
|
|
616
837
|
try {
|
|
617
838
|
index = JSON.parse(fs.readFileSync(triggerIndexPath(brain), 'utf8'));
|
|
618
839
|
} catch {
|
|
619
|
-
console.log(' No trigger index yet. Run: crbro guard --rebuild (crbro_consolidate writes it too).');
|
|
840
|
+
console.log(' No trigger index yet. Run: npx crbro-memory guard --rebuild (crbro_consolidate writes it too).');
|
|
841
|
+
process.exitCode = 1;
|
|
620
842
|
return;
|
|
621
843
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
844
|
+
// As the hook would answer here: this folder's project, lessons of other origins only with CRBRO_GUARD_ORIGINS=all.
|
|
845
|
+
const allOrigins = String(process.env.CRBRO_GUARD_ORIGINS || '').toLowerCase() === 'all';
|
|
846
|
+
const lessons = lessonsFor(index, texto, 10, { cwd: process.cwd(), allOrigins });
|
|
847
|
+
if (lessons.length === 0) { console.log(' Nothing stored mentions that command (for this folder\'s project).'); return; }
|
|
848
|
+
for (const l of lessons) console.log(` • [${l.o ? `from ${l.o} · ` : ''}${l.k}${l.d ? ' · ' + l.d : ''} · ${l.n}] ${l.t}\n`);
|
|
625
849
|
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
626
850
|
|
|
627
851
|
} else if (command === 'usage' || command === 'postmortem') {
|
|
@@ -671,20 +895,29 @@ if (command === 'init') {
|
|
|
671
895
|
// any difference, so it can gate a script.
|
|
672
896
|
// The Claude Code mod is checked too (install-mod): a copy in the home
|
|
673
897
|
// folder that runs in every session, so the same question applies.
|
|
674
|
-
|
|
898
|
+
// Codex's copy (<CODEX_HOME>/crbro-hooks, run by hooks.json) is checked the same way when it exists.
|
|
899
|
+
Promise.all([import('../dist/engine/hookverify.js'), import('../dist/engine/modinstall.js'), import('../dist/engine/installio.js')]).then(([
|
|
675
900
|
{ verifyHooks, formatVerify },
|
|
676
901
|
{ verifyMod, formatModVerify },
|
|
902
|
+
io,
|
|
677
903
|
]) => {
|
|
678
904
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
905
|
+
const claudeHome = io.claudeDir(homedir(), process.env);
|
|
679
906
|
const r = verifyHooks({
|
|
680
907
|
packageDir: join(here, '..', 'hooks'),
|
|
681
|
-
installedDir: join(
|
|
682
|
-
settingsPath: join(
|
|
908
|
+
installedDir: join(claudeHome, 'crbro-hooks'),
|
|
909
|
+
settingsPath: join(claudeHome, 'settings.json'),
|
|
683
910
|
});
|
|
911
|
+
const codexHome = io.codexHome(homedir(), process.env);
|
|
912
|
+
const codex = existsSync(join(codexHome, 'crbro-hooks')) ? verifyHooks({
|
|
913
|
+
packageDir: join(here, '..', 'hooks'),
|
|
914
|
+
installedDir: join(codexHome, 'crbro-hooks'),
|
|
915
|
+
settingsPath: join(codexHome, 'hooks.json'),
|
|
916
|
+
}) : null;
|
|
684
917
|
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;
|
|
918
|
+
if (args.includes('--json')) console.log(JSON.stringify({ ...r, mod: m, ...(codex ? { codex } : {}) }, null, 2));
|
|
919
|
+
else console.log(formatVerify(r) + (codex ? formatVerify(codex) : '') + formatModVerify(m));
|
|
920
|
+
if (!r.ok || !m.ok || (codex && !codex.ok)) process.exitCode = 1;
|
|
688
921
|
}).catch(err => {
|
|
689
922
|
console.error(' ❌ ' + (err && err.code === 'ERR_MODULE_NOT_FOUND' ? 'Build required. Run: npm run build' : err.message));
|
|
690
923
|
process.exit(1);
|
|
@@ -706,27 +939,21 @@ if (command === 'init') {
|
|
|
706
939
|
// crbro-hooks/ so uninstall puts it back exactly. A hand-written CRBRO hook
|
|
707
940
|
// is never replaced: ours is installed with --no-boot / --no-reminder next
|
|
708
941
|
// to it, so nothing is read twice. Merged, never rewritten; idempotent.
|
|
709
|
-
import('fs').then(async fs => {
|
|
710
|
-
const
|
|
711
|
-
const
|
|
942
|
+
Promise.all([import('fs'), import('../dist/engine/installio.js'), import('../dist/engine/hookrefresh.js')]).then(async ([fs, io, hr]) => {
|
|
943
|
+
const claudeHome = io.claudeDir(homedir(), process.env);
|
|
944
|
+
const settingsPath = join(claudeHome, 'settings.json');
|
|
945
|
+
const hookDir = join(claudeHome, 'crbro-hooks');
|
|
712
946
|
const hookScript = join(hookDir, 'crbro-lifecycle.mjs');
|
|
713
947
|
const replacedPath = join(hookDir, 'crbro-lifecycle.replaced.json');
|
|
714
948
|
const ours = h => !!h && typeof h.command === 'string' && h.command.includes('crbro-lifecycle');
|
|
715
949
|
const entryIsOurs = e => Array.isArray(e?.hooks) && e.hooks.some(ours);
|
|
716
950
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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.`);
|
|
951
|
+
// A settings.json that is not a JSON object is never touched.
|
|
952
|
+
const read = io.readJsonObject(settingsPath);
|
|
953
|
+
if (!read.ok) { console.error(` ❌ ${read.error}`); process.exit(1); }
|
|
954
|
+
let settings = read.value;
|
|
955
|
+
if (settings?.hooks !== undefined && (typeof settings.hooks !== 'object' || settings.hooks === null || Array.isArray(settings.hooks))) {
|
|
956
|
+
console.error(` ❌ hooks in ${settingsPath} is not an object — not touching it.`);
|
|
730
957
|
process.exit(1);
|
|
731
958
|
}
|
|
732
959
|
for (const event of ['PreCompact', 'SessionStart']) {
|
|
@@ -736,10 +963,10 @@ if (command === 'init') {
|
|
|
736
963
|
process.exit(1);
|
|
737
964
|
}
|
|
738
965
|
}
|
|
966
|
+
// A backup first; refused if Claude Code or an editor saved it since it was read.
|
|
739
967
|
const write = obj => {
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
fs.renameSync(tmp, settingsPath);
|
|
968
|
+
const backup = io.writeGuarded(settingsPath, io.jsonText(obj, read.indent, read.eol, read.bom), { expectHash: read.hash });
|
|
969
|
+
if (backup) console.log(` backup: ${backup}`);
|
|
743
970
|
};
|
|
744
971
|
|
|
745
972
|
if (command === 'uninstall-hooks') {
|
|
@@ -785,13 +1012,15 @@ if (command === 'init') {
|
|
|
785
1012
|
}
|
|
786
1013
|
|
|
787
1014
|
// — install —
|
|
788
|
-
if (settings === null && !fs.existsSync(
|
|
789
|
-
console.log(
|
|
1015
|
+
if (settings === null && !fs.existsSync(claudeHome)) {
|
|
1016
|
+
console.log(` ⚪ ${claudeHome} not found: Claude Code is the only client with PreCompact, so nothing was wired.`);
|
|
790
1017
|
return;
|
|
791
1018
|
}
|
|
792
1019
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
793
|
-
|
|
794
|
-
|
|
1020
|
+
// Copied with a manifest entry, so later releases refresh it on their own while it is unmodified.
|
|
1021
|
+
const source = join(here, '..', 'hooks', 'crbro-lifecycle.mjs');
|
|
1022
|
+
hr.installHookFile(dirname(source), basename(source), hookDir);
|
|
1023
|
+
refreshInstalledHooks(); // the other copies CRBRO installed, when unmodified
|
|
795
1024
|
settings = settings || {};
|
|
796
1025
|
settings.hooks = settings.hooks || {};
|
|
797
1026
|
const node = `node "${hookScript.split('\\').join('/')}"`;
|
|
@@ -841,11 +1070,12 @@ if (command === 'init') {
|
|
|
841
1070
|
console.log(' Undo with: npx crbro-memory uninstall-hooks --compact');
|
|
842
1071
|
}
|
|
843
1072
|
console.log(' Never blocks; checkpoints are redacted and expire after 7 days. New sessions pick it up.');
|
|
844
|
-
}).catch(
|
|
1073
|
+
}).catch(fail);
|
|
845
1074
|
|
|
846
1075
|
} else if (command === 'uninstall-hooks') {
|
|
847
1076
|
console.log(' Usage: npx crbro-memory uninstall-hooks --compact');
|
|
848
|
-
console.log(' (the guard and subagent hooks are removed by deleting their entry from
|
|
1077
|
+
console.log(' (the guard and subagent hooks are removed by deleting their entry from settings.json, or all of CRBRO with: npx crbro-memory uninstall)');
|
|
1078
|
+
process.exitCode = 2;
|
|
849
1079
|
|
|
850
1080
|
} else if (command === 'install-hooks' && process.argv.includes('--guard')) {
|
|
851
1081
|
// ─── Wire the PreToolUse guard into Claude Code (opt-in) ───────
|
|
@@ -855,46 +1085,44 @@ if (command === 'init') {
|
|
|
855
1085
|
// that mention it to the model's context — once per session each, three at
|
|
856
1086
|
// most. It never blocks and never asks. Remove the PreToolUse entry from
|
|
857
1087
|
// ~/.claude/settings.json to uninstall.
|
|
858
|
-
import('
|
|
859
|
-
const
|
|
1088
|
+
Promise.all([import('../dist/engine/installio.js'), import('../dist/engine/hookrefresh.js')]).then(async ([io, hr]) => {
|
|
1089
|
+
const claudeHome = io.claudeDir(homedir(), process.env);
|
|
1090
|
+
const settingsPath = join(claudeHome, 'settings.json');
|
|
860
1091
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
861
|
-
const hookDir = join(
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
let
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
settings =
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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 {
|
|
1092
|
+
const hookDir = join(claudeHome, 'crbro-hooks');
|
|
1093
|
+
const source = join(here, '..', 'hooks', 'crbro-guard.mjs');
|
|
1094
|
+
const hookScript = hr.installHookFile(dirname(source), basename(source), hookDir);
|
|
1095
|
+
refreshInstalledHooks(); // the other copies CRBRO installed, when unmodified
|
|
1096
|
+
|
|
1097
|
+
let installed = false;
|
|
1098
|
+
const r0 = io.updateJsonFile(settingsPath, settings => {
|
|
1099
|
+
if (settings.hooks !== undefined && (typeof settings.hooks !== 'object' || settings.hooks === null || Array.isArray(settings.hooks))) throw new Error(`hooks in ${settingsPath} is not an object — not touching it.`);
|
|
1100
|
+
settings.hooks = settings.hooks || {};
|
|
1101
|
+
if (settings.hooks.PreToolUse !== undefined && !Array.isArray(settings.hooks.PreToolUse)) throw new Error(`hooks.PreToolUse in ${settingsPath} is not a list — not touching it.`);
|
|
1102
|
+
const list = settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
|
|
1103
|
+
if (JSON.stringify(list).includes('crbro-guard')) return false;
|
|
882
1104
|
list.push({
|
|
883
1105
|
matcher: 'Bash|PowerShell',
|
|
884
1106
|
hooks: [{ type: 'command', command: `node "${hookScript.split('\\').join('/')}"`, timeout: 5 }],
|
|
885
1107
|
});
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1108
|
+
installed = true;
|
|
1109
|
+
return true;
|
|
1110
|
+
});
|
|
1111
|
+
if (!r0.ok) { console.error(` ❌ ${r0.error}`); process.exit(1); }
|
|
1112
|
+
if (!installed) {
|
|
1113
|
+
console.log(' ✅ PreToolUse guard already installed. Script refreshed.');
|
|
1114
|
+
} else {
|
|
889
1115
|
console.log(' ✅ PreToolUse guard installed.');
|
|
890
1116
|
console.log(` ${settingsPath}`);
|
|
1117
|
+
if (r0.backup) console.log(` backup: ${r0.backup}`);
|
|
891
1118
|
}
|
|
892
1119
|
const { Brain } = await import('../dist/engine/brain.js');
|
|
893
1120
|
const { writeTriggerIndex } = await import('../dist/engine/triggers.js');
|
|
894
1121
|
const r = await writeTriggerIndex(new Brain());
|
|
895
|
-
if (
|
|
1122
|
+
if ('error' in r) { console.log(` ⚠️ Trigger index not written: ${r.error}`); process.exitCode = 1; }
|
|
1123
|
+
else console.log(` Trigger index: ${r.entries} lessons under ${r.keys} commands. Try: npx crbro-memory guard "git push"`);
|
|
896
1124
|
console.log(' It adds context, never blocks. New sessions pick it up.');
|
|
897
|
-
}).catch(
|
|
1125
|
+
}).catch(fail);
|
|
898
1126
|
|
|
899
1127
|
} else if (command === 'install-hooks') {
|
|
900
1128
|
// ─── Wire the SubagentStart hook into Claude Code ──────────────
|
|
@@ -906,18 +1134,20 @@ if (command === 'init') {
|
|
|
906
1134
|
//
|
|
907
1135
|
// Merges into ~/.claude/settings.json without touching anything else.
|
|
908
1136
|
// Idempotent: running it twice changes nothing the second time.
|
|
909
|
-
import('
|
|
910
|
-
const
|
|
1137
|
+
Promise.all([import('../dist/engine/installio.js'), import('../dist/engine/hookrefresh.js')]).then(async ([io, hr]) => {
|
|
1138
|
+
const claudeHome = io.claudeDir(homedir(), process.env);
|
|
1139
|
+
const settingsPath = join(claudeHome, 'settings.json');
|
|
911
1140
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
912
1141
|
const source = join(here, '..', 'hooks', 'crbro-subagent.mjs');
|
|
913
1142
|
|
|
914
1143
|
// Copy the hook to a stable location. When CRBRO runs from the npx
|
|
915
1144
|
// cache, `here` changes with every release and the stale path would
|
|
916
1145
|
// break the hook silently on the next update.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1146
|
+
// installHookFile records it in the folder's manifest: an unmodified copy
|
|
1147
|
+
// is refreshed on its own by later releases (hookrefresh.ts).
|
|
1148
|
+
const hookDir = join(claudeHome, 'crbro-hooks');
|
|
1149
|
+
const hookScript = hr.installHookFile(dirname(source), basename(source), hookDir);
|
|
1150
|
+
refreshInstalledHooks(); // the other copies CRBRO installed, when unmodified
|
|
921
1151
|
// Injection is OPT-IN since 1.12 (pre-registered consequence of three
|
|
922
1152
|
// clean-control benchmark runs: no measured benefit in any model, harm
|
|
923
1153
|
// and fabricated compliance in small ones — see the benchmarks in the
|
|
@@ -926,43 +1156,38 @@ if (command === 'init') {
|
|
|
926
1156
|
const conInyeccion = process.argv.includes('--inject');
|
|
927
1157
|
const hookCmd = (conInyeccion ? 'CRBRO_SUBAGENT_INJECT=full ' : '') + `node "${hookScript.split('\\').join('/')}"`;
|
|
928
1158
|
|
|
929
|
-
let
|
|
930
|
-
|
|
931
|
-
settings
|
|
932
|
-
|
|
933
|
-
if (
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1159
|
+
let yaEsta = false, injectsAlready = false;
|
|
1160
|
+
const r0 = io.updateJsonFile(settingsPath, settings => {
|
|
1161
|
+
if (settings.hooks !== undefined && (typeof settings.hooks !== 'object' || settings.hooks === null || Array.isArray(settings.hooks))) throw new Error(`hooks in ${settingsPath} is not an object — not touching it.`);
|
|
1162
|
+
settings.hooks = settings.hooks || {};
|
|
1163
|
+
if (settings.hooks.SubagentStart !== undefined && !Array.isArray(settings.hooks.SubagentStart)) throw new Error(`hooks.SubagentStart in ${settingsPath} is not a list — not touching it.`);
|
|
1164
|
+
const list = settings.hooks.SubagentStart = settings.hooks.SubagentStart || [];
|
|
1165
|
+
yaEsta = JSON.stringify(list).includes('crbro-subagent');
|
|
1166
|
+
injectsAlready = JSON.stringify(list).includes('CRBRO_SUBAGENT_INJECT');
|
|
1167
|
+
if (yaEsta) return false;
|
|
1168
|
+
list.push({
|
|
1169
|
+
hooks: [{
|
|
1170
|
+
type: 'command',
|
|
1171
|
+
command: hookCmd,
|
|
1172
|
+
timeout: 5,
|
|
1173
|
+
statusMessage: 'Inyectando protocolos CRBRO en el subagente...',
|
|
1174
|
+
}],
|
|
1175
|
+
});
|
|
1176
|
+
return true;
|
|
1177
|
+
});
|
|
1178
|
+
if (!r0.ok) { console.error(` ❌ ${r0.error}`); process.exit(1); }
|
|
943
1179
|
if (yaEsta) {
|
|
944
1180
|
console.log(' ✅ SubagentStart hook already installed. Script refreshed.');
|
|
945
|
-
if (conInyeccion && !
|
|
1181
|
+
if (conInyeccion && !injectsAlready) {
|
|
946
1182
|
console.log(' ⚠️ The installed entry does NOT enable injection. To enable it,');
|
|
947
1183
|
console.log(' remove the SubagentStart entry from settings.json and re-run');
|
|
948
1184
|
console.log(' install-hooks --inject.');
|
|
949
1185
|
}
|
|
950
1186
|
return;
|
|
951
1187
|
}
|
|
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
1188
|
console.log(' ✅ SubagentStart hook installed.');
|
|
965
1189
|
console.log(` ${settingsPath}`);
|
|
1190
|
+
if (r0.backup) console.log(` backup: ${r0.backup}`);
|
|
966
1191
|
if (conInyeccion) {
|
|
967
1192
|
console.log(' Injection ENABLED: every Task-spawned subagent receives the');
|
|
968
1193
|
console.log(' protocol block. Scope it with CRBRO_SUBAGENT_MATCHER (regex on');
|
|
@@ -974,7 +1199,7 @@ if (command === 'init') {
|
|
|
974
1199
|
console.log(' three clean benchmark runs found no benefit in any model and');
|
|
975
1200
|
console.log(' harm in small ones). Re-run with --inject to enable it.');
|
|
976
1201
|
}
|
|
977
|
-
}).catch(
|
|
1202
|
+
}).catch(fail);
|
|
978
1203
|
|
|
979
1204
|
} else if (command === 'install-mod' && process.argv.includes('--verify')) {
|
|
980
1205
|
// ─── Is the installed mod the one this package ships? ──────────
|
|
@@ -1045,120 +1270,105 @@ if (command === 'init') {
|
|
|
1045
1270
|
// like it was never there. Every report of "CRBRO does not remember" so far
|
|
1046
1271
|
// has been this, not a bug in the recall.
|
|
1047
1272
|
//
|
|
1048
|
-
// So this wires the start itself, per client:
|
|
1273
|
+
// So this wires the start itself, per client (src/engine/installer.ts):
|
|
1049
1274
|
//
|
|
1050
|
-
// Claude Code
|
|
1275
|
+
// Claude Code <config>/settings.json — SessionStart runs a command whose
|
|
1051
1276
|
// stdout is added to the session, telling the model to call
|
|
1052
1277
|
// crbro_boot first. Claude Code has no way to invoke an MCP
|
|
1053
1278
|
// tool from a hook, so the instruction is the mechanism.
|
|
1054
|
-
// Codex
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1279
|
+
// Codex <CODEX_HOME>/config.toml gets the MCP server if it has none,
|
|
1280
|
+
// and startup/tool timeouts when they are missing (a value
|
|
1281
|
+
// the user set is kept). hooks/crbro-codex.mjs is copied to
|
|
1282
|
+
// <CODEX_HOME>/crbro-hooks and wired on SessionStart and
|
|
1283
|
+
// UserPromptSubmit: it boots the engine itself and hands the
|
|
1284
|
+
// protocols to the session, or prints the notice.
|
|
1059
1285
|
//
|
|
1060
|
-
// Only
|
|
1061
|
-
// rewritten
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1286
|
+
// Only clients whose folder exists are touched; every file is merged, never
|
|
1287
|
+
// rewritten, after a timestamped backup. Idempotent. --dry-run writes nothing.
|
|
1288
|
+
(async () => {
|
|
1289
|
+
const { ctx, inst, io } = await installContext({ dryRun: args.includes('--dry-run') });
|
|
1290
|
+
const root = io.engineRoot(homedir(), process.env);
|
|
1291
|
+
const stable = existsSync(join(root, 'current.json'));
|
|
1292
|
+
// With a stable engine (crbro-memory install) Codex runs it; otherwise the npx form, as before.
|
|
1293
|
+
const wiring = stable
|
|
1294
|
+
? { command: process.execPath, args: [join(root, 'crbro.mjs')], replaceCommand: false, engineArg: root }
|
|
1295
|
+
: { command: 'npx', args: ['-y', 'crbro-memory'], replaceCommand: false };
|
|
1296
|
+
const steps = inst.wireBoot(ctx, ['claude-code', 'codex'], wiring);
|
|
1297
|
+
if (ctx.dryRun) console.log(' Dry run: nothing is written.\n');
|
|
1298
|
+
for (const line of inst.formatSteps(steps)) console.log(line);
|
|
1299
|
+
if (!ctx.dryRun) refreshInstalledHooks();
|
|
1147
1300
|
console.log('');
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
1151
|
-
for (const a of ausentes) console.log(` ⚪ Skipped ${a}`);
|
|
1301
|
+
const wired = steps.filter(s => s.status !== 'skipped');
|
|
1302
|
+
if (!wired.length) console.log(` ⚠️ Neither ${io.claudeDir(homedir(), process.env)} nor ${io.codexHome(homedir(), process.env)} was found, so nothing was wired.`);
|
|
1152
1303
|
console.log('');
|
|
1153
1304
|
console.log(' Other tools (Cursor, Windsurf, Antigravity…) have no session hooks.');
|
|
1154
1305
|
console.log(' Put this line in their always-on rules file (.cursorrules, .windsurfrules,');
|
|
1155
1306
|
console.log(' User Rules) and it does the same job:');
|
|
1156
1307
|
console.log('');
|
|
1157
|
-
console.log(` ${
|
|
1308
|
+
console.log(` ${ctx.bootNotice}`);
|
|
1158
1309
|
console.log('');
|
|
1159
|
-
if (
|
|
1310
|
+
if (steps.some(s => s.status === 'done')) console.log(' Restart the tool, then check a new conversation actually boots CRBRO.');
|
|
1311
|
+
if (!stable && steps.some(s => s.client === 'Codex' && s.status !== 'skipped')) {
|
|
1312
|
+
console.log(' For a stable engine that clients start without npx: npx crbro-memory install');
|
|
1313
|
+
}
|
|
1160
1314
|
console.log('');
|
|
1161
|
-
|
|
1315
|
+
if (inst.failed(steps)) process.exitCode = 1;
|
|
1316
|
+
})().catch(fail);
|
|
1317
|
+
|
|
1318
|
+
} else if (command === 'install' || command === 'uninstall') {
|
|
1319
|
+
// ─── One command in, one command out ───────────────────────────
|
|
1320
|
+
//
|
|
1321
|
+
// install: the engine copied to a stable folder (~/.crbro-engine, or
|
|
1322
|
+
// --engine-dir), registered in each client as `node <launcher>`, the session
|
|
1323
|
+
// start wired, installed hooks brought up to date. uninstall: the reverse,
|
|
1324
|
+
// only what CRBRO added; backups kept; the brain is never touched.
|
|
1325
|
+
// src/engine/installer.ts does the work; this prints it.
|
|
1326
|
+
(async () => {
|
|
1327
|
+
const { ctx, inst } = await installContext({ dryRun: args.includes('--dry-run'), engineDir: flagValue('--engine-dir') });
|
|
1328
|
+
const spec = flagValue('--clients');
|
|
1329
|
+
let clients;
|
|
1330
|
+
try {
|
|
1331
|
+
clients = inst.parseClients(spec, () => inst.detectClients(ctx.home, ctx.env, ctx.platform, ctx.claude));
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
usageError(e.message);
|
|
1334
|
+
}
|
|
1335
|
+
const json = args.includes('--json');
|
|
1336
|
+
let report, brain = null;
|
|
1337
|
+
if (command === 'install') {
|
|
1338
|
+
report = inst.install(ctx, clients);
|
|
1339
|
+
} else {
|
|
1340
|
+
const [{ resolveBrainDir }, { uninstallMod }] = await Promise.all([import('../dist/engine/brain.js'), import('../dist/engine/modinstall.js')]);
|
|
1341
|
+
brain = resolveBrainDir(process.env, () => {});
|
|
1342
|
+
report = inst.uninstall(ctx, clients, {
|
|
1343
|
+
brainDir: brain, removeEngine: !spec || spec === 'auto',
|
|
1344
|
+
uninstallMod: home => uninstallMod({ home }),
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
if (json) {
|
|
1348
|
+
console.log(JSON.stringify({ command, dry_run: ctx.dryRun, clients, ...report }, null, 2));
|
|
1349
|
+
} else {
|
|
1350
|
+
const names = clients.map(c => inst.CLIENT_NAMES[c]).join(', ') || 'none found';
|
|
1351
|
+
console.log('');
|
|
1352
|
+
console.log(` 🧠 CRBRO ${command}${ctx.dryRun ? ' — dry run: nothing is written' : ''} · clients: ${names}`);
|
|
1353
|
+
console.log('');
|
|
1354
|
+
for (const line of inst.formatSteps(report.steps)) console.log(line);
|
|
1355
|
+
console.log('');
|
|
1356
|
+
if (command === 'install') {
|
|
1357
|
+
if (report.engine) console.log(` Clients start: ${process.execPath} ${report.engine.launcher}`);
|
|
1358
|
+
console.log(' Restart each client; a new conversation boots CRBRO on its own (Claude Code, Codex) or');
|
|
1359
|
+
console.log(' with this line in the tool\'s always-on rules (Cursor, Claude Desktop, others):');
|
|
1360
|
+
console.log(` ${ctx.bootNotice}`);
|
|
1361
|
+
console.log(' Undo everything with: npx crbro-memory uninstall');
|
|
1362
|
+
} else {
|
|
1363
|
+
console.log(` Your brain was not touched: ${brain}`);
|
|
1364
|
+
console.log(' Delete that folder yourself if you want the memory gone too. Credentials stay in the OS');
|
|
1365
|
+
console.log(' keychain (npx crbro-memory secret list); the auto-miner, if you set it up, goes with');
|
|
1366
|
+
console.log(' npx crbro-memory remove-miner. Servers already running stop when their client restarts.');
|
|
1367
|
+
}
|
|
1368
|
+
console.log('');
|
|
1369
|
+
}
|
|
1370
|
+
if (inst.failed(report.steps)) process.exitCode = 1;
|
|
1371
|
+
})().catch(fail);
|
|
1162
1372
|
|
|
1163
1373
|
} else if (command === 'secret') {
|
|
1164
1374
|
// ─── Credentials, from the terminal ────────────────────────────
|
|
@@ -1249,8 +1459,20 @@ if (command === 'init') {
|
|
|
1249
1459
|
return;
|
|
1250
1460
|
}
|
|
1251
1461
|
|
|
1462
|
+
if (sub === 'copies') {
|
|
1463
|
+
// Windows: sealed byte copies kept on every write, plus leftovers. Never opened here.
|
|
1464
|
+
const copies = kc.listSealedCopies();
|
|
1465
|
+
console.log('');
|
|
1466
|
+
if (copies.length === 0) { console.log(' No sealed copies or leftovers next to the store.'); console.log(''); return; }
|
|
1467
|
+
for (const c of copies) console.log(` ${c.id.padEnd(52)} ${c.kind.padEnd(14)} ${c.modified} ${c.bytes} B`);
|
|
1468
|
+
console.log('');
|
|
1469
|
+
console.log(' Put one back (validated with DPAPI first): npx crbro-memory secret restore <id> --yes');
|
|
1470
|
+
console.log('');
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1252
1474
|
if (!name) {
|
|
1253
|
-
console.error(` ❌ Missing name. Usage: npx crbro-memory secret ${sub || '<set|get|list|remove|status>'} NAME`);
|
|
1475
|
+
console.error(` ❌ Missing name. Usage: npx crbro-memory secret ${sub || '<set|get|list|remove|status|copies|restore>'} NAME`);
|
|
1254
1476
|
process.exit(1);
|
|
1255
1477
|
}
|
|
1256
1478
|
|
|
@@ -1262,7 +1484,7 @@ if (command === 'init') {
|
|
|
1262
1484
|
console.error(' ❌ Empty value. Nothing was stored.');
|
|
1263
1485
|
process.exit(1);
|
|
1264
1486
|
}
|
|
1265
|
-
kc.setSecret(name, value, flagValue('--description'));
|
|
1487
|
+
kc.setSecret(name, value, flagValue('--description'), { force: args.includes('--force') });
|
|
1266
1488
|
console.log(` ✅ ${name} sealed in the OS keychain. CRBRO keeps no copy.`);
|
|
1267
1489
|
console.log(' Record only the NAME in the brain, never the value.');
|
|
1268
1490
|
return;
|
|
@@ -1292,7 +1514,19 @@ if (command === 'init') {
|
|
|
1292
1514
|
return;
|
|
1293
1515
|
}
|
|
1294
1516
|
|
|
1295
|
-
|
|
1517
|
+
if (sub === 'restore') {
|
|
1518
|
+
// `name` is the copy id from `secret copies`.
|
|
1519
|
+
if (!args.includes('--yes')) {
|
|
1520
|
+
console.error(` ❌ This replaces the current store with ${name} (the current one is kept as a sealed copy).`);
|
|
1521
|
+
console.error(` Re-run to confirm: npx crbro-memory secret restore ${name} --yes`);
|
|
1522
|
+
process.exit(1);
|
|
1523
|
+
}
|
|
1524
|
+
const r = kc.restoreSealedCopy(name);
|
|
1525
|
+
console.log(` ✅ Restored ${r.restored}: ${r.entries} credential(s). The previous store, if any, is kept as a sealed copy.`);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
console.error(' Usage: npx crbro-memory secret <set|get|list|remove|status|copies|restore> [NAME]');
|
|
1296
1530
|
process.exit(1);
|
|
1297
1531
|
} catch (err) {
|
|
1298
1532
|
console.error(` ❌ ${err instanceof Error ? err.message : err}`);
|
|
@@ -1314,7 +1548,8 @@ if (command === 'init') {
|
|
|
1314
1548
|
const kb = (n) => `${(n / 1024).toFixed(0)} KB`;
|
|
1315
1549
|
try {
|
|
1316
1550
|
if (sub === 'create') {
|
|
1317
|
-
|
|
1551
|
+
// --keep present means a whole number ≥ 1; anything else ("siete", a missing value) is refused, never "keep none".
|
|
1552
|
+
const keep = args.includes('--keep') ? bk.validateKeep(/^\d+$/.test(flag('--keep') ?? '') ? Number(flag('--keep')) : flag('--keep')) : undefined;
|
|
1318
1553
|
const r = await bk.createBackup(paths, { dir, keep });
|
|
1319
1554
|
console.log('');
|
|
1320
1555
|
console.log(` ✅ Backup written: ${r.path}`);
|
|
@@ -1364,74 +1599,95 @@ if (command === 'init') {
|
|
|
1364
1599
|
process.exit(1);
|
|
1365
1600
|
});
|
|
1366
1601
|
|
|
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
1602
|
} else {
|
|
1435
1603
|
// ─── Default: start MCP server ─────────────────────────────────
|
|
1436
|
-
|
|
1604
|
+
// Only with no arguments, or --daemon / --proxy / --stdio: anything else
|
|
1605
|
+
// was checked by preDispatch() and rejected.
|
|
1606
|
+
import('../dist/index.js').catch(fail);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// ─── Help ────────────────────────────────────────────────────────
|
|
1610
|
+
function printHelp() {
|
|
1611
|
+
const L = [
|
|
1612
|
+
'',
|
|
1613
|
+
' 🧠 CRBRO — Persistent Neural Memory for AI',
|
|
1614
|
+
' ═══════════════════════════════════════════',
|
|
1615
|
+
'',
|
|
1616
|
+
' Setup:',
|
|
1617
|
+
' npx crbro-memory install Install for every MCP client found: stable engine in ~/.crbro-engine,',
|
|
1618
|
+
' registrations (Codex with timeouts), session start. --dry-run shows the plan',
|
|
1619
|
+
' npx crbro-memory uninstall Remove what install added; backups kept, the brain never touched. --dry-run',
|
|
1620
|
+
' npx crbro-memory init Initialize the brain, detect clients, install semantic recall',
|
|
1621
|
+
' npx crbro-memory install-boot Make the memory load itself in every conversation (Claude Code, Codex)',
|
|
1622
|
+
' npx crbro-memory status Show brain status',
|
|
1623
|
+
' npx crbro-memory doctor [--json] Read-only diagnostics: brain, clients, hooks, daemon leftovers, backups',
|
|
1624
|
+
'',
|
|
1625
|
+
' Auto-Mining:',
|
|
1626
|
+
' npx crbro-memory mine [dir] One-shot mining of editor artifacts',
|
|
1627
|
+
' npx crbro-memory setup-miner Install scheduled auto-miner',
|
|
1628
|
+
' npx crbro-memory miner-status Check auto-miner status',
|
|
1629
|
+
' npx crbro-memory remove-miner Remove auto-miner',
|
|
1630
|
+
'',
|
|
1631
|
+
' Backup (one gzipped file per copy; consolidate makes one a day on its own):',
|
|
1632
|
+
' npx crbro-memory backup Back the brain up now (keeps the newest 7, plus daily, weekly and monthly copies)',
|
|
1633
|
+
' npx crbro-memory backup list What there is, newest first',
|
|
1634
|
+
' npx crbro-memory backup restore FILE Unpack into a NEW folder, never over the live brain',
|
|
1635
|
+
' CRBRO_BACKUP_DIR=<synced folder> Put them somewhere that survives the disk',
|
|
1636
|
+
'',
|
|
1637
|
+
' Hooks (Claude Code; copies in <config>/crbro-hooks refresh themselves when CRBRO updates, unless edited):',
|
|
1638
|
+
' npx crbro-memory install-hooks SubagentStart hook, inert unless --inject (CRBRO_SUBAGENT_INJECT=full)',
|
|
1639
|
+
' npx crbro-memory install-hooks --guard PreToolUse guard: stored lessons for this project speak before a shell command',
|
|
1640
|
+
' npx crbro-memory guard "<command>" What it would say for a command · --rebuild writes the index',
|
|
1641
|
+
' npx crbro-memory install-hooks --compact PreCompact checkpoint + SessionStart that picks it back up',
|
|
1642
|
+
' npx crbro-memory uninstall-hooks --compact Remove both and put back what was replaced',
|
|
1643
|
+
' npx crbro-memory install-hooks --verify SHA-256 of the installed hooks and mod against this package (read-only)',
|
|
1644
|
+
' npx crbro-memory install-mod [--lang en|es|auto] Claude Code band + /pending with the open items (Claude Code 2.1.286+)',
|
|
1645
|
+
' crbro_boot installs and updates it on its own; CRBRO_MOD=0 turns that off',
|
|
1646
|
+
' npx crbro-memory uninstall-mod Remove the mod and its CLAUDE_CODE_PLUGIN_DIRS entry; never put back on its own',
|
|
1647
|
+
' npx crbro-memory install-mod --verify SHA-256 of the installed mod against this package (read-only)',
|
|
1648
|
+
'',
|
|
1649
|
+
' Looking back at Claude Code sessions (read-only; last 7 days unless --days N, 0 = all):',
|
|
1650
|
+
' npx crbro-memory usage [--session ID] [--project DIR] [--json] Tokens per model and session, subagents apart',
|
|
1651
|
+
' npx crbro-memory postmortem [--max N] [--json] Candidate lessons: corrections, failing tools, repeats',
|
|
1652
|
+
'',
|
|
1653
|
+
' Daemon (opt-in): one process owns the brain for every client — one index, one model, one writer:',
|
|
1654
|
+
' npx crbro-memory daemon on | off Switch every client of this brain, the next time each starts',
|
|
1655
|
+
' npx crbro-memory daemon status Is it on, who is running, how much it holds',
|
|
1656
|
+
' npx crbro-memory daemon stop Stop it; clients start a new one on demand',
|
|
1657
|
+
'',
|
|
1658
|
+
' Search:',
|
|
1659
|
+
' npx crbro-memory reindex Rebuild the search index',
|
|
1660
|
+
' npx crbro-memory eval Measure retrieval against <brain>/.eval/queries.json',
|
|
1661
|
+
'',
|
|
1662
|
+
' Semantic recall (installed by init, ~500 MB once per machine; init --no-semantic skips it, CRBRO_SEMANTIC=0 turns it off):',
|
|
1663
|
+
' npx crbro-memory semantic status Runtime, model and whether it is enabled',
|
|
1664
|
+
' npx crbro-memory semantic install Install transformers.js into ~/.crbro/.semantic and embed the brain',
|
|
1665
|
+
' npx crbro-memory semantic build Embed the whole brain again',
|
|
1666
|
+
'',
|
|
1667
|
+
' Credentials (per machine, sealed in the OS keychain):',
|
|
1668
|
+
' npx crbro-memory secret set NAME Store one; the value is read from stdin, never argv',
|
|
1669
|
+
' npx crbro-memory secret list Names only, never values',
|
|
1670
|
+
' npx crbro-memory secret get NAME Print one; pipe it to keep it off the screen',
|
|
1671
|
+
' npx crbro-memory secret remove NAME --yes',
|
|
1672
|
+
' npx crbro-memory secret status Which keychain this machine offers',
|
|
1673
|
+
' npx crbro-memory secret copies Windows: sealed copies of the store',
|
|
1674
|
+
' npx crbro-memory secret restore ID --yes Put a sealed copy back (validated first)',
|
|
1675
|
+
'',
|
|
1676
|
+
' Server:',
|
|
1677
|
+
' npx crbro-memory Start the MCP server (stdio)',
|
|
1678
|
+
' npx crbro-memory serve --http [--port 8787] [--host 127.0.0.1] [--allow-remote] [--new-token]',
|
|
1679
|
+
' MCP over Streamable HTTP, bearer token, no TLS, no OAuth',
|
|
1680
|
+
' npx crbro-memory --version Print the version',
|
|
1681
|
+
' npx crbro-memory <command> --help Help for one command; nothing runs',
|
|
1682
|
+
'',
|
|
1683
|
+
' See and move your memory:',
|
|
1684
|
+
' npx crbro-memory view [--port N] [--no-open] Local page (127.0.0.1, one-time token)',
|
|
1685
|
+
' npx crbro-memory export [--out DIR] [--format md|json] [--include-retired] Obsidian vault or JSON',
|
|
1686
|
+
' npx crbro-memory import PATH [--apply] [--topic NAME] [--type fact|preference|pattern] Dry run by default',
|
|
1687
|
+
'',
|
|
1688
|
+
' Exit codes: 0 done · 1 failed · 2 the command line is wrong (unknown command or option).',
|
|
1689
|
+
' Open source (MIT) — https://github.com/Octonove/crbro-memory',
|
|
1690
|
+
'',
|
|
1691
|
+
];
|
|
1692
|
+
console.log(L.join('\n'));
|
|
1437
1693
|
}
|