crbro-memory 2.10.0 → 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 -465
- package/SECURITY.md +114 -12
- package/bin/crbro.mjs +539 -300
- 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 +139 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +667 -232
- package/dist/engine/doctor.js +312 -14
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +18 -7
- 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 +721 -162
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +29 -6
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +25 -3
- 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 +797 -176
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +583 -373
- package/dist/sync/git.js +203 -72
- package/dist/sync/space.js +100 -25
- 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 +94 -7
- 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 +9 -5
- 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 -682
- 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/doctor.d.ts +0 -93
- package/dist/engine/doctor.d.ts.map +0 -1
- package/dist/engine/doctor.js.map +0 -1
- package/dist/engine/evidence.d.ts +0 -26
- package/dist/engine/evidence.d.ts.map +0 -1
- package/dist/engine/evidence.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 -64
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/projects.d.ts +0 -20
- package/dist/engine/projects.d.ts.map +0 -1
- package/dist/engine/projects.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 -155
- 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 -328
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/quality.d.ts +0 -18
- package/dist/search/quality.d.ts.map +0 -1
- package/dist/search/quality.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 -36
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -147
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -118
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -369
- 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 -13
- 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,9 @@ 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') {
|
|
172
370
|
// Observational only: never fall through to initialization or MCP startup.
|
|
173
371
|
const json = args.includes('--json');
|
|
174
372
|
try {
|
|
@@ -242,7 +440,7 @@ if (command === 'doctor') {
|
|
|
242
440
|
console.log(' 2. (Optional) npx crbro-memory setup-miner');
|
|
243
441
|
console.log(' 3. Restart your IDE — CRBRO boots automatically!');
|
|
244
442
|
console.log('');
|
|
245
|
-
}).catch(
|
|
443
|
+
}).catch(fail);
|
|
246
444
|
|
|
247
445
|
} else if (command === 'status') {
|
|
248
446
|
// ─── Show brain status ─────────────────────────────────────────
|
|
@@ -264,12 +462,16 @@ if (command === 'doctor') {
|
|
|
264
462
|
console.log('');
|
|
265
463
|
|
|
266
464
|
// Show detected IDEs
|
|
465
|
+
// A config file is not a connection: say whether CRBRO is in it.
|
|
267
466
|
const ides = detectIDEs();
|
|
268
467
|
if (ides.length > 0) {
|
|
269
|
-
console.log(' 📡
|
|
468
|
+
console.log(' 📡 Clients found:');
|
|
270
469
|
for (const ide of ides) {
|
|
271
|
-
|
|
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'}`);
|
|
272
473
|
}
|
|
474
|
+
console.log(' npx crbro-memory install registers it everywhere; npx crbro-memory doctor checks each registration.');
|
|
273
475
|
console.log('');
|
|
274
476
|
}
|
|
275
477
|
} catch {
|
|
@@ -277,8 +479,9 @@ if (command === 'doctor') {
|
|
|
277
479
|
console.log(' 🧠 CRBRO brain not initialized.');
|
|
278
480
|
console.log(' Run: npx crbro-memory init');
|
|
279
481
|
console.log('');
|
|
482
|
+
process.exitCode = 1;
|
|
280
483
|
}
|
|
281
|
-
}).catch(
|
|
484
|
+
}).catch(fail);
|
|
282
485
|
|
|
283
486
|
} else if (command === 'activate') {
|
|
284
487
|
// ─── Legacy command (pre-1.4.0) — CRBRO is now fully free ──────
|
|
@@ -317,10 +520,11 @@ if (command === 'doctor') {
|
|
|
317
520
|
for (const err of result.errors.slice(0, 5)) {
|
|
318
521
|
console.log(` ${err}`);
|
|
319
522
|
}
|
|
523
|
+
process.exitCode = 1;
|
|
320
524
|
}
|
|
321
525
|
|
|
322
526
|
console.log('');
|
|
323
|
-
}).catch(
|
|
527
|
+
}).catch(fail);
|
|
324
528
|
|
|
325
529
|
} else if (command === 'setup-miner') {
|
|
326
530
|
// ─── Setup automatic mining ────────────────────────────────────
|
|
@@ -331,8 +535,9 @@ if (command === 'doctor') {
|
|
|
331
535
|
|
|
332
536
|
const result = await setupScheduler();
|
|
333
537
|
console.log(result.message);
|
|
538
|
+
if (!result.success) process.exitCode = 1;
|
|
334
539
|
console.log('');
|
|
335
|
-
}).catch(
|
|
540
|
+
}).catch(fail);
|
|
336
541
|
|
|
337
542
|
} else if (command === 'miner-status') {
|
|
338
543
|
// ─── Check miner status ────────────────────────────────────────
|
|
@@ -370,7 +575,7 @@ if (command === 'doctor') {
|
|
|
370
575
|
console.log(' ⚠️ No IDE directories detected.');
|
|
371
576
|
}
|
|
372
577
|
console.log('');
|
|
373
|
-
}).catch(
|
|
578
|
+
}).catch(fail);
|
|
374
579
|
|
|
375
580
|
} else if (command === 'remove-miner') {
|
|
376
581
|
// ─── Remove automatic mining ───────────────────────────────────
|
|
@@ -378,8 +583,9 @@ if (command === 'doctor') {
|
|
|
378
583
|
const result = await removeScheduler();
|
|
379
584
|
console.log('');
|
|
380
585
|
console.log(result.success ? ` ✅ ${result.message}` : ` ❌ ${result.message}`);
|
|
586
|
+
if (!result.success) process.exitCode = 1;
|
|
381
587
|
console.log('');
|
|
382
|
-
}).catch(
|
|
588
|
+
}).catch(fail);
|
|
383
589
|
|
|
384
590
|
} else if (command === 'reindex') {
|
|
385
591
|
// ─── Rebuild the search index from the cortex ──────────────────
|
|
@@ -401,7 +607,7 @@ if (command === 'doctor') {
|
|
|
401
607
|
console.log(' Every fact, decision and pattern is now searchable on its own,');
|
|
402
608
|
console.log(' so a big neuron is no longer buried by short ones.');
|
|
403
609
|
console.log('');
|
|
404
|
-
}).catch(
|
|
610
|
+
}).catch(fail);
|
|
405
611
|
|
|
406
612
|
} else if (command === 'semantic') {
|
|
407
613
|
// ─── Semantic recall: status | install | build ─────────────────
|
|
@@ -424,6 +630,7 @@ if (command === 'doctor') {
|
|
|
424
630
|
console.log('');
|
|
425
631
|
console.log(' ❌ Runtime not installed. Run: npx crbro-memory semantic install');
|
|
426
632
|
console.log('');
|
|
633
|
+
process.exitCode = 1;
|
|
427
634
|
return;
|
|
428
635
|
}
|
|
429
636
|
console.log('');
|
|
@@ -446,7 +653,7 @@ if (command === 'doctor') {
|
|
|
446
653
|
console.log(` Home: ${st.home}`);
|
|
447
654
|
console.log('');
|
|
448
655
|
}
|
|
449
|
-
}).catch(
|
|
656
|
+
}).catch(fail);
|
|
450
657
|
|
|
451
658
|
} else if (command === 'eval') {
|
|
452
659
|
// ─── Measure retrieval quality against a query set ─────────────
|
|
@@ -478,6 +685,7 @@ if (command === 'doctor') {
|
|
|
478
685
|
console.log(' Build it from facts you already saved: take six or eight words');
|
|
479
686
|
console.log(' out of a real fact and name the neuron that holds it.');
|
|
480
687
|
console.log('');
|
|
688
|
+
process.exitCode = 1;
|
|
481
689
|
return;
|
|
482
690
|
}
|
|
483
691
|
|
|
@@ -529,7 +737,7 @@ if (command === 'doctor') {
|
|
|
529
737
|
if (misses.length > 25) console.log(` ... and ${misses.length - 25} more`);
|
|
530
738
|
}
|
|
531
739
|
console.log('');
|
|
532
|
-
}).catch(
|
|
740
|
+
}).catch(fail);
|
|
533
741
|
|
|
534
742
|
} else if (command === 'daemon') {
|
|
535
743
|
// ─── One process owns the brain for every client (opt-in) ──────
|
|
@@ -546,24 +754,20 @@ if (command === 'doctor') {
|
|
|
546
754
|
const { resolveBrainDir } = await import('../dist/engine/brain.js');
|
|
547
755
|
const ep = await import('../dist/daemon/endpoint.js');
|
|
548
756
|
const { controlDaemon } = await import('../dist/daemon/proxy.js');
|
|
549
|
-
const fs = await import('fs');
|
|
550
757
|
const root = resolveBrainDir();
|
|
551
758
|
const sub = args[1] || 'status';
|
|
552
759
|
|
|
760
|
+
let cleaned = 0;
|
|
553
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;
|
|
554
769
|
const out = [];
|
|
555
|
-
for (const st of
|
|
556
|
-
const reply = await controlDaemon(root, 'status', st.build);
|
|
557
|
-
if (reply) { out.push({ st, reply }); continue; }
|
|
558
|
-
// No answer is not proof of death: a daemon in the middle of a rebuild
|
|
559
|
-
// answers late, and deleting its state file would orphan it — alive,
|
|
560
|
-
// holding the brain, and unreachable by every client. Only a pid the
|
|
561
|
-
// system says is gone loses its file.
|
|
562
|
-
let alive = true;
|
|
563
|
-
try { process.kill(st.pid, 0); } catch (e) { alive = e.code === 'EPERM'; }
|
|
564
|
-
if (alive) out.push({ st, reply: null });
|
|
565
|
-
else { try { fs.rmSync(ep.stateFile(root, st.build), { force: true }); } catch { /* stale file, best effort */ } }
|
|
566
|
-
}
|
|
770
|
+
for (const st of [...swept.alive, ...swept.unknown]) out.push({ st, reply: await controlDaemon(root, 'status', st.build) });
|
|
567
771
|
return out;
|
|
568
772
|
};
|
|
569
773
|
|
|
@@ -598,6 +802,7 @@ if (command === 'doctor') {
|
|
|
598
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`);
|
|
599
803
|
}
|
|
600
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`);
|
|
601
806
|
console.log(` Log: ${root}${root.includes('\\') ? '\\' : '/'}.daemon${root.includes('\\') ? '\\' : '/'}daemon.log`);
|
|
602
807
|
console.log('');
|
|
603
808
|
}
|
|
@@ -625,19 +830,22 @@ if (command === 'doctor') {
|
|
|
625
830
|
}
|
|
626
831
|
const texto = process.argv.slice(3).filter(a => !a.startsWith('--')).join(' ').trim();
|
|
627
832
|
if (!texto) {
|
|
628
|
-
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');
|
|
629
834
|
return;
|
|
630
835
|
}
|
|
631
836
|
let index;
|
|
632
837
|
try {
|
|
633
838
|
index = JSON.parse(fs.readFileSync(triggerIndexPath(brain), 'utf8'));
|
|
634
839
|
} catch {
|
|
635
|
-
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;
|
|
636
842
|
return;
|
|
637
843
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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`);
|
|
641
849
|
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
642
850
|
|
|
643
851
|
} else if (command === 'usage' || command === 'postmortem') {
|
|
@@ -687,20 +895,29 @@ if (command === 'doctor') {
|
|
|
687
895
|
// any difference, so it can gate a script.
|
|
688
896
|
// The Claude Code mod is checked too (install-mod): a copy in the home
|
|
689
897
|
// folder that runs in every session, so the same question applies.
|
|
690
|
-
|
|
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(([
|
|
691
900
|
{ verifyHooks, formatVerify },
|
|
692
901
|
{ verifyMod, formatModVerify },
|
|
902
|
+
io,
|
|
693
903
|
]) => {
|
|
694
904
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
905
|
+
const claudeHome = io.claudeDir(homedir(), process.env);
|
|
695
906
|
const r = verifyHooks({
|
|
696
907
|
packageDir: join(here, '..', 'hooks'),
|
|
697
|
-
installedDir: join(
|
|
698
|
-
settingsPath: join(
|
|
908
|
+
installedDir: join(claudeHome, 'crbro-hooks'),
|
|
909
|
+
settingsPath: join(claudeHome, 'settings.json'),
|
|
699
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;
|
|
700
917
|
const m = verifyMod({ packageDir: join(here, '..', 'mods', 'crbro-pending'), home: homedir() });
|
|
701
|
-
if (args.includes('--json')) console.log(JSON.stringify({ ...r, mod: m }, null, 2));
|
|
702
|
-
else console.log(formatVerify(r) + formatModVerify(m));
|
|
703
|
-
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;
|
|
704
921
|
}).catch(err => {
|
|
705
922
|
console.error(' ❌ ' + (err && err.code === 'ERR_MODULE_NOT_FOUND' ? 'Build required. Run: npm run build' : err.message));
|
|
706
923
|
process.exit(1);
|
|
@@ -722,27 +939,21 @@ if (command === 'doctor') {
|
|
|
722
939
|
// crbro-hooks/ so uninstall puts it back exactly. A hand-written CRBRO hook
|
|
723
940
|
// is never replaced: ours is installed with --no-boot / --no-reminder next
|
|
724
941
|
// to it, so nothing is read twice. Merged, never rewritten; idempotent.
|
|
725
|
-
import('fs').then(async fs => {
|
|
726
|
-
const
|
|
727
|
-
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');
|
|
728
946
|
const hookScript = join(hookDir, 'crbro-lifecycle.mjs');
|
|
729
947
|
const replacedPath = join(hookDir, 'crbro-lifecycle.replaced.json');
|
|
730
948
|
const ours = h => !!h && typeof h.command === 'string' && h.command.includes('crbro-lifecycle');
|
|
731
949
|
const entryIsOurs = e => Array.isArray(e?.hooks) && e.hooks.some(ours);
|
|
732
950
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
|
|
740
|
-
console.error(` ${e.message}`);
|
|
741
|
-
process.exit(1);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (settings !== null && (typeof settings !== 'object' || Array.isArray(settings))) {
|
|
745
|
-
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.`);
|
|
746
957
|
process.exit(1);
|
|
747
958
|
}
|
|
748
959
|
for (const event of ['PreCompact', 'SessionStart']) {
|
|
@@ -752,10 +963,10 @@ if (command === 'doctor') {
|
|
|
752
963
|
process.exit(1);
|
|
753
964
|
}
|
|
754
965
|
}
|
|
966
|
+
// A backup first; refused if Claude Code or an editor saved it since it was read.
|
|
755
967
|
const write = obj => {
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
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}`);
|
|
759
970
|
};
|
|
760
971
|
|
|
761
972
|
if (command === 'uninstall-hooks') {
|
|
@@ -801,13 +1012,15 @@ if (command === 'doctor') {
|
|
|
801
1012
|
}
|
|
802
1013
|
|
|
803
1014
|
// — install —
|
|
804
|
-
if (settings === null && !fs.existsSync(
|
|
805
|
-
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.`);
|
|
806
1017
|
return;
|
|
807
1018
|
}
|
|
808
1019
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
811
1024
|
settings = settings || {};
|
|
812
1025
|
settings.hooks = settings.hooks || {};
|
|
813
1026
|
const node = `node "${hookScript.split('\\').join('/')}"`;
|
|
@@ -857,11 +1070,12 @@ if (command === 'doctor') {
|
|
|
857
1070
|
console.log(' Undo with: npx crbro-memory uninstall-hooks --compact');
|
|
858
1071
|
}
|
|
859
1072
|
console.log(' Never blocks; checkpoints are redacted and expire after 7 days. New sessions pick it up.');
|
|
860
|
-
}).catch(
|
|
1073
|
+
}).catch(fail);
|
|
861
1074
|
|
|
862
1075
|
} else if (command === 'uninstall-hooks') {
|
|
863
1076
|
console.log(' Usage: npx crbro-memory uninstall-hooks --compact');
|
|
864
|
-
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;
|
|
865
1079
|
|
|
866
1080
|
} else if (command === 'install-hooks' && process.argv.includes('--guard')) {
|
|
867
1081
|
// ─── Wire the PreToolUse guard into Claude Code (opt-in) ───────
|
|
@@ -871,46 +1085,44 @@ if (command === 'doctor') {
|
|
|
871
1085
|
// that mention it to the model's context — once per session each, three at
|
|
872
1086
|
// most. It never blocks and never asks. Remove the PreToolUse entry from
|
|
873
1087
|
// ~/.claude/settings.json to uninstall.
|
|
874
|
-
import('
|
|
875
|
-
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');
|
|
876
1091
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
877
|
-
const hookDir = join(
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
let
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
settings =
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
console.error(` ${e.message}`);
|
|
890
|
-
process.exit(1);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
settings.hooks = settings.hooks || {};
|
|
894
|
-
const list = settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
|
|
895
|
-
if (JSON.stringify(list).includes('crbro-guard')) {
|
|
896
|
-
console.log(' ✅ PreToolUse guard already installed. Script refreshed.');
|
|
897
|
-
} 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;
|
|
898
1104
|
list.push({
|
|
899
1105
|
matcher: 'Bash|PowerShell',
|
|
900
1106
|
hooks: [{ type: 'command', command: `node "${hookScript.split('\\').join('/')}"`, timeout: 5 }],
|
|
901
1107
|
});
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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 {
|
|
905
1115
|
console.log(' ✅ PreToolUse guard installed.');
|
|
906
1116
|
console.log(` ${settingsPath}`);
|
|
1117
|
+
if (r0.backup) console.log(` backup: ${r0.backup}`);
|
|
907
1118
|
}
|
|
908
1119
|
const { Brain } = await import('../dist/engine/brain.js');
|
|
909
1120
|
const { writeTriggerIndex } = await import('../dist/engine/triggers.js');
|
|
910
1121
|
const r = await writeTriggerIndex(new Brain());
|
|
911
|
-
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"`);
|
|
912
1124
|
console.log(' It adds context, never blocks. New sessions pick it up.');
|
|
913
|
-
}).catch(
|
|
1125
|
+
}).catch(fail);
|
|
914
1126
|
|
|
915
1127
|
} else if (command === 'install-hooks') {
|
|
916
1128
|
// ─── Wire the SubagentStart hook into Claude Code ──────────────
|
|
@@ -922,18 +1134,20 @@ if (command === 'doctor') {
|
|
|
922
1134
|
//
|
|
923
1135
|
// Merges into ~/.claude/settings.json without touching anything else.
|
|
924
1136
|
// Idempotent: running it twice changes nothing the second time.
|
|
925
|
-
import('
|
|
926
|
-
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');
|
|
927
1140
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
928
1141
|
const source = join(here, '..', 'hooks', 'crbro-subagent.mjs');
|
|
929
1142
|
|
|
930
1143
|
// Copy the hook to a stable location. When CRBRO runs from the npx
|
|
931
1144
|
// cache, `here` changes with every release and the stale path would
|
|
932
1145
|
// break the hook silently on the next update.
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
|
937
1151
|
// Injection is OPT-IN since 1.12 (pre-registered consequence of three
|
|
938
1152
|
// clean-control benchmark runs: no measured benefit in any model, harm
|
|
939
1153
|
// and fabricated compliance in small ones — see the benchmarks in the
|
|
@@ -942,43 +1156,38 @@ if (command === 'doctor') {
|
|
|
942
1156
|
const conInyeccion = process.argv.includes('--inject');
|
|
943
1157
|
const hookCmd = (conInyeccion ? 'CRBRO_SUBAGENT_INJECT=full ' : '') + `node "${hookScript.split('\\').join('/')}"`;
|
|
944
1158
|
|
|
945
|
-
let
|
|
946
|
-
|
|
947
|
-
settings
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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); }
|
|
959
1179
|
if (yaEsta) {
|
|
960
1180
|
console.log(' ✅ SubagentStart hook already installed. Script refreshed.');
|
|
961
|
-
if (conInyeccion && !
|
|
1181
|
+
if (conInyeccion && !injectsAlready) {
|
|
962
1182
|
console.log(' ⚠️ The installed entry does NOT enable injection. To enable it,');
|
|
963
1183
|
console.log(' remove the SubagentStart entry from settings.json and re-run');
|
|
964
1184
|
console.log(' install-hooks --inject.');
|
|
965
1185
|
}
|
|
966
1186
|
return;
|
|
967
1187
|
}
|
|
968
|
-
list.push({
|
|
969
|
-
hooks: [{
|
|
970
|
-
type: 'command',
|
|
971
|
-
command: hookCmd,
|
|
972
|
-
timeout: 5,
|
|
973
|
-
statusMessage: 'Inyectando protocolos CRBRO en el subagente...',
|
|
974
|
-
}],
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
const tmp = settingsPath + '.' + process.pid + '.tmp';
|
|
978
|
-
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
|
|
979
|
-
fs.renameSync(tmp, settingsPath);
|
|
980
1188
|
console.log(' ✅ SubagentStart hook installed.');
|
|
981
1189
|
console.log(` ${settingsPath}`);
|
|
1190
|
+
if (r0.backup) console.log(` backup: ${r0.backup}`);
|
|
982
1191
|
if (conInyeccion) {
|
|
983
1192
|
console.log(' Injection ENABLED: every Task-spawned subagent receives the');
|
|
984
1193
|
console.log(' protocol block. Scope it with CRBRO_SUBAGENT_MATCHER (regex on');
|
|
@@ -990,7 +1199,7 @@ if (command === 'doctor') {
|
|
|
990
1199
|
console.log(' three clean benchmark runs found no benefit in any model and');
|
|
991
1200
|
console.log(' harm in small ones). Re-run with --inject to enable it.');
|
|
992
1201
|
}
|
|
993
|
-
}).catch(
|
|
1202
|
+
}).catch(fail);
|
|
994
1203
|
|
|
995
1204
|
} else if (command === 'install-mod' && process.argv.includes('--verify')) {
|
|
996
1205
|
// ─── Is the installed mod the one this package ships? ──────────
|
|
@@ -1061,120 +1270,105 @@ if (command === 'doctor') {
|
|
|
1061
1270
|
// like it was never there. Every report of "CRBRO does not remember" so far
|
|
1062
1271
|
// has been this, not a bug in the recall.
|
|
1063
1272
|
//
|
|
1064
|
-
// So this wires the start itself, per client:
|
|
1273
|
+
// So this wires the start itself, per client (src/engine/installer.ts):
|
|
1065
1274
|
//
|
|
1066
|
-
// Claude Code
|
|
1275
|
+
// Claude Code <config>/settings.json — SessionStart runs a command whose
|
|
1067
1276
|
// stdout is added to the session, telling the model to call
|
|
1068
1277
|
// crbro_boot first. Claude Code has no way to invoke an MCP
|
|
1069
1278
|
// tool from a hook, so the instruction is the mechanism.
|
|
1070
|
-
// Codex
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
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.
|
|
1075
1285
|
//
|
|
1076
|
-
// Only
|
|
1077
|
-
// rewritten
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
if (!fs.existsSync(p)) return null;
|
|
1091
|
-
try {
|
|
1092
|
-
return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^/, ''));
|
|
1093
|
-
} catch (e) {
|
|
1094
|
-
console.error(` ❌ ${p} exists but could not be parsed — not touching it.`);
|
|
1095
|
-
console.error(` ${e.message}`);
|
|
1096
|
-
process.exit(1);
|
|
1097
|
-
}
|
|
1098
|
-
};
|
|
1099
|
-
const escribirJson = (p, obj) => {
|
|
1100
|
-
const tmp = p + '.' + process.pid + '.tmp';
|
|
1101
|
-
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
1102
|
-
fs.renameSync(tmp, p);
|
|
1103
|
-
};
|
|
1104
|
-
|
|
1105
|
-
let tocados = 0, yaEstaban = 0, ausentes = [];
|
|
1106
|
-
|
|
1107
|
-
// — Claude Code —
|
|
1108
|
-
const claudePath = join(homedir(), '.claude', 'settings.json');
|
|
1109
|
-
const claude = leerJson(claudePath) ?? (fs.existsSync(join(homedir(), '.claude')) ? {} : null);
|
|
1110
|
-
if (claude === null) {
|
|
1111
|
-
ausentes.push('Claude Code (~/.claude not found)');
|
|
1112
|
-
} else {
|
|
1113
|
-
claude.hooks = claude.hooks || {};
|
|
1114
|
-
const list = claude.hooks.SessionStart = claude.hooks.SessionStart || [];
|
|
1115
|
-
// Any mention of crbro counts as "already wired". A hand-rolled hook
|
|
1116
|
-
// often points at a file — `cat ~/.claude/crbro-session-start.txt` — so
|
|
1117
|
-
// looking for crbro_boot alone misses it and installs a second entry
|
|
1118
|
-
// that boots the brain twice.
|
|
1119
|
-
if (/crbro/i.test(JSON.stringify(list))) {
|
|
1120
|
-
yaEstaban++;
|
|
1121
|
-
console.log(' ⚪ Claude Code: SessionStart already starts CRBRO. Left alone.');
|
|
1122
|
-
} else {
|
|
1123
|
-
list.push({
|
|
1124
|
-
matcher: MATCHER,
|
|
1125
|
-
hooks: [{ type: 'command', command: cmdPosix, shell: 'bash', timeout: 10, statusMessage: 'Loading CRBRO memory...' }],
|
|
1126
|
-
});
|
|
1127
|
-
escribirJson(claudePath, claude);
|
|
1128
|
-
tocados++;
|
|
1129
|
-
console.log(` ✅ Claude Code: SessionStart hook added.\n ${claudePath}`);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// — Codex —
|
|
1134
|
-
const codexDir = join(homedir(), '.codex');
|
|
1135
|
-
const codexPath = join(codexDir, 'hooks.json');
|
|
1136
|
-
if (!fs.existsSync(codexDir)) {
|
|
1137
|
-
ausentes.push('Codex (~/.codex not found)');
|
|
1138
|
-
} else {
|
|
1139
|
-
const codex = leerJson(codexPath) ?? {};
|
|
1140
|
-
codex.hooks = codex.hooks || {};
|
|
1141
|
-
const list = codex.hooks.SessionStart = codex.hooks.SessionStart || [];
|
|
1142
|
-
// Any mention of crbro counts as "already wired". A hand-rolled hook
|
|
1143
|
-
// often points at a file — `cat ~/.claude/crbro-session-start.txt` — so
|
|
1144
|
-
// looking for crbro_boot alone misses it and installs a second entry
|
|
1145
|
-
// that boots the brain twice.
|
|
1146
|
-
if (/crbro/i.test(JSON.stringify(list))) {
|
|
1147
|
-
yaEstaban++;
|
|
1148
|
-
console.log(' ⚪ Codex: SessionStart already starts CRBRO. Left alone.');
|
|
1149
|
-
} else {
|
|
1150
|
-
list.push({
|
|
1151
|
-
matcher: MATCHER,
|
|
1152
|
-
hooks: [
|
|
1153
|
-
{ type: 'mcp_tool', server: 'crbro', tool: 'crbro_boot', input: {}, timeout: 30, statusMessage: 'Loading CRBRO memory...' },
|
|
1154
|
-
{ type: 'command', command: cmdPosix, commandWindows: cmdWin, timeout: 10, statusMessage: 'Checking CRBRO start...' },
|
|
1155
|
-
],
|
|
1156
|
-
});
|
|
1157
|
-
escribirJson(codexPath, codex);
|
|
1158
|
-
tocados++;
|
|
1159
|
-
console.log(` ✅ Codex: SessionStart hook added (MCP call + fallback).\n ${codexPath}`);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
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();
|
|
1163
1300
|
console.log('');
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
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.`);
|
|
1168
1303
|
console.log('');
|
|
1169
1304
|
console.log(' Other tools (Cursor, Windsurf, Antigravity…) have no session hooks.');
|
|
1170
1305
|
console.log(' Put this line in their always-on rules file (.cursorrules, .windsurfrules,');
|
|
1171
1306
|
console.log(' User Rules) and it does the same job:');
|
|
1172
1307
|
console.log('');
|
|
1173
|
-
console.log(` ${
|
|
1308
|
+
console.log(` ${ctx.bootNotice}`);
|
|
1174
1309
|
console.log('');
|
|
1175
|
-
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
|
+
}
|
|
1176
1314
|
console.log('');
|
|
1177
|
-
|
|
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);
|
|
1178
1372
|
|
|
1179
1373
|
} else if (command === 'secret') {
|
|
1180
1374
|
// ─── Credentials, from the terminal ────────────────────────────
|
|
@@ -1265,8 +1459,20 @@ if (command === 'doctor') {
|
|
|
1265
1459
|
return;
|
|
1266
1460
|
}
|
|
1267
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
|
+
|
|
1268
1474
|
if (!name) {
|
|
1269
|
-
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`);
|
|
1270
1476
|
process.exit(1);
|
|
1271
1477
|
}
|
|
1272
1478
|
|
|
@@ -1278,7 +1484,7 @@ if (command === 'doctor') {
|
|
|
1278
1484
|
console.error(' ❌ Empty value. Nothing was stored.');
|
|
1279
1485
|
process.exit(1);
|
|
1280
1486
|
}
|
|
1281
|
-
kc.setSecret(name, value, flagValue('--description'));
|
|
1487
|
+
kc.setSecret(name, value, flagValue('--description'), { force: args.includes('--force') });
|
|
1282
1488
|
console.log(` ✅ ${name} sealed in the OS keychain. CRBRO keeps no copy.`);
|
|
1283
1489
|
console.log(' Record only the NAME in the brain, never the value.');
|
|
1284
1490
|
return;
|
|
@@ -1308,7 +1514,19 @@ if (command === 'doctor') {
|
|
|
1308
1514
|
return;
|
|
1309
1515
|
}
|
|
1310
1516
|
|
|
1311
|
-
|
|
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]');
|
|
1312
1530
|
process.exit(1);
|
|
1313
1531
|
} catch (err) {
|
|
1314
1532
|
console.error(` ❌ ${err instanceof Error ? err.message : err}`);
|
|
@@ -1330,7 +1548,8 @@ if (command === 'doctor') {
|
|
|
1330
1548
|
const kb = (n) => `${(n / 1024).toFixed(0)} KB`;
|
|
1331
1549
|
try {
|
|
1332
1550
|
if (sub === 'create') {
|
|
1333
|
-
|
|
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;
|
|
1334
1553
|
const r = await bk.createBackup(paths, { dir, keep });
|
|
1335
1554
|
console.log('');
|
|
1336
1555
|
console.log(` ✅ Backup written: ${r.path}`);
|
|
@@ -1380,75 +1599,95 @@ if (command === 'doctor') {
|
|
|
1380
1599
|
process.exit(1);
|
|
1381
1600
|
});
|
|
1382
1601
|
|
|
1383
|
-
} else if (command === '--help' || command === '-h') {
|
|
1384
|
-
// ─── Help ──────────────────────────────────────────────────────
|
|
1385
|
-
console.log('');
|
|
1386
|
-
console.log(' 🧠 CRBRO — Persistent Neural Memory for AI');
|
|
1387
|
-
console.log(' ═══════════════════════════════════════════');
|
|
1388
|
-
console.log('');
|
|
1389
|
-
console.log(' Setup:');
|
|
1390
|
-
console.log(' npx crbro-memory init Initialize brain + detect IDEs');
|
|
1391
|
-
console.log(' npx crbro-memory install-boot Make the memory load itself in every conversation');
|
|
1392
|
-
console.log(' npx crbro-memory status Show brain status');
|
|
1393
|
-
console.log(' npx crbro-memory doctor [--json] Read-only local diagnostics; never initializes or boots');
|
|
1394
|
-
console.log('');
|
|
1395
|
-
console.log(' Auto-Mining:');
|
|
1396
|
-
console.log(' npx crbro-memory mine [dir] One-shot mining of artifacts');
|
|
1397
|
-
console.log(' npx crbro-memory setup-miner Install scheduled auto-miner');
|
|
1398
|
-
console.log(' npx crbro-memory miner-status Check auto-miner status');
|
|
1399
|
-
console.log(' npx crbro-memory remove-miner Remove auto-miner');
|
|
1400
|
-
console.log('');
|
|
1401
|
-
console.log(' Backup (one gzipped file per copy; consolidate makes one a day on its own):');
|
|
1402
|
-
console.log(' npx crbro-memory backup Back the brain up now (keeps the newest 7)');
|
|
1403
|
-
console.log(' npx crbro-memory backup list What there is, newest first');
|
|
1404
|
-
console.log(' npx crbro-memory backup restore FILE Unpack into a NEW folder, never over the live brain');
|
|
1405
|
-
console.log(' CRBRO_BACKUP_DIR=<synced folder> Put them somewhere that survives the disk');
|
|
1406
|
-
console.log('');
|
|
1407
|
-
console.log(' Guard (opt-in, Claude Code): stored lessons speak before a shell command runs:');
|
|
1408
|
-
console.log(' npx crbro-memory install-hooks --guard Wire the PreToolUse hook (adds context, never blocks)');
|
|
1409
|
-
console.log(' npx crbro-memory guard "<command>" What it would say for a command · --rebuild writes the index');
|
|
1410
|
-
console.log('');
|
|
1411
|
-
console.log(' Compact without losing the thread (opt-in, Claude Code):');
|
|
1412
|
-
console.log(' npx crbro-memory install-hooks --compact PreCompact checkpoint + SessionStart that picks it back up');
|
|
1413
|
-
console.log(' npx crbro-memory uninstall-hooks --compact Remove both and put back what was replaced');
|
|
1414
|
-
console.log(' npx crbro-memory install-hooks --verify SHA-256 of the installed hooks and mod against this package (read-only)');
|
|
1415
|
-
console.log(' npx crbro-memory install-mod [--lang en|es|auto] Claude Code band + /pending with the open items (Claude Code 2.1.286+)');
|
|
1416
|
-
console.log(' crbro_boot installs and updates it on its own; CRBRO_MOD=0 turns that off');
|
|
1417
|
-
console.log(' npx crbro-memory uninstall-mod Remove the mod and its CLAUDE_CODE_PLUGIN_DIRS entry; never put back on its own');
|
|
1418
|
-
console.log(' npx crbro-memory install-mod --verify SHA-256 of the installed mod against this package (read-only)');
|
|
1419
|
-
console.log('');
|
|
1420
|
-
console.log(' Looking back at Claude Code sessions (read-only; last 7 days unless --days N, 0 = all):');
|
|
1421
|
-
console.log(' npx crbro-memory usage [--session ID] [--project DIR] [--json] Tokens per model and session, subagents apart');
|
|
1422
|
-
console.log(' npx crbro-memory postmortem [--max N] [--json] Candidate lessons: corrections, failing tools, repeats');
|
|
1423
|
-
console.log('');
|
|
1424
|
-
console.log(' Daemon (opt-in): one process owns the brain for every client — one index, one model, one writer:');
|
|
1425
|
-
console.log(' npx crbro-memory daemon on | off Switch every client of this brain, the next time each starts');
|
|
1426
|
-
console.log(' npx crbro-memory daemon status Is it on, who is running, how much it holds');
|
|
1427
|
-
console.log(' npx crbro-memory daemon stop Stop it; clients start a new one on demand');
|
|
1428
|
-
console.log('');
|
|
1429
|
-
console.log(' Search:');
|
|
1430
|
-
console.log(' npx crbro-memory reindex Rebuild the search index');
|
|
1431
|
-
console.log(' npx crbro-memory eval Measure retrieval against .crbro/.eval/queries.json');
|
|
1432
|
-
console.log('');
|
|
1433
|
-
console.log(' Semantic layer (opt-in, ~500 MB on disk, measured in benchmarks/):');
|
|
1434
|
-
console.log(' npx crbro-memory semantic install Install transformers.js into ~/.crbro/.semantic');
|
|
1435
|
-
console.log(' npx crbro-memory semantic build Embed the whole brain once (needs CRBRO_SEMANTIC=1)');
|
|
1436
|
-
console.log(' npx crbro-memory semantic status Runtime, model and whether it is enabled');
|
|
1437
|
-
console.log('');
|
|
1438
|
-
console.log(' Credentials (per machine, sealed in the OS keychain):');
|
|
1439
|
-
console.log(' npx crbro-memory secret set NAME Store one; the value is read from stdin, never argv');
|
|
1440
|
-
console.log(' npx crbro-memory secret list Names only, never values');
|
|
1441
|
-
console.log(' npx crbro-memory secret get NAME Print one; pipe it to keep it off the screen');
|
|
1442
|
-
console.log(' npx crbro-memory secret remove NAME --yes');
|
|
1443
|
-
console.log(' npx crbro-memory secret status Which keychain this machine offers');
|
|
1444
|
-
console.log('');
|
|
1445
|
-
console.log(' Server:');
|
|
1446
|
-
console.log(' npx crbro-memory Start MCP server (stdio)');
|
|
1447
|
-
console.log('');
|
|
1448
|
-
console.log(' Open source (MIT) — https://github.com/Octonove/crbro-memory');
|
|
1449
|
-
console.log('');
|
|
1450
|
-
|
|
1451
1602
|
} else {
|
|
1452
1603
|
// ─── Default: start MCP server ─────────────────────────────────
|
|
1453
|
-
|
|
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'));
|
|
1454
1693
|
}
|