crbro-memory 2.9.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +256 -456
- package/SECURITY.md +135 -8
- package/bin/crbro.mjs +555 -299
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +143 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +762 -311
- package/dist/engine/doctor.js +676 -0
- package/dist/engine/evidence.js +171 -0
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +50 -36
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +735 -144
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +126 -69
- package/dist/engine/projects.js +80 -0
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +34 -11
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +810 -176
- package/dist/search/quality.js +31 -0
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +649 -343
- package/dist/sync/git.js +203 -72
- package/dist/sync/materialize.js +33 -8
- package/dist/sync/space.js +286 -101
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +219 -52
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +12 -8
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -670
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -61
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -156
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -323
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -35
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -142
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -116
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -346
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -11
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -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
|
-
|
|
73
|
-
|
|
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 =
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 { ...
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|