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
@@ -42,6 +42,7 @@ exports.modPaths = modPaths;
42
42
  exports.pathListSeparator = pathListSeparator;
43
43
  exports.sha256 = sha256;
44
44
  exports.modFiles = modFiles;
45
+ exports.staleStaging = staleStaging;
45
46
  exports.readModState = readModState;
46
47
  exports.compareVersions = compareVersions;
47
48
  exports.installMod = installMod;
@@ -51,13 +52,17 @@ exports.formatModVerify = formatModVerify;
51
52
  exports.modSwitchedOff = modSwitchedOff;
52
53
  exports.takeModNotice = takeModNotice;
53
54
  exports.autoInstallMod = autoInstallMod;
55
+ exports.hookRefreshSwitchedOff = hookRefreshSwitchedOff;
54
56
  exports.startModOnBoot = startModOnBoot;
57
+ exports.takeHooksNotice = takeHooksNotice;
55
58
  exports.modNoticeOnBoot = modNoticeOnBoot;
56
59
  exports.resetModOnBoot = resetModOnBoot;
57
60
  const crypto_1 = require("crypto");
58
61
  const fs_1 = require("fs");
59
62
  const os_1 = require("os");
60
63
  const path_1 = __importDefault(require("path"));
64
+ const installio_js_1 = require("./installio.js");
65
+ const hookrefresh_js_1 = require("./hookrefresh.js");
61
66
  exports.MOD_NAME = 'crbro-pending';
62
67
  /** The local copy this mod grew out of: a folder in the list holding it is replaced. */
63
68
  exports.LEGACY_NAME = 'crbro-pendientes';
@@ -69,8 +74,9 @@ exports.PLUGIN_DIRS_VAR = 'CLAUDE_CODE_PLUGIN_DIRS';
69
74
  /** What the package ships but the install leaves out: tests and editor config. */
70
75
  const NOT_SHIPPED_TOP = new Set(['test', 'tsconfig.json', '.gitignore', '.npmignore', 'node_modules']);
71
76
  const NOT_SHIPPED_DIRS = new Set(['.claude-plugin/types']);
72
- function modPaths(home) {
73
- const claudeDir = path_1.default.join(home, '.claude');
77
+ /** Claude Code's folder is CLAUDE_CONFIG_DIR when set, as Claude Code itself reads it; ~/.claude otherwise. */
78
+ function modPaths(home, env = process.env) {
79
+ const claudeDir = (0, installio_js_1.claudeDir)(home, env);
74
80
  return {
75
81
  claudeDir,
76
82
  settingsPath: path_1.default.join(claudeDir, 'settings.json'),
@@ -212,7 +218,7 @@ function readSettings(settingsPath) {
212
218
  return { ok: true, settings, indent, eol, hash };
213
219
  }
214
220
  /** Thrown by writeAtomic when settings.json changed after it was read: the caller merges again. */
215
- const SETTINGS_CHANGED = 'ESETTINGSCHANGED';
221
+ const SETTINGS_CHANGED = installio_js_1.FILE_CHANGED;
216
222
  /** Errors a later start may well not meet (a file held open, an antivirus scan): never written down as a failure. */
217
223
  const TRANSIENT = new Set(['EBUSY', 'EPERM', 'EACCES', 'ENOTEMPTY', 'EEXIST', SETTINGS_CHANGED]);
218
224
  /**
@@ -226,43 +232,37 @@ const TRANSIENT = new Set(['EBUSY', 'EPERM', 'EACCES', 'ENOTEMPTY', 'EEXIST', SE
226
232
  * inherited ACL, and a hard link to settings.json is not kept.
227
233
  */
228
234
  function writeAtomic(settingsPath, settings, indent = 2, eol = '\n', expectHash) {
229
- let target = settingsPath;
230
- try {
231
- if ((0, fs_1.lstatSync)(settingsPath).isSymbolicLink())
232
- target = (0, fs_1.realpathSync)(settingsPath);
233
- }
234
- catch {
235
- // Not there yet: written as a plain file.
236
- }
237
- let mode;
238
- try {
239
- mode = (0, fs_1.statSync)(target).mode & 0o777;
240
- }
241
- catch {
242
- mode = undefined;
243
- }
244
- const tmp = `${target}.${process.pid}.tmp`;
235
+ // installio.writeGuarded: the same steps (symlink target, mode, line
236
+ // endings, hash checked again right before the rename), plus a timestamped
237
+ // copy of the file as it was, kept beside it (settings.json.crbro-bak-<stamp>).
238
+ (0, installio_js_1.writeGuarded)(settingsPath, (0, installio_js_1.jsonText)(settings, indent, eol), { expectHash });
239
+ }
240
+ function splitList(value, sep) {
241
+ return (value ?? '').split(sep).map(s => s.trim()).filter(Boolean);
242
+ }
243
+ const STAGING = /^crbro-pending\.(\d+)\.(tmp|old)$/;
244
+ /**
245
+ * A staging or set-aside folder another run left behind, safe to remove: its
246
+ * owner's pid is gone (or it is this process's own). A live pid is another
247
+ * install under way — install-mod by hand beside a boot — and its folder is
248
+ * left alone, or that install would fail half way on its rename.
249
+ */
250
+ function staleStaging(name, isAlive = pidAlive) {
251
+ const m = STAGING.exec(name);
252
+ if (!m)
253
+ return false;
254
+ const pid = Number(m[1]);
255
+ return pid === process.pid || !isAlive(pid);
256
+ }
257
+ function pidAlive(pid) {
245
258
  try {
246
- let text = JSON.stringify(settings, null, indent) + '\n';
247
- if (eol !== '\n')
248
- text = text.replace(/\n/g, eol);
249
- (0, fs_1.writeFileSync)(tmp, text, mode === undefined ? 'utf8' : { encoding: 'utf8', mode });
250
- if (expectHash !== undefined && settingsFingerprint(target) !== expectHash) {
251
- const changed = new Error(`${settingsPath} changed while it was being written.`);
252
- changed.code = SETTINGS_CHANGED;
253
- throw changed;
254
- }
255
- (0, fs_1.renameSync)(tmp, target);
259
+ process.kill(pid, 0);
260
+ return true;
256
261
  }
257
262
  catch (e) {
258
- (0, fs_1.rmSync)(tmp, { force: true });
259
- throw e;
263
+ return e.code === 'EPERM';
260
264
  }
261
265
  }
262
- function splitList(value, sep) {
263
- return (value ?? '').split(sep).map(s => s.trim()).filter(Boolean);
264
- }
265
- const STAGING = /^crbro-pending\.\d+\.(tmp|old)$/;
266
266
  function copyFailed(installedDir, e) {
267
267
  const err = new Error(`could not replace ${installedDir} (${e.message}).\n` +
268
268
  ' Close the sessions and terminals using that folder and run it again.');
@@ -280,7 +280,7 @@ function copyMod(packageDir, installedDir) {
280
280
  (0, fs_1.mkdirSync)(parent, { recursive: true });
281
281
  // A staging or set-aside folder left by a run that failed half way: its pid is gone.
282
282
  for (const name of (0, fs_1.readdirSync)(parent)) {
283
- if (STAGING.test(name))
283
+ if (staleStaging(name))
284
284
  (0, fs_1.rmSync)(path_1.default.join(parent, name), { recursive: true, force: true, maxRetries: 3 });
285
285
  }
286
286
  const staging = `${installedDir}.${process.pid}.tmp`;
@@ -326,7 +326,7 @@ async function copyModAsync(packageDir, installedDir) {
326
326
  const parent = path_1.default.dirname(installedDir);
327
327
  await fs_1.promises.mkdir(parent, { recursive: true });
328
328
  for (const name of await fs_1.promises.readdir(parent)) {
329
- if (STAGING.test(name))
329
+ if (staleStaging(name))
330
330
  await fs_1.promises.rm(path_1.default.join(parent, name), { recursive: true, force: true, maxRetries: 3 });
331
331
  }
332
332
  const staging = `${installedDir}.${process.pid}.tmp`;
@@ -1290,33 +1290,67 @@ async function autoInstallMod(opts) {
1290
1290
  }
1291
1291
  }
1292
1292
  let onBoot = null;
1293
+ /** The refresh of installed hook copies (hookrefresh.ts), started beside the mod's install, once per process. */
1294
+ let hooksOnBoot = null;
1295
+ let hooksResult = null;
1296
+ let hooksNoticeTaken = false;
1297
+ /** CRBRO_HOOK_REFRESH=0 (off, false, no) keeps installed hook copies as they are. */
1298
+ function hookRefreshSwitchedOff(env = process.env) {
1299
+ return exports.MOD_OFF_VALUES.has(String(env.CRBRO_HOOK_REFRESH ?? '').trim().toLowerCase());
1300
+ }
1293
1301
  /**
1294
1302
  * What crbro_boot calls first: starts the automatic install the first time in
1295
1303
  * this process and returns the function to call once the boot has its answer
1296
1304
  * ready. That function waits for the install until `budgetMs` after the start
1297
1305
  * and takes the notice waiting to be said, or null. Neither throws. A boot
1298
1306
  * that fails never calls it, so it never takes a notice it cannot hand over.
1307
+ *
1308
+ * It also starts, once per process, the refresh of the hook copies CRBRO
1309
+ * installed (hookrefresh.ts): unmodified copies of an older release are
1310
+ * replaced by this one's. That part does not depend on CRBRO_MOD (it never
1311
+ * adds anything, only keeps current what is there); CRBRO_HOOK_REFRESH=0
1312
+ * turns it off. Its notice is taken with takeHooksNotice().
1299
1313
  */
1300
1314
  function startModOnBoot(opts = {}) {
1301
1315
  try {
1302
1316
  const home = opts.home ?? (0, os_1.homedir)();
1303
1317
  const env = opts.env ?? process.env;
1304
- if (modSwitchedOff(env) || !(0, fs_1.existsSync)(modPaths(home).claudeDir))
1305
- return async () => null;
1306
- if (!onBoot) {
1307
- const packageDir = opts.packageDir ?? path_1.default.join(__dirname, '..', '..', 'mods', exports.MOD_NAME);
1308
- onBoot = autoInstallMod({ ...opts, home, env, packageDir })
1309
- .catch(e => ({ action: 'failed', reason: e.message }));
1318
+ if (!hooksOnBoot && !hookRefreshSwitchedOff(env)) {
1319
+ hooksOnBoot = new Promise(resolve => setImmediate(() => {
1320
+ try {
1321
+ resolve((0, hookrefresh_js_1.refreshHooks)({ home, env }));
1322
+ }
1323
+ catch {
1324
+ resolve([]);
1325
+ }
1326
+ })).then(copies => { hooksResult = copies; return copies; });
1310
1327
  }
1311
- const running = onBoot;
1328
+ const hooks = hooksOnBoot ?? Promise.resolve([]);
1312
1329
  let timer;
1313
1330
  const budget = new Promise(resolve => {
1314
1331
  timer = setTimeout(resolve, opts.budgetMs ?? exports.BOOT_BUDGET_MS);
1315
1332
  timer.unref?.();
1316
1333
  });
1334
+ if (modSwitchedOff(env) || !(0, fs_1.existsSync)(modPaths(home, env).claudeDir)) {
1335
+ return async () => {
1336
+ try {
1337
+ await Promise.race([hooks, budget]);
1338
+ }
1339
+ catch { /* never fails the boot */ }
1340
+ if (timer)
1341
+ clearTimeout(timer);
1342
+ return null;
1343
+ };
1344
+ }
1345
+ if (!onBoot) {
1346
+ const packageDir = opts.packageDir ?? path_1.default.join(__dirname, '..', '..', 'mods', exports.MOD_NAME);
1347
+ onBoot = autoInstallMod({ ...opts, home, env, packageDir })
1348
+ .catch(e => ({ action: 'failed', reason: e.message }));
1349
+ }
1350
+ const running = onBoot;
1317
1351
  return async () => {
1318
1352
  try {
1319
- await Promise.race([running, budget]);
1353
+ await Promise.race([Promise.all([running, hooks]), budget]);
1320
1354
  if (timer)
1321
1355
  clearTimeout(timer);
1322
1356
  return takeModNotice(home, opts.now);
@@ -1330,6 +1364,22 @@ function startModOnBoot(opts = {}) {
1330
1364
  return async () => null;
1331
1365
  }
1332
1366
  }
1367
+ /**
1368
+ * The boot's notice about installed hooks, once per process, if the refresh
1369
+ * has finished: copies left alone because somebody edited them, or a refresh
1370
+ * that failed. Each edited copy is told once (remembered in its manifest).
1371
+ */
1372
+ function takeHooksNotice() {
1373
+ if (hooksNoticeTaken || !hooksResult)
1374
+ return null;
1375
+ hooksNoticeTaken = true;
1376
+ try {
1377
+ return (0, hookrefresh_js_1.hooksNotice)(hooksResult);
1378
+ }
1379
+ catch {
1380
+ return null;
1381
+ }
1382
+ }
1333
1383
  /** startModOnBoot and its notice in one call. */
1334
1384
  async function modNoticeOnBoot(opts = {}) {
1335
1385
  return startModOnBoot(opts)();
@@ -1337,6 +1387,9 @@ async function modNoticeOnBoot(opts = {}) {
1337
1387
  /** Tests only: the next boot in this process behaves as the first boot of a new process. */
1338
1388
  function resetModOnBoot() {
1339
1389
  onBoot = null;
1390
+ hooksOnBoot = null;
1391
+ hooksResult = null;
1392
+ hooksNoticeTaken = false;
1340
1393
  handedOver.clear();
1341
1394
  }
1342
1395
  //# sourceMappingURL=modinstall.js.map
@@ -3,9 +3,11 @@
3
3
  // Active context, hot topics, and global map (clustering + bridges)
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.Prefrontal = void 0;
6
+ exports.sortOpenItems = sortOpenItems;
6
7
  const fs_js_1 = require("../utils/fs.js");
7
8
  const hash_js_1 = require("../utils/hash.js");
8
9
  const secrets_js_1 = require("./secrets.js");
10
+ const heat_js_1 = require("./heat.js");
9
11
  /** How many resolved items to keep around as a reminder. */
10
12
  const RECENTLY_CLOSED_CAP = 15;
11
13
  /** Normalise a stored entry: v1 brains hold plain strings. */
@@ -15,6 +17,33 @@ function toTask(entry) {
15
17
  }
16
18
  return entry;
17
19
  }
20
+ function normalizeContext(ctx) {
21
+ return {
22
+ last_session: '', active_topics: [], last_updated: (0, fs_js_1.now)(), ...ctx,
23
+ pending_tasks: (ctx?.pending_tasks || []).map(toTask),
24
+ recently_closed: ctx?.recently_closed || [],
25
+ };
26
+ }
27
+ /**
28
+ * The one order open items are read in, whatever the scope: what is due
29
+ * first, then the newest. A cap applied after it keeps what matters now; the
30
+ * insertion order kept the oldest, so a boot without a project showed the
31
+ * twelve items nobody had touched in months.
32
+ */
33
+ function sortOpenItems(items) {
34
+ return [...items].sort((a, b) => (a.due || '9999').localeCompare(b.due || '9999') || String(b.added || '').localeCompare(String(a.added || '')));
35
+ }
36
+ function projectView(ctx, project) {
37
+ if (!project)
38
+ return ctx;
39
+ const visible = (t) => !t.project || t.project === project;
40
+ return {
41
+ ...ctx, active_topics: ctx.project_topics?.[project] || [],
42
+ project_topics: ctx.project_topics?.[project] ? { [project]: ctx.project_topics[project] } : {},
43
+ pending_tasks: sortOpenItems(ctx.pending_tasks.map(toTask).filter(visible)),
44
+ recently_closed: (ctx.recently_closed || []).filter(visible),
45
+ };
46
+ }
18
47
  class Prefrontal {
19
48
  brain;
20
49
  constructor(brain) {
@@ -24,21 +53,9 @@ class Prefrontal {
24
53
  /**
25
54
  * Get the current active context.
26
55
  */
27
- async getContext() {
56
+ async getContext(project) {
28
57
  const ctx = await (0, fs_js_1.readJSON)(this.brain.paths.activeContext());
29
- if (!ctx) {
30
- return {
31
- last_session: '',
32
- active_topics: [],
33
- pending_tasks: [],
34
- recently_closed: [],
35
- last_updated: (0, fs_js_1.now)(),
36
- };
37
- }
38
- // Upgrade v1 string entries in memory; they get written back on next update.
39
- ctx.pending_tasks = (ctx.pending_tasks || []).map(toTask);
40
- ctx.recently_closed = ctx.recently_closed || [];
41
- return ctx;
58
+ return projectView(normalizeContext(ctx), project);
42
59
  }
43
60
  /**
44
61
  * Update the active context.
@@ -48,76 +65,106 @@ class Prefrontal {
48
65
  * timestamp said "changed just now" about a context nobody had changed.
49
66
  */
50
67
  async updateContext(updates) {
51
- const ctx = await this.getContext();
68
+ if (updates.project && !/^[a-zA-Z0-9_-]{1,100}$/.test(updates.project))
69
+ throw new Error('Invalid project id.');
70
+ if (updates.due && (!/^\d{4}-\d{2}-\d{2}$/.test(updates.due) ||
71
+ !Number.isFinite(Date.parse(updates.due)) || new Date(updates.due).toISOString().slice(0, 10) !== updates.due)) {
72
+ throw new Error('Invalid due date; use a real YYYY-MM-DD calendar day.');
73
+ }
52
74
  const hayCambio = updates.clear === true ||
53
75
  updates.set_topics !== undefined ||
54
76
  (updates.add_pending !== undefined && updates.add_pending !== '') ||
55
77
  (updates.resolve_pending !== undefined && updates.resolve_pending !== '') ||
56
78
  (updates.discard_pending !== undefined && updates.discard_pending !== '');
57
79
  if (!hayCambio) {
58
- return { ...ctx, resolved: [], discarded: [], written: false };
59
- }
60
- if (updates.clear) {
61
- ctx.active_topics = [];
62
- ctx.pending_tasks = [];
63
- ctx.recently_closed = [];
80
+ return { ...await this.getContext(updates.project), resolved: [], discarded: [], written: false };
64
81
  }
65
- let tasks = ctx.pending_tasks.map(toTask);
66
82
  let resolved = [];
67
83
  let discarded = [];
68
84
  let redacted = [];
69
- if (updates.set_topics) {
70
- ctx.active_topics = updates.set_topics;
71
- }
72
- if (updates.add_pending) {
73
- // An open item is read back by crbro_boot in every session and copied
74
- // into compaction checkpoints: it goes through the same filter as learn.
75
- const filtrado = (0, secrets_js_1.redact)(updates.add_pending.trim());
76
- redacted = filtrado.found;
77
- const text = filtrado.text;
78
- const id = (0, hash_js_1.pendingId)(text);
79
- if (!tasks.some(t => t.id === id)) {
80
- tasks.push({ id, text, added: (0, fs_js_1.now)() });
85
+ let added;
86
+ const saved = await (0, fs_js_1.updateJSON)(this.brain.paths.activeContext(), current => {
87
+ const ctx = normalizeContext(current);
88
+ if (updates.clear) {
89
+ if (updates.project) {
90
+ delete ctx.project_topics?.[updates.project];
91
+ ctx.pending_tasks = ctx.pending_tasks.map(toTask).filter(t => t.project !== updates.project);
92
+ ctx.recently_closed = ctx.recently_closed?.filter(t => t.project !== updates.project);
93
+ }
94
+ else {
95
+ ctx.active_topics = [];
96
+ ctx.project_topics = {};
97
+ ctx.pending_tasks = [];
98
+ ctx.recently_closed = [];
99
+ }
81
100
  }
82
- }
83
- // Id, exact text, or a case-insensitive substring of at least 8 chars in
84
- // either direction — the same predicate for resolving and discarding.
85
- const matcher = (needleRaw) => {
86
- const needle = needleRaw.trim();
87
- const lower = needle.toLowerCase();
88
- return (t) => t.id === needle ||
89
- t.text === needle ||
90
- (lower.length >= 8 &&
91
- (t.text.toLowerCase().includes(lower) || lower.includes(t.text.toLowerCase())));
92
- };
93
- if (updates.resolve_pending) {
94
- const hit = matcher(updates.resolve_pending);
95
- resolved = tasks.filter(hit).map(t => ({ ...t, closed: (0, fs_js_1.now)() }));
96
- tasks = tasks.filter(t => !hit(t));
97
- if (resolved.length > 0) {
98
- ctx.recently_closed = [...resolved, ...(ctx.recently_closed || [])]
99
- .slice(0, RECENTLY_CLOSED_CAP);
101
+ let tasks = ctx.pending_tasks.map(toTask);
102
+ if (updates.set_topics) {
103
+ if (updates.project)
104
+ (ctx.project_topics ||= {})[updates.project] = updates.set_topics;
105
+ else
106
+ ctx.active_topics = updates.set_topics;
100
107
  }
101
- }
102
- if (updates.discard_pending) {
103
- const hit = matcher(updates.discard_pending);
104
- discarded = tasks.filter(hit);
105
- tasks = tasks.filter(t => !hit(t));
106
- }
107
- ctx.pending_tasks = tasks;
108
- ctx.last_updated = (0, fs_js_1.now)();
109
- await (0, fs_js_1.writeJSON)(this.brain.paths.activeContext(), ctx);
110
- return { ...ctx, resolved, discarded, written: true, ...(redacted.length ? { redacted } : {}) };
108
+ if (updates.add_pending) {
109
+ // An open item is read back by crbro_boot in every session and copied
110
+ // into compaction checkpoints: it goes through the same filter as learn.
111
+ const filtrado = (0, secrets_js_1.redact)(updates.add_pending.trim());
112
+ redacted = filtrado.found;
113
+ const text = filtrado.text;
114
+ const id = (0, hash_js_1.pendingId)(updates.project ? `${updates.project}\n${text}` : text);
115
+ const criterion = updates.done_when ? (0, secrets_js_1.redact)(updates.done_when) : undefined;
116
+ redacted.push(...(criterion?.found || []));
117
+ const existing = tasks.find(t => t.id === id);
118
+ const metadata = {
119
+ ...(updates.project ? { project: updates.project } : {}),
120
+ ...(updates.due ? { due: updates.due } : {}),
121
+ ...(criterion ? { done_when: criterion.text } : {}),
122
+ };
123
+ if (existing)
124
+ Object.assign(existing, metadata);
125
+ else
126
+ tasks.push({ id, text, added: (0, fs_js_1.now)(), status: 'open', ...metadata });
127
+ added = { ...(existing ?? tasks[tasks.length - 1]) };
128
+ }
129
+ // Id, exact text, or a case-insensitive substring of at least 8 chars in
130
+ // either direction — the same predicate for resolving and discarding.
131
+ const matcher = (needleRaw) => {
132
+ const needle = needleRaw.trim();
133
+ const lower = needle.toLowerCase();
134
+ return (t) => (!updates.project || !t.project || t.project === updates.project) && (t.id === needle ||
135
+ t.text === needle ||
136
+ (lower.length >= 8 &&
137
+ (t.text.toLowerCase().includes(lower) || lower.includes(t.text.toLowerCase()))));
138
+ };
139
+ if (updates.resolve_pending) {
140
+ const hit = matcher(updates.resolve_pending);
141
+ resolved = tasks.filter(hit).map(t => ({ ...t, closed: (0, fs_js_1.now)(), status: 'done' }));
142
+ tasks = tasks.filter(t => !hit(t));
143
+ if (resolved.length > 0) {
144
+ ctx.recently_closed = [...resolved, ...(ctx.recently_closed || [])]
145
+ .slice(0, RECENTLY_CLOSED_CAP);
146
+ }
147
+ }
148
+ if (updates.discard_pending) {
149
+ const hit = matcher(updates.discard_pending);
150
+ discarded = tasks.filter(hit);
151
+ tasks = tasks.filter(t => !hit(t));
152
+ }
153
+ ctx.pending_tasks = tasks;
154
+ ctx.last_updated = (0, fs_js_1.now)();
155
+ return ctx;
156
+ });
157
+ return { ...projectView(saved, updates.project), resolved, discarded, written: true,
158
+ ...(redacted.length ? { redacted } : {}), ...(added ? { added } : {}) };
111
159
  }
112
160
  /**
113
161
  * Record which session was logged last. Nothing else in the context moves.
114
162
  * Before this, nothing ever wrote last_session, so boot reported null forever.
115
163
  */
116
164
  async setLastSession(sessionId) {
117
- const ctx = await this.getContext();
118
- ctx.last_session = sessionId;
119
- ctx.last_updated = (0, fs_js_1.now)();
120
- await (0, fs_js_1.writeJSON)(this.brain.paths.activeContext(), ctx);
165
+ await (0, fs_js_1.updateJSON)(this.brain.paths.activeContext(), current => ({
166
+ ...normalizeContext(current), last_session: sessionId, last_updated: (0, fs_js_1.now)(),
167
+ }));
121
168
  }
122
169
  // ─── Hot Topics ─────────────────────────────────────────────────
123
170
  /**
@@ -147,10 +194,20 @@ class Prefrontal {
147
194
  async getGlobalMap() {
148
195
  const ids = await (0, fs_js_1.listJSONFiles)(this.brain.paths.cortex);
149
196
  const neurons = [];
197
+ // Heat lives in the derived heat state since 2.11; a neuron file keeps
198
+ // the value it had when heat stopped being written into it.
199
+ const calor = (await (0, heat_js_1.readHeatState)(this.brain))?.neurons || {};
150
200
  for (const id of ids) {
151
- const neuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
152
- if (neuron)
153
- neurons.push(neuron);
201
+ // 2.11 (C1): an unreadable neuron is skipped, not the end of maintenance or the map.
202
+ let neuron = null;
203
+ try {
204
+ neuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
205
+ }
206
+ catch { /* corrupt: skipped */ }
207
+ if (!neuron)
208
+ continue;
209
+ const h = calor[neuron.id]?.heat;
210
+ neurons.push(typeof h === 'number' ? { ...neuron, heat: h } : neuron);
154
211
  }
155
212
  // Build clusters by domain
156
213
  const domainGroups = {};
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ProjectRegistry = void 0;
7
+ exports.projectReference = projectReference;
8
+ // Identidad local explícita: un nombre de carpeta no identifica un proyecto.
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const hash_js_1 = require("../utils/hash.js");
11
+ const fs_js_1 = require("../utils/fs.js");
12
+ const secrets_js_1 = require("./secrets.js");
13
+ function projectReference(value) {
14
+ let ref = value.trim().replace(/\\/g, '/').replace(/\/+$/, '');
15
+ if (!ref || ref.length > 2048 || /[\x00-\x1f]/.test(ref))
16
+ throw new Error('Invalid project reference.');
17
+ if ((0, secrets_js_1.secretKinds)(ref).length)
18
+ throw new Error('Project references must not contain credentials.');
19
+ if (/^git@[^:]+:/.test(ref))
20
+ ref = ref.replace(/^git@([^:]+):/, 'https://$1/');
21
+ if (/^https?:\/\//i.test(ref)) {
22
+ const url = new URL(ref);
23
+ if (url.username || url.password || url.search || url.hash) {
24
+ throw new Error('Project URLs must not contain credentials, query parameters or fragments.');
25
+ }
26
+ ref = `${url.host.toLowerCase()}${url.pathname}`;
27
+ }
28
+ ref = ref.replace(/\.git$/, '').replace(/\/+$/, '');
29
+ if (/^[a-z]:\//i.test(ref))
30
+ ref = node_path_1.default.posix.normalize(ref).toLowerCase();
31
+ return ref;
32
+ }
33
+ class ProjectRegistry {
34
+ file;
35
+ constructor(brain) { this.file = node_path_1.default.join(brain.paths.root, 'projects.json'); }
36
+ async find(reference) {
37
+ const ref = projectReference(reference);
38
+ const data = await (0, fs_js_1.readJSON)(this.file);
39
+ return Object.values(data?.projects || {}).find(p => p.id === ref || p.aliases.includes(ref)) || null;
40
+ }
41
+ async boundNeurons() {
42
+ const data = await (0, fs_js_1.readJSON)(this.file);
43
+ return [...new Set(Object.values(data?.projects || {}).flatMap(p => p.neurons))];
44
+ }
45
+ async bind(reference, options) {
46
+ const ref = projectReference(reference);
47
+ const aliases = [...new Set([ref, ...(options.aliases || []).map(projectReference)])];
48
+ if (aliases.length > 32)
49
+ throw new Error('At most 32 project aliases.');
50
+ if (options.id && !/^[a-zA-Z0-9_-]{1,100}$/.test(options.id))
51
+ throw new Error('Invalid project id.');
52
+ if (options.neurons?.some(id => !/^[a-zA-Z0-9_-]{1,200}$/.test(id)))
53
+ throw new Error('Invalid neuron id.');
54
+ let result;
55
+ await (0, fs_js_1.updateJSON)(this.file, current => {
56
+ const data = current || { version: 1, projects: {} };
57
+ const existing = Object.values(data.projects).find(p => p.id === ref || p.aliases.includes(ref));
58
+ const id = options.id || existing?.id || `project_${(0, hash_js_1.contentHash)(ref, 16)}`;
59
+ for (const p of Object.values(data.projects)) {
60
+ if (p.id !== id && (aliases.includes(p.id) || p.aliases.includes(id) || p.aliases.some(a => aliases.includes(a)))) {
61
+ throw new Error('Project alias already belongs to another project; no assignment changed.');
62
+ }
63
+ }
64
+ const previous = data.projects[id];
65
+ const mergedAliases = [...new Set([...(previous?.aliases || []), ...aliases])];
66
+ if (mergedAliases.length > 32)
67
+ throw new Error('At most 32 project aliases.');
68
+ result = {
69
+ id, aliases: mergedAliases,
70
+ neurons: options.neurons === undefined ? previous?.neurons || [] : [...new Set(options.neurons)],
71
+ updated: (0, fs_js_1.now)(),
72
+ };
73
+ data.projects[id] = result;
74
+ return data;
75
+ });
76
+ return result;
77
+ }
78
+ }
79
+ exports.ProjectRegistry = ProjectRegistry;
80
+ //# sourceMappingURL=projects.js.map