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.
Files changed (197) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -456
  3. package/SECURITY.md +135 -8
  4. package/bin/crbro.mjs +555 -299
  5. package/dist/build-id.json +10 -0
  6. package/dist/cli/common.js +211 -0
  7. package/dist/cli/export.js +522 -0
  8. package/dist/cli/import.js +683 -0
  9. package/dist/cli/markdown.js +313 -0
  10. package/dist/cli/mcpclient.js +107 -0
  11. package/dist/cli/serve.js +453 -0
  12. package/dist/cli/view.js +358 -0
  13. package/dist/cli/viewer-page.js +378 -0
  14. package/dist/daemon/daemon.js +119 -11
  15. package/dist/daemon/endpoint.js +222 -21
  16. package/dist/daemon/lines.js +102 -13
  17. package/dist/daemon/proxy.js +480 -125
  18. package/dist/daemon/surface.js +116 -0
  19. package/dist/daemon/trace.js +73 -0
  20. package/dist/engine/atrest.js +132 -0
  21. package/dist/engine/backup.js +143 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +762 -311
  25. package/dist/engine/doctor.js +676 -0
  26. package/dist/engine/evidence.js +171 -0
  27. package/dist/engine/heat.js +81 -37
  28. package/dist/engine/hippocampus.js +50 -36
  29. package/dist/engine/hookrefresh.js +296 -0
  30. package/dist/engine/hookverify.js +15 -3
  31. package/dist/engine/installer.js +1022 -0
  32. package/dist/engine/installio.js +279 -0
  33. package/dist/engine/keychain.js +735 -144
  34. package/dist/engine/maintenance.js +355 -87
  35. package/dist/engine/modinstall.js +98 -45
  36. package/dist/engine/prefrontal.js +126 -69
  37. package/dist/engine/projects.js +80 -0
  38. package/dist/engine/quarantine.js +204 -0
  39. package/dist/engine/secrets.js +41 -5
  40. package/dist/engine/shelf.js +34 -11
  41. package/dist/engine/synapses.js +183 -80
  42. package/dist/engine/triggers.js +141 -11
  43. package/dist/index.js +37 -3
  44. package/dist/mcp-surface.json +1 -0
  45. package/dist/miner/extractor.js +64 -17
  46. package/dist/miner/scheduler.js +144 -68
  47. package/dist/search/index.js +810 -176
  48. package/dist/search/quality.js +31 -0
  49. package/dist/search/semantic.js +208 -18
  50. package/dist/search/tokenize.js +44 -0
  51. package/dist/server.js +649 -343
  52. package/dist/sync/git.js +203 -72
  53. package/dist/sync/materialize.js +33 -8
  54. package/dist/sync/space.js +286 -101
  55. package/dist/sync/validate.js +204 -0
  56. package/dist/utils/budget.js +15 -2
  57. package/dist/utils/fs.js +40 -6
  58. package/dist/utils/ids.js +128 -14
  59. package/dist/utils/lock.js +219 -52
  60. package/hooks/crbro-codex.mjs +284 -0
  61. package/hooks/crbro-guard.mjs +281 -173
  62. package/hooks/crbro-hooks.shipped.json +20 -0
  63. package/hooks/crbro-lifecycle.mjs +16 -4
  64. package/hooks/crbro-subagent.mjs +180 -180
  65. package/package.json +12 -8
  66. package/dist/daemon/daemon.d.ts +0 -25
  67. package/dist/daemon/daemon.d.ts.map +0 -1
  68. package/dist/daemon/daemon.js.map +0 -1
  69. package/dist/daemon/endpoint.d.ts +0 -66
  70. package/dist/daemon/endpoint.d.ts.map +0 -1
  71. package/dist/daemon/endpoint.js.map +0 -1
  72. package/dist/daemon/lines.d.ts +0 -55
  73. package/dist/daemon/lines.d.ts.map +0 -1
  74. package/dist/daemon/lines.js.map +0 -1
  75. package/dist/daemon/proxy.d.ts +0 -55
  76. package/dist/daemon/proxy.d.ts.map +0 -1
  77. package/dist/daemon/proxy.js.map +0 -1
  78. package/dist/engine/backup.d.ts +0 -68
  79. package/dist/engine/backup.d.ts.map +0 -1
  80. package/dist/engine/backup.js.map +0 -1
  81. package/dist/engine/brain.d.ts +0 -79
  82. package/dist/engine/brain.d.ts.map +0 -1
  83. package/dist/engine/brain.js.map +0 -1
  84. package/dist/engine/cortex.d.ts +0 -670
  85. package/dist/engine/cortex.d.ts.map +0 -1
  86. package/dist/engine/cortex.js.map +0 -1
  87. package/dist/engine/dates.d.ts +0 -28
  88. package/dist/engine/dates.d.ts.map +0 -1
  89. package/dist/engine/dates.js.map +0 -1
  90. package/dist/engine/heat.d.ts +0 -28
  91. package/dist/engine/heat.d.ts.map +0 -1
  92. package/dist/engine/heat.js.map +0 -1
  93. package/dist/engine/hippocampus.d.ts +0 -60
  94. package/dist/engine/hippocampus.d.ts.map +0 -1
  95. package/dist/engine/hippocampus.js.map +0 -1
  96. package/dist/engine/hookverify.d.ts +0 -42
  97. package/dist/engine/hookverify.d.ts.map +0 -1
  98. package/dist/engine/hookverify.js.map +0 -1
  99. package/dist/engine/keychain.d.ts +0 -32
  100. package/dist/engine/keychain.d.ts.map +0 -1
  101. package/dist/engine/keychain.js.map +0 -1
  102. package/dist/engine/maintenance.d.ts +0 -218
  103. package/dist/engine/maintenance.d.ts.map +0 -1
  104. package/dist/engine/maintenance.js.map +0 -1
  105. package/dist/engine/modinstall.d.ts +0 -215
  106. package/dist/engine/modinstall.d.ts.map +0 -1
  107. package/dist/engine/modinstall.js.map +0 -1
  108. package/dist/engine/postmortem.d.ts +0 -81
  109. package/dist/engine/postmortem.d.ts.map +0 -1
  110. package/dist/engine/postmortem.js.map +0 -1
  111. package/dist/engine/prefrontal.d.ts +0 -61
  112. package/dist/engine/prefrontal.d.ts.map +0 -1
  113. package/dist/engine/prefrontal.js.map +0 -1
  114. package/dist/engine/secrets.d.ts +0 -30
  115. package/dist/engine/secrets.d.ts.map +0 -1
  116. package/dist/engine/secrets.js.map +0 -1
  117. package/dist/engine/shelf.d.ts +0 -156
  118. package/dist/engine/shelf.d.ts.map +0 -1
  119. package/dist/engine/shelf.js.map +0 -1
  120. package/dist/engine/source.d.ts +0 -6
  121. package/dist/engine/source.d.ts.map +0 -1
  122. package/dist/engine/source.js.map +0 -1
  123. package/dist/engine/synapses.d.ts +0 -82
  124. package/dist/engine/synapses.d.ts.map +0 -1
  125. package/dist/engine/synapses.js.map +0 -1
  126. package/dist/engine/triggers.d.ts +0 -53
  127. package/dist/engine/triggers.d.ts.map +0 -1
  128. package/dist/engine/triggers.js.map +0 -1
  129. package/dist/engine/usage.d.ts +0 -61
  130. package/dist/engine/usage.d.ts.map +0 -1
  131. package/dist/engine/usage.js.map +0 -1
  132. package/dist/index.d.ts +0 -2
  133. package/dist/index.d.ts.map +0 -1
  134. package/dist/index.js.map +0 -1
  135. package/dist/miner/extractor.d.ts +0 -12
  136. package/dist/miner/extractor.d.ts.map +0 -1
  137. package/dist/miner/extractor.js.map +0 -1
  138. package/dist/miner/index.d.ts +0 -61
  139. package/dist/miner/index.d.ts.map +0 -1
  140. package/dist/miner/index.js.map +0 -1
  141. package/dist/miner/scheduler.d.ts +0 -30
  142. package/dist/miner/scheduler.d.ts.map +0 -1
  143. package/dist/miner/scheduler.js.map +0 -1
  144. package/dist/miner/types.d.ts +0 -11
  145. package/dist/miner/types.d.ts.map +0 -1
  146. package/dist/miner/types.js.map +0 -1
  147. package/dist/search/index.d.ts +0 -323
  148. package/dist/search/index.d.ts.map +0 -1
  149. package/dist/search/index.js.map +0 -1
  150. package/dist/search/semantic.d.ts +0 -80
  151. package/dist/search/semantic.d.ts.map +0 -1
  152. package/dist/search/semantic.js.map +0 -1
  153. package/dist/search/synonyms.d.ts +0 -2
  154. package/dist/search/synonyms.d.ts.map +0 -1
  155. package/dist/search/synonyms.js.map +0 -1
  156. package/dist/search/tokenize.d.ts +0 -39
  157. package/dist/search/tokenize.d.ts.map +0 -1
  158. package/dist/search/tokenize.js.map +0 -1
  159. package/dist/server.d.ts +0 -41
  160. package/dist/server.d.ts.map +0 -1
  161. package/dist/server.js.map +0 -1
  162. package/dist/sync/git.d.ts +0 -36
  163. package/dist/sync/git.d.ts.map +0 -1
  164. package/dist/sync/git.js.map +0 -1
  165. package/dist/sync/materialize.d.ts +0 -35
  166. package/dist/sync/materialize.d.ts.map +0 -1
  167. package/dist/sync/materialize.js.map +0 -1
  168. package/dist/sync/ops.d.ts +0 -142
  169. package/dist/sync/ops.d.ts.map +0 -1
  170. package/dist/sync/ops.js.map +0 -1
  171. package/dist/sync/space.d.ts +0 -116
  172. package/dist/sync/space.d.ts.map +0 -1
  173. package/dist/sync/space.js.map +0 -1
  174. package/dist/types/index.d.ts +0 -346
  175. package/dist/types/index.d.ts.map +0 -1
  176. package/dist/types/index.js.map +0 -1
  177. package/dist/utils/budget.d.ts +0 -44
  178. package/dist/utils/budget.d.ts.map +0 -1
  179. package/dist/utils/budget.js.map +0 -1
  180. package/dist/utils/fs.d.ts +0 -59
  181. package/dist/utils/fs.d.ts.map +0 -1
  182. package/dist/utils/fs.js.map +0 -1
  183. package/dist/utils/hash.d.ts +0 -20
  184. package/dist/utils/hash.d.ts.map +0 -1
  185. package/dist/utils/hash.js.map +0 -1
  186. package/dist/utils/ids.d.ts +0 -47
  187. package/dist/utils/ids.d.ts.map +0 -1
  188. package/dist/utils/ids.js.map +0 -1
  189. package/dist/utils/lock.d.ts +0 -11
  190. package/dist/utils/lock.d.ts.map +0 -1
  191. package/dist/utils/lock.js.map +0 -1
  192. package/dist/utils/transcripts.d.ts +0 -63
  193. package/dist/utils/transcripts.d.ts.map +0 -1
  194. package/dist/utils/transcripts.js.map +0 -1
  195. package/dist/version.d.ts +0 -26
  196. package/dist/version.d.ts.map +0 -1
  197. 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', '@huggingface/transformers@3'],
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 (command === 'init') {
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(console.error);
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(' 📡 Connected IDEs:');
468
+ console.log(' 📡 Clients found:');
254
469
  for (const ide of ides) {
255
- console.log(` ${ide.exists ? '✅' : '⚠️ '} ${ide.name}`);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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 await ep.listStates(root)) {
540
- const reply = await controlDaemon(root, 'status', st.build);
541
- if (reply) { out.push({ st, reply }); continue; }
542
- // No answer is not proof of death: a daemon in the middle of a rebuild
543
- // answers late, and deleting its state file would orphan it — alive,
544
- // holding the brain, and unreachable by every client. Only a pid the
545
- // system says is gone loses its file.
546
- let alive = true;
547
- try { process.kill(st.pid, 0); } catch (e) { alive = e.code === 'EPERM'; }
548
- if (alive) out.push({ st, reply: null });
549
- else { try { fs.rmSync(ep.stateFile(root, st.build), { force: true }); } catch { /* stale file, best effort */ } }
550
- }
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
- const lessons = lessonsFor(index, texto, 10);
623
- if (lessons.length === 0) { console.log(' Nothing stored mentions that command.'); return; }
624
- for (const l of lessons) console.log(` • [${l.k}${l.d ? ' · ' + l.d : ''} · ${l.n}] ${l.t}\n`);
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
- Promise.all([import('../dist/engine/hookverify.js'), import('../dist/engine/modinstall.js')]).then(([
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(homedir(), '.claude', 'crbro-hooks'),
682
- settingsPath: join(homedir(), '.claude', 'settings.json'),
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 settingsPath = join(homedir(), '.claude', 'settings.json');
711
- const hookDir = join(homedir(), '.claude', 'crbro-hooks');
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
- let settings = null;
718
- try {
719
- const raw = fs.readFileSync(settingsPath, 'utf8');
720
- settings = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
721
- } catch (e) {
722
- if (fs.existsSync(settingsPath)) {
723
- console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
724
- console.error(` ${e.message}`);
725
- process.exit(1);
726
- }
727
- }
728
- if (settings !== null && (typeof settings !== 'object' || Array.isArray(settings))) {
729
- console.error(` ❌ ${settingsPath} is not a JSON object — not touching it.`);
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 tmp = settingsPath + '.' + process.pid + '.tmp';
741
- fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
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(join(homedir(), '.claude'))) {
789
- console.log(' ⚪ ~/.claude not found: Claude Code is the only client with PreCompact, so nothing was wired.');
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
- fs.mkdirSync(hookDir, { recursive: true });
794
- fs.copyFileSync(join(here, '..', 'hooks', 'crbro-lifecycle.mjs'), hookScript);
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(e => { console.error(e); process.exit(1); });
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 ~/.claude/settings.json)');
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('fs').then(async fs => {
859
- const settingsPath = join(homedir(), '.claude', 'settings.json');
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(homedir(), '.claude', 'crbro-hooks');
862
- const hookScript = join(hookDir, 'crbro-guard.mjs');
863
- fs.mkdirSync(hookDir, { recursive: true });
864
- fs.copyFileSync(join(here, '..', 'hooks', 'crbro-guard.mjs'), hookScript);
865
-
866
- let settings = {};
867
- try {
868
- const raw = fs.readFileSync(settingsPath, 'utf8');
869
- settings = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
870
- } catch (e) {
871
- if (fs.existsSync(settingsPath)) {
872
- console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
873
- console.error(` ${e.message}`);
874
- process.exit(1);
875
- }
876
- }
877
- settings.hooks = settings.hooks || {};
878
- const list = settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
879
- if (JSON.stringify(list).includes('crbro-guard')) {
880
- console.log(' ✅ PreToolUse guard already installed. Script refreshed.');
881
- } else {
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
- const tmp = settingsPath + '.' + process.pid + '.tmp';
887
- fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
888
- fs.renameSync(tmp, settingsPath);
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 (!('error' in r)) console.log(` Trigger index: ${r.entries} lessons under ${r.keys} commands. Try: crbro guard "git push"`);
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(console.error);
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('fs').then(async fs => {
910
- const settingsPath = join(homedir(), '.claude', 'settings.json');
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
- const hookDir = join(homedir(), '.claude', 'crbro-hooks');
918
- const hookScript = join(hookDir, 'crbro-subagent.mjs');
919
- fs.mkdirSync(hookDir, { recursive: true });
920
- fs.copyFileSync(source, hookScript);
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 settings = {};
930
- try {
931
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8').replace(/^/, ''));
932
- } catch (e) {
933
- if (fs.existsSync(settingsPath)) {
934
- console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
935
- console.error(` ${e.message}`);
936
- process.exit(1);
937
- }
938
- }
939
-
940
- settings.hooks = settings.hooks || {};
941
- const list = settings.hooks.SubagentStart = settings.hooks.SubagentStart || [];
942
- const yaEsta = JSON.stringify(list).includes('crbro-subagent');
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 && !JSON.stringify(list).includes('CRBRO_SUBAGENT_INJECT')) {
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(console.error);
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 ~/.claude/settings.json — SessionStart runs a command whose
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 ~/.codex/hooks.json — SessionStart can call an MCP tool
1055
- // directly (type "mcp_tool"), so it does, AND keeps the same
1056
- // printed instruction as a second layer: the hook can fire
1057
- // before the MCP server has finished starting, and then the
1058
- // direct call is simply lost.
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 files that already exist are touched, and each one is merged, never
1061
- // rewritten. Idempotent: a second run reports and changes nothing.
1062
- import('fs').then(async fs => {
1063
- // One copy of the notice: the lifecycle hook prints the same text in its
1064
- // session-start mode, and `install-hooks --compact` swaps this entry for it.
1065
- const { BOOT_NOTICE: AVISO } = await import('../hooks/crbro-lifecycle.mjs');
1066
-
1067
- // printf on a shell, Write-Output on Windows PowerShell. One line, no
1068
- // external file: a path stored in a hook goes stale on the next update.
1069
- const cmdPosix = bootCommandPosix(AVISO);
1070
- const cmdWin = `powershell -NoProfile -Command ${JSON.stringify('Write-Output ' + JSON.stringify(AVISO))}`;
1071
- const MATCHER = 'startup|resume|clear|compact';
1072
-
1073
- const leerJson = (p) => {
1074
- if (!fs.existsSync(p)) return null;
1075
- try {
1076
- return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^/, ''));
1077
- } catch (e) {
1078
- console.error(` ❌ ${p} exists but could not be parsed — not touching it.`);
1079
- console.error(` ${e.message}`);
1080
- process.exit(1);
1081
- }
1082
- };
1083
- const escribirJson = (p, obj) => {
1084
- const tmp = p + '.' + process.pid + '.tmp';
1085
- fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
1086
- fs.renameSync(tmp, p);
1087
- };
1088
-
1089
- let tocados = 0, yaEstaban = 0, ausentes = [];
1090
-
1091
- // — Claude Code —
1092
- const claudePath = join(homedir(), '.claude', 'settings.json');
1093
- const claude = leerJson(claudePath) ?? (fs.existsSync(join(homedir(), '.claude')) ? {} : null);
1094
- if (claude === null) {
1095
- ausentes.push('Claude Code (~/.claude not found)');
1096
- } else {
1097
- claude.hooks = claude.hooks || {};
1098
- const list = claude.hooks.SessionStart = claude.hooks.SessionStart || [];
1099
- // Any mention of crbro counts as "already wired". A hand-rolled hook
1100
- // often points at a file — `cat ~/.claude/crbro-session-start.txt` — so
1101
- // looking for crbro_boot alone misses it and installs a second entry
1102
- // that boots the brain twice.
1103
- if (/crbro/i.test(JSON.stringify(list))) {
1104
- yaEstaban++;
1105
- console.log(' ⚪ Claude Code: SessionStart already starts CRBRO. Left alone.');
1106
- } else {
1107
- list.push({
1108
- matcher: MATCHER,
1109
- hooks: [{ type: 'command', command: cmdPosix, shell: 'bash', timeout: 10, statusMessage: 'Loading CRBRO memory...' }],
1110
- });
1111
- escribirJson(claudePath, claude);
1112
- tocados++;
1113
- console.log(` ✅ Claude Code: SessionStart hook added.\n ${claudePath}`);
1114
- }
1115
- }
1116
-
1117
- // — Codex —
1118
- const codexDir = join(homedir(), '.codex');
1119
- const codexPath = join(codexDir, 'hooks.json');
1120
- if (!fs.existsSync(codexDir)) {
1121
- ausentes.push('Codex (~/.codex not found)');
1122
- } else {
1123
- const codex = leerJson(codexPath) ?? {};
1124
- codex.hooks = codex.hooks || {};
1125
- const list = codex.hooks.SessionStart = codex.hooks.SessionStart || [];
1126
- // Any mention of crbro counts as "already wired". A hand-rolled hook
1127
- // often points at a file — `cat ~/.claude/crbro-session-start.txt` — so
1128
- // looking for crbro_boot alone misses it and installs a second entry
1129
- // that boots the brain twice.
1130
- if (/crbro/i.test(JSON.stringify(list))) {
1131
- yaEstaban++;
1132
- console.log(' ⚪ Codex: SessionStart already starts CRBRO. Left alone.');
1133
- } else {
1134
- list.push({
1135
- matcher: MATCHER,
1136
- hooks: [
1137
- { type: 'mcp_tool', server: 'crbro', tool: 'crbro_boot', input: {}, timeout: 30, statusMessage: 'Loading CRBRO memory...' },
1138
- { type: 'command', command: cmdPosix, commandWindows: cmdWin, timeout: 10, statusMessage: 'Checking CRBRO start...' },
1139
- ],
1140
- });
1141
- escribirJson(codexPath, codex);
1142
- tocados++;
1143
- console.log(` ✅ Codex: SessionStart hook added (MCP call + fallback).\n ${codexPath}`);
1144
- }
1145
- }
1146
-
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
- if (!tocados && !yaEstaban) {
1149
- console.log(' ⚠️ Neither ~/.claude nor ~/.codex was found, so nothing was wired.');
1150
- }
1151
- for (const a of ausentes) console.log(` ⚪ Skipped ${a}`);
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(` ${AVISO}`);
1308
+ console.log(` ${ctx.bootNotice}`);
1158
1309
  console.log('');
1159
- if (tocados) console.log(' Restart the tool, then check a new conversation actually boots CRBRO.');
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
- }).catch(console.error);
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
- console.error(' Usage: npx crbro-memory secret <set|get|list|remove|status> [NAME]');
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
- const keep = flag('--keep') ? Number(flag('--keep')) : undefined;
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
- import('../dist/index.js').catch(console.error);
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
  }