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.
Files changed (205) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -465
  3. package/SECURITY.md +114 -12
  4. package/bin/crbro.mjs +539 -300
  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 +139 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +667 -232
  25. package/dist/engine/doctor.js +312 -14
  26. package/dist/engine/heat.js +81 -37
  27. package/dist/engine/hippocampus.js +18 -7
  28. package/dist/engine/hookrefresh.js +296 -0
  29. package/dist/engine/hookverify.js +15 -3
  30. package/dist/engine/installer.js +1022 -0
  31. package/dist/engine/installio.js +279 -0
  32. package/dist/engine/keychain.js +721 -162
  33. package/dist/engine/maintenance.js +355 -87
  34. package/dist/engine/modinstall.js +98 -45
  35. package/dist/engine/prefrontal.js +29 -6
  36. package/dist/engine/quarantine.js +204 -0
  37. package/dist/engine/secrets.js +41 -5
  38. package/dist/engine/shelf.js +25 -3
  39. package/dist/engine/synapses.js +183 -80
  40. package/dist/engine/triggers.js +141 -11
  41. package/dist/index.js +37 -3
  42. package/dist/mcp-surface.json +1 -0
  43. package/dist/miner/extractor.js +64 -17
  44. package/dist/miner/scheduler.js +144 -68
  45. package/dist/search/index.js +797 -176
  46. package/dist/search/semantic.js +208 -18
  47. package/dist/search/tokenize.js +44 -0
  48. package/dist/server.js +583 -373
  49. package/dist/sync/git.js +203 -72
  50. package/dist/sync/space.js +100 -25
  51. package/dist/sync/validate.js +204 -0
  52. package/dist/utils/budget.js +15 -2
  53. package/dist/utils/fs.js +40 -6
  54. package/dist/utils/ids.js +128 -14
  55. package/dist/utils/lock.js +94 -7
  56. package/hooks/crbro-codex.mjs +284 -0
  57. package/hooks/crbro-guard.mjs +281 -173
  58. package/hooks/crbro-hooks.shipped.json +20 -0
  59. package/hooks/crbro-lifecycle.mjs +16 -4
  60. package/hooks/crbro-subagent.mjs +180 -180
  61. package/package.json +9 -5
  62. package/dist/daemon/daemon.d.ts +0 -25
  63. package/dist/daemon/daemon.d.ts.map +0 -1
  64. package/dist/daemon/daemon.js.map +0 -1
  65. package/dist/daemon/endpoint.d.ts +0 -66
  66. package/dist/daemon/endpoint.d.ts.map +0 -1
  67. package/dist/daemon/endpoint.js.map +0 -1
  68. package/dist/daemon/lines.d.ts +0 -55
  69. package/dist/daemon/lines.d.ts.map +0 -1
  70. package/dist/daemon/lines.js.map +0 -1
  71. package/dist/daemon/proxy.d.ts +0 -55
  72. package/dist/daemon/proxy.d.ts.map +0 -1
  73. package/dist/daemon/proxy.js.map +0 -1
  74. package/dist/engine/backup.d.ts +0 -68
  75. package/dist/engine/backup.d.ts.map +0 -1
  76. package/dist/engine/backup.js.map +0 -1
  77. package/dist/engine/brain.d.ts +0 -79
  78. package/dist/engine/brain.d.ts.map +0 -1
  79. package/dist/engine/brain.js.map +0 -1
  80. package/dist/engine/cortex.d.ts +0 -682
  81. package/dist/engine/cortex.d.ts.map +0 -1
  82. package/dist/engine/cortex.js.map +0 -1
  83. package/dist/engine/dates.d.ts +0 -28
  84. package/dist/engine/dates.d.ts.map +0 -1
  85. package/dist/engine/dates.js.map +0 -1
  86. package/dist/engine/doctor.d.ts +0 -93
  87. package/dist/engine/doctor.d.ts.map +0 -1
  88. package/dist/engine/doctor.js.map +0 -1
  89. package/dist/engine/evidence.d.ts +0 -26
  90. package/dist/engine/evidence.d.ts.map +0 -1
  91. package/dist/engine/evidence.js.map +0 -1
  92. package/dist/engine/heat.d.ts +0 -28
  93. package/dist/engine/heat.d.ts.map +0 -1
  94. package/dist/engine/heat.js.map +0 -1
  95. package/dist/engine/hippocampus.d.ts +0 -60
  96. package/dist/engine/hippocampus.d.ts.map +0 -1
  97. package/dist/engine/hippocampus.js.map +0 -1
  98. package/dist/engine/hookverify.d.ts +0 -42
  99. package/dist/engine/hookverify.d.ts.map +0 -1
  100. package/dist/engine/hookverify.js.map +0 -1
  101. package/dist/engine/keychain.d.ts +0 -32
  102. package/dist/engine/keychain.d.ts.map +0 -1
  103. package/dist/engine/keychain.js.map +0 -1
  104. package/dist/engine/maintenance.d.ts +0 -218
  105. package/dist/engine/maintenance.d.ts.map +0 -1
  106. package/dist/engine/maintenance.js.map +0 -1
  107. package/dist/engine/modinstall.d.ts +0 -215
  108. package/dist/engine/modinstall.d.ts.map +0 -1
  109. package/dist/engine/modinstall.js.map +0 -1
  110. package/dist/engine/postmortem.d.ts +0 -81
  111. package/dist/engine/postmortem.d.ts.map +0 -1
  112. package/dist/engine/postmortem.js.map +0 -1
  113. package/dist/engine/prefrontal.d.ts +0 -64
  114. package/dist/engine/prefrontal.d.ts.map +0 -1
  115. package/dist/engine/prefrontal.js.map +0 -1
  116. package/dist/engine/projects.d.ts +0 -20
  117. package/dist/engine/projects.d.ts.map +0 -1
  118. package/dist/engine/projects.js.map +0 -1
  119. package/dist/engine/secrets.d.ts +0 -30
  120. package/dist/engine/secrets.d.ts.map +0 -1
  121. package/dist/engine/secrets.js.map +0 -1
  122. package/dist/engine/shelf.d.ts +0 -155
  123. package/dist/engine/shelf.d.ts.map +0 -1
  124. package/dist/engine/shelf.js.map +0 -1
  125. package/dist/engine/source.d.ts +0 -6
  126. package/dist/engine/source.d.ts.map +0 -1
  127. package/dist/engine/source.js.map +0 -1
  128. package/dist/engine/synapses.d.ts +0 -82
  129. package/dist/engine/synapses.d.ts.map +0 -1
  130. package/dist/engine/synapses.js.map +0 -1
  131. package/dist/engine/triggers.d.ts +0 -53
  132. package/dist/engine/triggers.d.ts.map +0 -1
  133. package/dist/engine/triggers.js.map +0 -1
  134. package/dist/engine/usage.d.ts +0 -61
  135. package/dist/engine/usage.d.ts.map +0 -1
  136. package/dist/engine/usage.js.map +0 -1
  137. package/dist/index.d.ts +0 -2
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/index.js.map +0 -1
  140. package/dist/miner/extractor.d.ts +0 -12
  141. package/dist/miner/extractor.d.ts.map +0 -1
  142. package/dist/miner/extractor.js.map +0 -1
  143. package/dist/miner/index.d.ts +0 -61
  144. package/dist/miner/index.d.ts.map +0 -1
  145. package/dist/miner/index.js.map +0 -1
  146. package/dist/miner/scheduler.d.ts +0 -30
  147. package/dist/miner/scheduler.d.ts.map +0 -1
  148. package/dist/miner/scheduler.js.map +0 -1
  149. package/dist/miner/types.d.ts +0 -11
  150. package/dist/miner/types.d.ts.map +0 -1
  151. package/dist/miner/types.js.map +0 -1
  152. package/dist/search/index.d.ts +0 -328
  153. package/dist/search/index.d.ts.map +0 -1
  154. package/dist/search/index.js.map +0 -1
  155. package/dist/search/quality.d.ts +0 -18
  156. package/dist/search/quality.d.ts.map +0 -1
  157. package/dist/search/quality.js.map +0 -1
  158. package/dist/search/semantic.d.ts +0 -80
  159. package/dist/search/semantic.d.ts.map +0 -1
  160. package/dist/search/semantic.js.map +0 -1
  161. package/dist/search/synonyms.d.ts +0 -2
  162. package/dist/search/synonyms.d.ts.map +0 -1
  163. package/dist/search/synonyms.js.map +0 -1
  164. package/dist/search/tokenize.d.ts +0 -39
  165. package/dist/search/tokenize.d.ts.map +0 -1
  166. package/dist/search/tokenize.js.map +0 -1
  167. package/dist/server.d.ts +0 -41
  168. package/dist/server.d.ts.map +0 -1
  169. package/dist/server.js.map +0 -1
  170. package/dist/sync/git.d.ts +0 -36
  171. package/dist/sync/git.d.ts.map +0 -1
  172. package/dist/sync/git.js.map +0 -1
  173. package/dist/sync/materialize.d.ts +0 -36
  174. package/dist/sync/materialize.d.ts.map +0 -1
  175. package/dist/sync/materialize.js.map +0 -1
  176. package/dist/sync/ops.d.ts +0 -147
  177. package/dist/sync/ops.d.ts.map +0 -1
  178. package/dist/sync/ops.js.map +0 -1
  179. package/dist/sync/space.d.ts +0 -118
  180. package/dist/sync/space.d.ts.map +0 -1
  181. package/dist/sync/space.js.map +0 -1
  182. package/dist/types/index.d.ts +0 -369
  183. package/dist/types/index.d.ts.map +0 -1
  184. package/dist/types/index.js.map +0 -1
  185. package/dist/utils/budget.d.ts +0 -44
  186. package/dist/utils/budget.d.ts.map +0 -1
  187. package/dist/utils/budget.js.map +0 -1
  188. package/dist/utils/fs.d.ts +0 -59
  189. package/dist/utils/fs.d.ts.map +0 -1
  190. package/dist/utils/fs.js.map +0 -1
  191. package/dist/utils/hash.d.ts +0 -20
  192. package/dist/utils/hash.d.ts.map +0 -1
  193. package/dist/utils/hash.js.map +0 -1
  194. package/dist/utils/ids.d.ts +0 -47
  195. package/dist/utils/ids.d.ts.map +0 -1
  196. package/dist/utils/ids.js.map +0 -1
  197. package/dist/utils/lock.d.ts +0 -13
  198. package/dist/utils/lock.d.ts.map +0 -1
  199. package/dist/utils/lock.js.map +0 -1
  200. package/dist/utils/transcripts.d.ts +0 -63
  201. package/dist/utils/transcripts.d.ts.map +0 -1
  202. package/dist/utils/transcripts.js.map +0 -1
  203. package/dist/version.d.ts +0 -26
  204. package/dist/version.d.ts.map +0 -1
  205. 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,9 @@ async function semanticBuild() {
168
364
  return { chunks, vectors: engine.semanticCount(), seconds: ((Date.now() - started) / 1000).toFixed(1) };
169
365
  }
170
366
 
171
- if (command === 'doctor') {
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(console.error);
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(' 📡 Connected IDEs:');
468
+ console.log(' 📡 Clients found:');
270
469
  for (const ide of ides) {
271
- 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'}`);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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(console.error);
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 await ep.listStates(root)) {
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
- const lessons = lessonsFor(index, texto, 10);
639
- if (lessons.length === 0) { console.log(' Nothing stored mentions that command.'); return; }
640
- 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`);
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
- 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(([
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(homedir(), '.claude', 'crbro-hooks'),
698
- settingsPath: join(homedir(), '.claude', 'settings.json'),
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 settingsPath = join(homedir(), '.claude', 'settings.json');
727
- 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');
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
- let settings = null;
734
- try {
735
- const raw = fs.readFileSync(settingsPath, 'utf8');
736
- settings = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
737
- } catch (e) {
738
- if (fs.existsSync(settingsPath)) {
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 tmp = settingsPath + '.' + process.pid + '.tmp';
757
- fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
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(join(homedir(), '.claude'))) {
805
- 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.`);
806
1017
  return;
807
1018
  }
808
1019
  const here = dirname(fileURLToPath(import.meta.url));
809
- fs.mkdirSync(hookDir, { recursive: true });
810
- 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
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(e => { console.error(e); process.exit(1); });
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 ~/.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;
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('fs').then(async fs => {
875
- 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');
876
1091
  const here = dirname(fileURLToPath(import.meta.url));
877
- const hookDir = join(homedir(), '.claude', 'crbro-hooks');
878
- const hookScript = join(hookDir, 'crbro-guard.mjs');
879
- fs.mkdirSync(hookDir, { recursive: true });
880
- fs.copyFileSync(join(here, '..', 'hooks', 'crbro-guard.mjs'), hookScript);
881
-
882
- let settings = {};
883
- try {
884
- const raw = fs.readFileSync(settingsPath, 'utf8');
885
- settings = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
886
- } catch (e) {
887
- if (fs.existsSync(settingsPath)) {
888
- console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
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
- const tmp = settingsPath + '.' + process.pid + '.tmp';
903
- fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
904
- 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 {
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 (!('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"`);
912
1124
  console.log(' It adds context, never blocks. New sessions pick it up.');
913
- }).catch(console.error);
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('fs').then(async fs => {
926
- 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');
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
- const hookDir = join(homedir(), '.claude', 'crbro-hooks');
934
- const hookScript = join(hookDir, 'crbro-subagent.mjs');
935
- fs.mkdirSync(hookDir, { recursive: true });
936
- 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
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 settings = {};
946
- try {
947
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8').replace(/^/, ''));
948
- } catch (e) {
949
- if (fs.existsSync(settingsPath)) {
950
- console.error(` ❌ ${settingsPath} exists but could not be parsed — not touching it.`);
951
- console.error(` ${e.message}`);
952
- process.exit(1);
953
- }
954
- }
955
-
956
- settings.hooks = settings.hooks || {};
957
- const list = settings.hooks.SubagentStart = settings.hooks.SubagentStart || [];
958
- 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); }
959
1179
  if (yaEsta) {
960
1180
  console.log(' ✅ SubagentStart hook already installed. Script refreshed.');
961
- if (conInyeccion && !JSON.stringify(list).includes('CRBRO_SUBAGENT_INJECT')) {
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(console.error);
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 ~/.claude/settings.json — SessionStart runs a command whose
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 ~/.codex/hooks.json — SessionStart can call an MCP tool
1071
- // directly (type "mcp_tool"), so it does, AND keeps the same
1072
- // printed instruction as a second layer: the hook can fire
1073
- // before the MCP server has finished starting, and then the
1074
- // 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.
1075
1285
  //
1076
- // Only files that already exist are touched, and each one is merged, never
1077
- // rewritten. Idempotent: a second run reports and changes nothing.
1078
- import('fs').then(async fs => {
1079
- // One copy of the notice: the lifecycle hook prints the same text in its
1080
- // session-start mode, and `install-hooks --compact` swaps this entry for it.
1081
- const { BOOT_NOTICE: AVISO } = await import('../hooks/crbro-lifecycle.mjs');
1082
-
1083
- // printf on a shell, Write-Output on Windows PowerShell. One line, no
1084
- // external file: a path stored in a hook goes stale on the next update.
1085
- const cmdPosix = bootCommandPosix(AVISO);
1086
- const cmdWin = `powershell -NoProfile -Command ${JSON.stringify('Write-Output ' + JSON.stringify(AVISO))}`;
1087
- const MATCHER = 'startup|resume|clear|compact';
1088
-
1089
- const leerJson = (p) => {
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
- if (!tocados && !yaEstaban) {
1165
- console.log(' ⚠️ Neither ~/.claude nor ~/.codex was found, so nothing was wired.');
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(` ${AVISO}`);
1308
+ console.log(` ${ctx.bootNotice}`);
1174
1309
  console.log('');
1175
- 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
+ }
1176
1314
  console.log('');
1177
- }).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);
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
- 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]');
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
- 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;
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
- 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'));
1454
1693
  }