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
|
@@ -0,0 +1,676 @@
|
|
|
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.BACKUP_FAIL_DAYS = exports.BACKUP_WARN_DAYS = void 0;
|
|
7
|
+
exports.diagnose = diagnose;
|
|
8
|
+
exports.findings = findings;
|
|
9
|
+
exports.formatDoctor = formatDoctor;
|
|
10
|
+
// Diagnóstico observacional: no construye motores, no arranca servidores y no
|
|
11
|
+
// abre el llavero. Las salidas se construyen por lista blanca, nunca volcados.
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const node_module_1 = require("node:module");
|
|
15
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const brain_js_1 = require("./brain.js");
|
|
18
|
+
const backup_js_1 = require("./backup.js");
|
|
19
|
+
const endpoint_js_1 = require("../daemon/endpoint.js");
|
|
20
|
+
const proxy_js_1 = require("../daemon/proxy.js");
|
|
21
|
+
const version_js_1 = require("../version.js");
|
|
22
|
+
const hookverify_js_1 = require("./hookverify.js");
|
|
23
|
+
const hookrefresh_js_1 = require("./hookrefresh.js");
|
|
24
|
+
const installio_js_1 = require("./installio.js");
|
|
25
|
+
async function readParsed(file) {
|
|
26
|
+
let text;
|
|
27
|
+
try {
|
|
28
|
+
text = await node_fs_1.promises.readFile(file, 'utf8');
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return { status: e.code === 'ENOENT' ? 'missing' : 'unreadable' };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return { status: 'ok', value: JSON.parse(text) };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { status: 'invalid' };
|
|
38
|
+
} // No error.message: it may quote a credential.
|
|
39
|
+
}
|
|
40
|
+
async function exists(file) {
|
|
41
|
+
try {
|
|
42
|
+
await node_fs_1.promises.stat(file);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return e.code === 'ENOENT' ? false : null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function stamp(value) {
|
|
50
|
+
if (typeof value !== 'string' || !Number.isFinite(Date.parse(value)))
|
|
51
|
+
return null;
|
|
52
|
+
return new Date(value).toISOString();
|
|
53
|
+
}
|
|
54
|
+
function version(value) {
|
|
55
|
+
return typeof value === 'string' && /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(value) ? value : null;
|
|
56
|
+
}
|
|
57
|
+
function canonical(file) {
|
|
58
|
+
const resolved = node_path_1.default.resolve(file);
|
|
59
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
60
|
+
}
|
|
61
|
+
async function inspectBrain(root) {
|
|
62
|
+
const invalid = [], unreadable = [];
|
|
63
|
+
const manifestFile = node_path_1.default.join(root, 'manifest.json');
|
|
64
|
+
const manifest = await readParsed(manifestFile);
|
|
65
|
+
let checked = 0;
|
|
66
|
+
const check = (file, result) => {
|
|
67
|
+
if (result.status === 'ok' || result.status === 'invalid')
|
|
68
|
+
checked++;
|
|
69
|
+
if (result.status === 'invalid')
|
|
70
|
+
invalid.push(file);
|
|
71
|
+
if (result.status === 'unreadable')
|
|
72
|
+
unreadable.push(file);
|
|
73
|
+
};
|
|
74
|
+
check(manifestFile, manifest);
|
|
75
|
+
if (manifest.status === 'ok' && (!manifest.value || typeof manifest.value !== 'object' || Array.isArray(manifest.value)))
|
|
76
|
+
invalid.push(manifestFile);
|
|
77
|
+
// Sólo JSON del cerebro; excluye llavero, .daemon, cuarentena y backups.
|
|
78
|
+
for (const name of ['cortex', 'synapses', 'hippocampus', 'prefrontal']) {
|
|
79
|
+
const dir = node_path_1.default.join(root, name);
|
|
80
|
+
try {
|
|
81
|
+
for (const entry of await node_fs_1.promises.readdir(dir, { withFileTypes: true })) {
|
|
82
|
+
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
83
|
+
continue;
|
|
84
|
+
const file = node_path_1.default.join(dir, entry.name);
|
|
85
|
+
check(file, await readParsed(file));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (e.code !== 'ENOENT')
|
|
90
|
+
unreadable.push(dir);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const status = invalid.length || unreadable.length ? 'error' : manifest.status === 'ok' ? 'ok' : 'unknown';
|
|
94
|
+
return { status, path: root, exists: await exists(root), json_files_checked: checked,
|
|
95
|
+
invalid_json_paths: invalid.sort(), unreadable_paths: unreadable.sort(),
|
|
96
|
+
last_boot: stamp(manifest.value?.last_boot), last_boot_scope: 'aggregate',
|
|
97
|
+
client_autorun: 'unknown' };
|
|
98
|
+
}
|
|
99
|
+
async function inspectVersions(root) {
|
|
100
|
+
const states = await (0, endpoint_js_1.listStates)(root);
|
|
101
|
+
const daemons = await Promise.all(states.map(async (state) => {
|
|
102
|
+
const unknown = { status: 'unknown', running_version: null,
|
|
103
|
+
observed_via: null };
|
|
104
|
+
// Sólo el socket/pipe local derivado por CRBRO; nunca un destino de red de
|
|
105
|
+
// una configuración arbitraria. No se publica el estado ni su token.
|
|
106
|
+
if (typeof state.build !== 'string' || state.endpoint !== (0, endpoint_js_1.endpointFor)(root, state.build)
|
|
107
|
+
|| typeof state.brain !== 'string' || canonical(state.brain) !== canonical(root))
|
|
108
|
+
return unknown;
|
|
109
|
+
try {
|
|
110
|
+
const reply = await (0, proxy_js_1.controlDaemon)(root, 'status', state.build);
|
|
111
|
+
if (reply?.crbro !== 'status' || reply.build !== state.build || typeof reply.brain !== 'string'
|
|
112
|
+
|| canonical(reply.brain) !== canonical(root) || !version(reply.version))
|
|
113
|
+
return unknown;
|
|
114
|
+
return { status: 'ok', running_version: version(reply.version), observed_via: 'authenticated_status' };
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return unknown;
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
return { installed: version((0, version_js_1.readPackageVersion)()), daemon_status: daemons.some(d => d.status === 'ok') ? 'ok' : 'unknown', daemons };
|
|
121
|
+
}
|
|
122
|
+
function configuredPath(value, home) {
|
|
123
|
+
if (typeof value !== 'string' || !value.trim() || /[\r\n\0]|\$\{|%\w+%|:\/\//.test(value))
|
|
124
|
+
return null;
|
|
125
|
+
return node_path_1.default.resolve((0, brain_js_1.resolveBrainDir)({ HOME: home, CRBRO_PATH: value }, () => undefined));
|
|
126
|
+
}
|
|
127
|
+
// Lector conservador de las tablas MCP de Codex; no ejecuta TOML ni devuelve
|
|
128
|
+
// comandos, argumentos, cabeceras o valores del resto de variables.
|
|
129
|
+
function codexEntries(text) {
|
|
130
|
+
const entries = new Map();
|
|
131
|
+
let current = null, inEnv = false, serverTable = false, uncertain = false, multiline = '';
|
|
132
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
133
|
+
if (multiline) {
|
|
134
|
+
if (raw.includes(multiline))
|
|
135
|
+
multiline = '';
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const multi = raw.match(/(?:^|=)\s*("""|''')/);
|
|
139
|
+
if (multi) {
|
|
140
|
+
if (current)
|
|
141
|
+
uncertain = true;
|
|
142
|
+
if (raw.indexOf(multi[1], raw.indexOf(multi[1]) + 3) < 0)
|
|
143
|
+
multiline = multi[1];
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// Elimina comentarios respetando las comillas de las rutas.
|
|
147
|
+
const line = (raw.match(/^(?:[^#"']|"(?:\\.|[^"\\])*"|'[^']*')*/)?.[0] ?? '').trim();
|
|
148
|
+
if (!line)
|
|
149
|
+
continue;
|
|
150
|
+
if (line.startsWith('[')) {
|
|
151
|
+
serverTable = /^\[\s*mcp_servers\s*\]$/.test(line);
|
|
152
|
+
const table = /^\[\s*mcp_servers\.(?:"([^"]+)"|'([^']+)'|([\w-]+))(?:\.(env))?\s*\]$/.exec(line);
|
|
153
|
+
const name = table ? table[1] || table[2] || table[3] : '';
|
|
154
|
+
current = null;
|
|
155
|
+
if (name.toLowerCase().includes('crbro')) {
|
|
156
|
+
if (!entries.has(name))
|
|
157
|
+
entries.set(name, { env: {} });
|
|
158
|
+
current = entries.get(name);
|
|
159
|
+
inEnv = table[4] === 'env';
|
|
160
|
+
}
|
|
161
|
+
else if (line.includes('mcp_servers') && line.includes('crbro'))
|
|
162
|
+
uncertain = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!current) {
|
|
166
|
+
if ((serverTable && /^['"]?[\w-]*crbro[\w-]*['"]?\s*=/.test(line)) || /^mcp_servers\s*=/.test(line))
|
|
167
|
+
uncertain = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const pair = /^([\w.]+)\s*=\s*(.*)$/.exec(line);
|
|
171
|
+
if (!pair) {
|
|
172
|
+
uncertain = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const [, key, value] = pair;
|
|
176
|
+
if (!inEnv && key === 'enabled') {
|
|
177
|
+
if (value === 'true' || value === 'false')
|
|
178
|
+
current.enabled = value === 'true';
|
|
179
|
+
else
|
|
180
|
+
uncertain = true;
|
|
181
|
+
}
|
|
182
|
+
if ((!inEnv && key === 'env.CRBRO_PATH') || (inEnv && key === 'CRBRO_PATH')) {
|
|
183
|
+
try {
|
|
184
|
+
current.env.CRBRO_PATH = value.startsWith("'") && value.endsWith("'") ? value.slice(1, -1) : JSON.parse(value);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
uncertain = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!inEnv && key === 'env')
|
|
191
|
+
uncertain = true; // tabla inline no interpretada
|
|
192
|
+
// Sólo para comprobaciones booleanas (existe, usa npx) y los plazos: nunca se devuelven.
|
|
193
|
+
if (!inEnv && key === 'command') {
|
|
194
|
+
const m = /^"((?:\\.|[^"\\])*)"$|^'([^']*)'$/.exec(value);
|
|
195
|
+
if (m) {
|
|
196
|
+
try {
|
|
197
|
+
current.command = m[2] ?? JSON.parse(`"${m[1]}"`);
|
|
198
|
+
}
|
|
199
|
+
catch { /* no interpretado */ }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!inEnv && key === 'args' && value.startsWith('[') && value.endsWith(']')) {
|
|
203
|
+
current.args = [...value.matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'/g)].map(x => {
|
|
204
|
+
try {
|
|
205
|
+
return x[2] ?? JSON.parse(`"${x[1]}"`);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return '';
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (!inEnv && (key === 'startup_timeout_sec' || key === 'tool_timeout_sec')) {
|
|
213
|
+
if (/^\+?\d+(\.\d+)?$/.test(value))
|
|
214
|
+
current[key] = Number(value);
|
|
215
|
+
else
|
|
216
|
+
uncertain = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { entries: [...entries.values()], uncertain: uncertain || !!multiline };
|
|
220
|
+
}
|
|
221
|
+
async function inspectConfigs(home, env, root, cwd) {
|
|
222
|
+
const desktop = process.platform === 'win32'
|
|
223
|
+
? node_path_1.default.join(env.APPDATA || node_path_1.default.join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
|
|
224
|
+
: process.platform === 'darwin' ? node_path_1.default.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
225
|
+
: node_path_1.default.join(env.XDG_CONFIG_HOME || node_path_1.default.join(home, '.config'), 'Claude', 'claude_desktop_config.json');
|
|
226
|
+
const locations = [
|
|
227
|
+
{ client: 'Claude Desktop', file: desktop },
|
|
228
|
+
{ client: 'Claude Code', file: (0, installio_js_1.claudeJson)(home, env) },
|
|
229
|
+
{ client: 'Cursor', file: node_path_1.default.join(home, '.cursor', 'mcp.json') },
|
|
230
|
+
{ client: 'Codex', file: node_path_1.default.join(env.CODEX_HOME || node_path_1.default.join(home, '.codex'), 'config.toml') },
|
|
231
|
+
];
|
|
232
|
+
if (cwd)
|
|
233
|
+
locations.push({ client: 'Claude Code (project)', file: node_path_1.default.join(cwd, '.mcp.json') }, { client: 'Cursor (project)', file: node_path_1.default.join(cwd, '.cursor', 'mcp.json') }, { client: 'Codex (project)', file: node_path_1.default.join(cwd, '.codex', 'config.toml') });
|
|
234
|
+
const clients = [];
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
for (const location of locations) {
|
|
237
|
+
if (seen.has(canonical(location.file)))
|
|
238
|
+
continue;
|
|
239
|
+
seen.add(canonical(location.file));
|
|
240
|
+
const row = { client: location.client, path: location.file, exists: await exists(location.file),
|
|
241
|
+
status: 'unknown', registered: null, enabled: null, brain_paths: [], inherited_brain_path: false, client_autorun: 'unknown',
|
|
242
|
+
uses_npx: null, command_exists: null, script_exists: null, startup_timeout_sec: null, tool_timeout_sec: null };
|
|
243
|
+
let problem = false;
|
|
244
|
+
let entries = [], uncertain = false;
|
|
245
|
+
try {
|
|
246
|
+
if (location.file.endsWith('.toml')) {
|
|
247
|
+
({ entries, uncertain } = codexEntries(await node_fs_1.promises.readFile(location.file, 'utf8')));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const parsed = await readParsed(location.file);
|
|
251
|
+
if (parsed.status !== 'ok' || !parsed.value || typeof parsed.value !== 'object') {
|
|
252
|
+
clients.push(row);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const servers = parsed.value.mcpServers;
|
|
256
|
+
if (servers !== undefined && (!servers || typeof servers !== 'object' || Array.isArray(servers)))
|
|
257
|
+
uncertain = true;
|
|
258
|
+
else
|
|
259
|
+
entries = Object.entries(servers || {})
|
|
260
|
+
.filter(([name]) => name.toLowerCase().includes('crbro')).map(([, server]) => server);
|
|
261
|
+
}
|
|
262
|
+
const malformed = entries.some(e => !e || typeof e !== 'object' || Array.isArray(e)
|
|
263
|
+
|| (e.enabled !== undefined && typeof e.enabled !== 'boolean')
|
|
264
|
+
|| (e.disabled !== undefined && typeof e.disabled !== 'boolean')
|
|
265
|
+
|| (e.env !== undefined && (!e.env || typeof e.env !== 'object' || Array.isArray(e.env))));
|
|
266
|
+
uncertain ||= malformed;
|
|
267
|
+
row.registered = entries.length ? true : uncertain ? null : false;
|
|
268
|
+
row.enabled = entries.length && !malformed ? entries.some(e => e.enabled !== false && e.disabled !== true) : null;
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const raw = entry?.env?.CRBRO_PATH;
|
|
271
|
+
if (raw === undefined)
|
|
272
|
+
row.inherited_brain_path = true;
|
|
273
|
+
else {
|
|
274
|
+
const resolved = configuredPath(raw, home);
|
|
275
|
+
if (resolved)
|
|
276
|
+
row.brain_paths.push(resolved);
|
|
277
|
+
else
|
|
278
|
+
uncertain = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// What starts it: booleans only, never the command line (it can carry a token).
|
|
282
|
+
const first = entries.find(e => e && typeof e === 'object' && !Array.isArray(e));
|
|
283
|
+
if (first && typeof first.command === 'string') {
|
|
284
|
+
const command = first.command;
|
|
285
|
+
const args = Array.isArray(first.args) ? first.args.filter((a) => typeof a === 'string') : [];
|
|
286
|
+
row.uses_npx = /(^|[\\/])npx(\.cmd|\.exe)?$/i.test(command);
|
|
287
|
+
row.command_exists = node_path_1.default.isAbsolute(command) ? !!(await exists(command)) : (0, installio_js_1.findOnPath)(command, env) !== null;
|
|
288
|
+
const script = args.find(a => node_path_1.default.isAbsolute(a) && /\.(mjs|cjs|js)$/i.test(a));
|
|
289
|
+
row.script_exists = script ? !!(await exists(script)) : null;
|
|
290
|
+
if (row.command_exists === false || row.script_exists === false)
|
|
291
|
+
problem = true;
|
|
292
|
+
}
|
|
293
|
+
if (location.file.endsWith('.toml') && first) {
|
|
294
|
+
row.startup_timeout_sec = typeof first.startup_timeout_sec === 'number' ? first.startup_timeout_sec : null;
|
|
295
|
+
row.tool_timeout_sec = typeof first.tool_timeout_sec === 'number' ? first.tool_timeout_sec : null;
|
|
296
|
+
if (row.startup_timeout_sec === null || row.tool_timeout_sec === null)
|
|
297
|
+
problem = true;
|
|
298
|
+
}
|
|
299
|
+
row.status = uncertain ? 'unknown' : problem ? 'warning' : 'ok';
|
|
300
|
+
}
|
|
301
|
+
catch { /* missing, unreadable or unsupported: unknown, no raw error */ }
|
|
302
|
+
clients.push(row);
|
|
303
|
+
}
|
|
304
|
+
const paths = new Map([[canonical(root), root]]);
|
|
305
|
+
for (const client of clients)
|
|
306
|
+
for (const file of client.brain_paths)
|
|
307
|
+
paths.set(canonical(file), file);
|
|
308
|
+
const distinct_brain_paths = [...paths.values()].sort();
|
|
309
|
+
return { status: distinct_brain_paths.length > 1 || clients.some(c => c.status === 'warning') ? 'warning'
|
|
310
|
+
: clients.some(c => c.status === 'unknown') ? 'unknown' : 'ok',
|
|
311
|
+
clients, distinct_brain_paths, different_brain_paths: distinct_brain_paths.length > 1,
|
|
312
|
+
inherited_environment: 'unknown' };
|
|
313
|
+
}
|
|
314
|
+
async function inspectSemantic(root, home, env) {
|
|
315
|
+
const setting = (env.CRBRO_SEMANTIC || '').toLowerCase();
|
|
316
|
+
const mode = ['0', 'off', 'false'].includes(setting) ? 'disabled' : ['1', 'on', 'true'].includes(setting) ? 'forced' : 'auto';
|
|
317
|
+
const semanticHome = node_path_1.default.resolve(env.CRBRO_SEMANTIC_HOME || node_path_1.default.join(home, '.crbro', '.semantic'));
|
|
318
|
+
const model = env.CRBRO_SEMANTIC_MODEL || 'Xenova/multilingual-e5-small';
|
|
319
|
+
let runtime_present = null;
|
|
320
|
+
try {
|
|
321
|
+
(0, node_module_1.createRequire)(node_path_1.default.join(semanticHome, 'package.json')).resolve('@huggingface/transformers');
|
|
322
|
+
runtime_present = true;
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
if (e.code === 'MODULE_NOT_FOUND')
|
|
326
|
+
runtime_present = false;
|
|
327
|
+
}
|
|
328
|
+
const model_cache_present = /^[\w.-]+\/[\w.-]+$/.test(model) && !model.split('/').includes('..')
|
|
329
|
+
? await exists(node_path_1.default.join(semanticHome, 'models', ...model.split('/'))) : null;
|
|
330
|
+
const meta = await readParsed(node_path_1.default.join(root, '.search', 'vectors.meta.json'));
|
|
331
|
+
let bytes = null;
|
|
332
|
+
try {
|
|
333
|
+
bytes = (await node_fs_1.promises.stat(node_path_1.default.join(root, '.search', 'vectors.f32'))).size;
|
|
334
|
+
}
|
|
335
|
+
catch { /* unobserved */ }
|
|
336
|
+
const m = meta.value;
|
|
337
|
+
const valid = meta.status === 'ok' && Number.isSafeInteger(m?.dim) && m.dim > 0 && Array.isArray(m.ids)
|
|
338
|
+
&& m.ids.every((id) => typeof id === 'string') && m.model === model
|
|
339
|
+
&& m.dtype === (env.CRBRO_SEMANTIC_DTYPE || 'q8') && bytes === m.ids.length * m.dim * 4;
|
|
340
|
+
const index = { status: valid ? 'ok' : meta.status === 'missing' && bytes === null ? 'unknown' : 'warning',
|
|
341
|
+
vectors: valid ? m.ids.length : null, metadata_present: meta.status !== 'missing', data_present: bytes !== null };
|
|
342
|
+
// A valid index is evidence it works; no index is no evidence (semantic recall not installed), not a fault.
|
|
343
|
+
return { status: index.status,
|
|
344
|
+
mode, home: semanticHome, runtime_present, model_cache_present, index, operation: 'not_probed' };
|
|
345
|
+
}
|
|
346
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
347
|
+
/** Newest backup older than this: a warning; older than BACKUP_FAIL_DAYS: an error. */
|
|
348
|
+
exports.BACKUP_WARN_DAYS = 2;
|
|
349
|
+
exports.BACKUP_FAIL_DAYS = 7;
|
|
350
|
+
async function inspectBackups(root, env, home, now) {
|
|
351
|
+
const directory = (0, backup_js_1.resolveBackupDir)(root, env, home);
|
|
352
|
+
const files = await (0, backup_js_1.listBackups)(directory);
|
|
353
|
+
const latest = files[0] ? { path: files[0].path, created: files[0].created, size_bytes: files[0].size_bytes } : null;
|
|
354
|
+
const age_days = latest ? Math.max(0, Math.floor((now - Date.parse(latest.created)) / DAY_MS)) : null;
|
|
355
|
+
const brainThere = await exists(node_path_1.default.join(root, 'manifest.json'));
|
|
356
|
+
// Automatic copies come from consolidate once a day. A consolidate newer than
|
|
357
|
+
// the newest copy by more than a day, with them switched on, means they fail.
|
|
358
|
+
const enabled = !['0', 'off', 'false', 'no'].includes(String(env.CRBRO_AUTOBACKUP ?? '').trim().toLowerCase());
|
|
359
|
+
const manifest = await readParsed(node_path_1.default.join(root, 'manifest.json'));
|
|
360
|
+
const lastConsolidation = stamp(manifest.value?.last_consolidation);
|
|
361
|
+
const failing = !enabled || !lastConsolidation ? null
|
|
362
|
+
: (latest === null || Date.parse(lastConsolidation) - Date.parse(latest.created) > DAY_MS) && now - Date.parse(lastConsolidation) < exports.BACKUP_FAIL_DAYS * DAY_MS;
|
|
363
|
+
const status = !brainThere ? 'unknown'
|
|
364
|
+
: age_days === null ? 'warning'
|
|
365
|
+
: age_days > exports.BACKUP_FAIL_DAYS ? 'error'
|
|
366
|
+
: age_days > exports.BACKUP_WARN_DAYS || failing ? 'warning' : 'ok';
|
|
367
|
+
return { status, directory, latest, age_days, warn_after_days: exports.BACKUP_WARN_DAYS, fail_after_days: exports.BACKUP_FAIL_DAYS,
|
|
368
|
+
automatic: { enabled, last_consolidation: lastConsolidation, failing },
|
|
369
|
+
integrity: 'not_probed' };
|
|
370
|
+
}
|
|
371
|
+
/** A pid that exists right now (signal 0 checks, it sends nothing). */
|
|
372
|
+
function pidAlive(pid) {
|
|
373
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
374
|
+
return null;
|
|
375
|
+
try {
|
|
376
|
+
process.kill(pid, 0);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
return e.code === 'EPERM';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function inspectHooks(home, env) {
|
|
384
|
+
const pkg = (0, hookrefresh_js_1.packageHooksDir)();
|
|
385
|
+
// dryRun: compares and reports, writes nothing.
|
|
386
|
+
const copies = (0, hookrefresh_js_1.refreshHooks)({ home, env, pkgHooks: pkg, dryRun: true });
|
|
387
|
+
const dirs = [
|
|
388
|
+
{ client: 'claude-code', dir: (0, installio_js_1.claudeDir)(home, env), settings: 'settings.json' },
|
|
389
|
+
{ client: 'codex', dir: (0, installio_js_1.codexHome)(home, env), settings: 'hooks.json' },
|
|
390
|
+
];
|
|
391
|
+
const missing_scripts = [];
|
|
392
|
+
let settings_unreadable = false;
|
|
393
|
+
let npx_cache_references = 0;
|
|
394
|
+
for (const d of dirs) {
|
|
395
|
+
const settingsPath = node_path_1.default.join(d.dir, d.settings);
|
|
396
|
+
if (!(await exists(settingsPath)))
|
|
397
|
+
continue;
|
|
398
|
+
const v = (0, hookverify_js_1.verifyHooks)({ packageDir: pkg, installedDir: node_path_1.default.join(d.dir, 'crbro-hooks'), settingsPath });
|
|
399
|
+
missing_scripts.push(...v.missing_scripts);
|
|
400
|
+
settings_unreadable ||= v.settings_unreadable;
|
|
401
|
+
const parsed = await readParsed(settingsPath);
|
|
402
|
+
for (const list of Object.values(parsed.value?.hooks ?? {})) {
|
|
403
|
+
for (const entry of Array.isArray(list) ? list : []) {
|
|
404
|
+
for (const h of Array.isArray(entry?.hooks) ? entry.hooks : []) {
|
|
405
|
+
const cmd = `${h?.command ?? ''} ${h?.commandWindows ?? ''}`;
|
|
406
|
+
if (/crbro/i.test(cmd) && /[\\/]_npx[\\/]/.test(cmd))
|
|
407
|
+
npx_cache_references++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const installed = copies.map(c => ({ client: c.client, file: node_path_1.default.join(c.dir, c.name),
|
|
413
|
+
state: c.state === 'stale' ? 'older_release' : c.state }));
|
|
414
|
+
const status = missing_scripts.length || settings_unreadable || npx_cache_references
|
|
415
|
+
|| installed.some(c => c.state === 'older_release' || c.state === 'modified') ? 'warning' : 'ok';
|
|
416
|
+
return { status, installed, missing_scripts: [...new Set(missing_scripts)].sort(), settings_unreadable, npx_cache_references };
|
|
417
|
+
}
|
|
418
|
+
async function inspectDaemonLeftovers(root, now) {
|
|
419
|
+
const dir = node_path_1.default.join(root, '.daemon');
|
|
420
|
+
let names = [];
|
|
421
|
+
try {
|
|
422
|
+
names = await node_fs_1.promises.readdir(dir);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return { status: 'ok', directory: dir, descriptors: 0, dead_descriptors: 0, orphan_tmp: 0 };
|
|
426
|
+
}
|
|
427
|
+
let descriptors = 0, dead = 0, orphan = 0;
|
|
428
|
+
for (const n of names) {
|
|
429
|
+
const file = node_path_1.default.join(dir, n);
|
|
430
|
+
if (/\.json$/.test(n)) {
|
|
431
|
+
descriptors++;
|
|
432
|
+
const st = await readParsed(file);
|
|
433
|
+
if (st.status === 'ok' && pidAlive(st.value?.pid) === false)
|
|
434
|
+
dead++;
|
|
435
|
+
}
|
|
436
|
+
else if (/\.tmp$/.test(n)) {
|
|
437
|
+
// A write that never finished: its writer is gone, or it is older than an hour.
|
|
438
|
+
const pid = Number(/\.(\d+)\.tmp$/.exec(n)?.[1]);
|
|
439
|
+
let old = false;
|
|
440
|
+
try {
|
|
441
|
+
old = now - (await node_fs_1.promises.stat(file)).mtimeMs > 60 * 60 * 1000;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (old || pidAlive(pid) === false)
|
|
447
|
+
orphan++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return { status: (dead || orphan ? 'warning' : 'ok'), directory: dir, descriptors, dead_descriptors: dead, orphan_tmp: orphan };
|
|
451
|
+
}
|
|
452
|
+
async function inspectQuarantine(root, env, now) {
|
|
453
|
+
const dir = node_path_1.default.join(root, '.quarantine');
|
|
454
|
+
const days = Number(env.CRBRO_QUARANTINE_DAYS ?? 180);
|
|
455
|
+
const retention_days = Number.isFinite(days) && days >= 0 ? days : 180;
|
|
456
|
+
let files = 0, bytes = 0, oldest = null;
|
|
457
|
+
const walk = async (d) => {
|
|
458
|
+
let entries;
|
|
459
|
+
try {
|
|
460
|
+
entries = await node_fs_1.promises.readdir(d, { withFileTypes: true });
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
for (const e of entries) {
|
|
466
|
+
const p = node_path_1.default.join(d, e.name);
|
|
467
|
+
if (e.isDirectory()) {
|
|
468
|
+
await walk(p);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const st = await node_fs_1.promises.stat(p);
|
|
473
|
+
files++;
|
|
474
|
+
bytes += st.size;
|
|
475
|
+
oldest = oldest === null ? st.mtimeMs : Math.min(oldest, st.mtimeMs);
|
|
476
|
+
}
|
|
477
|
+
catch { /* gone */ }
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
await walk(dir);
|
|
481
|
+
const oldest_days = oldest === null ? null : Math.floor((now - oldest) / DAY_MS);
|
|
482
|
+
const status = (retention_days > 0 && oldest_days !== null && oldest_days > retention_days) || bytes > 500 * 1024 * 1024 ? 'warning' : 'ok';
|
|
483
|
+
return { status, directory: dir, files, bytes, oldest_days, retention_days };
|
|
484
|
+
}
|
|
485
|
+
async function inspectTriggers(root, home, env) {
|
|
486
|
+
const file = node_path_1.default.join(root, '.search', 'triggers.json');
|
|
487
|
+
const parsed = await readParsed(file);
|
|
488
|
+
const settings = await readParsed(node_path_1.default.join((0, installio_js_1.claudeDir)(home, env), 'settings.json'));
|
|
489
|
+
const guard_wired = /crbro-guard/.test(JSON.stringify(settings.value?.hooks ?? {}));
|
|
490
|
+
const v = parsed.value;
|
|
491
|
+
const valid = parsed.status === 'ok' && v && typeof v === 'object' && v.keys && typeof v.keys === 'object' && Array.isArray(v.entries);
|
|
492
|
+
const version = valid && Number.isSafeInteger(v.v) ? v.v : null;
|
|
493
|
+
const status = parsed.status === 'invalid' || parsed.status === 'unreadable' ? 'warning'
|
|
494
|
+
: parsed.status === 'missing' ? (guard_wired ? 'warning' : 'ok')
|
|
495
|
+
: !valid || ![2, 3].includes(version ?? -1) ? 'warning' : 'ok';
|
|
496
|
+
return { status, path: file, exists: parsed.status !== 'missing', version, built: valid ? stamp(v.built) : null,
|
|
497
|
+
entries: valid ? v.entries.length : null, guard_wired };
|
|
498
|
+
}
|
|
499
|
+
function gitRead(dir, args, env) {
|
|
500
|
+
return new Promise(resolve => {
|
|
501
|
+
(0, node_child_process_1.execFile)('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-c', 'core.untrackedCache=false', '-C', dir, ...args], {
|
|
502
|
+
env: { ...env, GIT_OPTIONAL_LOCKS: '0', GIT_NO_LAZY_FETCH: '1', GIT_TERMINAL_PROMPT: '0' },
|
|
503
|
+
encoding: 'utf8', timeout: 5_000, maxBuffer: 1024 * 1024, windowsHide: true,
|
|
504
|
+
}, (err, stdout) => resolve(err ? null : stdout)); // stderr may contain a remote credential.
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async function inspectTeams(root, env) {
|
|
508
|
+
const shared = node_path_1.default.join(root, 'shared');
|
|
509
|
+
const spaces = [];
|
|
510
|
+
let entries;
|
|
511
|
+
try {
|
|
512
|
+
entries = await node_fs_1.promises.readdir(shared, { withFileTypes: true });
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return { status: 'unknown', spaces, remote_state: 'not_fetched' };
|
|
516
|
+
}
|
|
517
|
+
for (const entry of entries) {
|
|
518
|
+
if (!entry.isDirectory())
|
|
519
|
+
continue;
|
|
520
|
+
const dir = node_path_1.default.join(shared, entry.name);
|
|
521
|
+
const row = { path: dir, status: 'unknown', dirty: null, worktree_state: 'not_probed',
|
|
522
|
+
ahead: null, behind: null, last_commit_at: null, remote_state: 'not_fetched' };
|
|
523
|
+
// Never accidentally report the enclosing project repository.
|
|
524
|
+
if (await exists(node_path_1.default.join(dir, '.git'))) {
|
|
525
|
+
// No inspeccionar contenido del worktree: incluso git status puede
|
|
526
|
+
// ejecutar filtros clean/process configurados por el repositorio.
|
|
527
|
+
// Sólo leer historia; dirty queda desconocido, no limpio por omisión.
|
|
528
|
+
const [ahead, behind, log] = await Promise.all([
|
|
529
|
+
gitRead(dir, ['rev-list', '--count', '@{upstream}..HEAD'], env),
|
|
530
|
+
gitRead(dir, ['rev-list', '--count', 'HEAD..@{upstream}'], env),
|
|
531
|
+
gitRead(dir, ['log', '-1', '--no-patch', '--no-show-signature', '--no-ext-diff', '--no-textconv', '--format=%cI'], env),
|
|
532
|
+
]);
|
|
533
|
+
const count = (s) => s !== null && /^\d+\s*$/.test(s) ? Number(s.trim()) : null;
|
|
534
|
+
row.ahead = count(ahead);
|
|
535
|
+
row.behind = count(behind);
|
|
536
|
+
row.last_commit_at = stamp(log?.trim());
|
|
537
|
+
row.status = row.ahead === null || row.behind === null ? 'unknown'
|
|
538
|
+
: row.ahead || row.behind ? 'warning' : 'unknown';
|
|
539
|
+
}
|
|
540
|
+
spaces.push(row);
|
|
541
|
+
}
|
|
542
|
+
return { status: spaces.some(s => s.status === 'warning') ? 'warning' : spaces.some(s => s.status === 'unknown') ? 'unknown' : 'ok',
|
|
543
|
+
spaces, remote_state: 'not_fetched' };
|
|
544
|
+
}
|
|
545
|
+
/** Reads local evidence only. Supplying env replaces, rather than merges, the process environment. */
|
|
546
|
+
async function diagnose(options = {}) {
|
|
547
|
+
const env = options.env ?? process.env;
|
|
548
|
+
const home = node_path_1.default.resolve(options.homeDir || env.HOME || env.USERPROFILE || node_os_1.default.homedir());
|
|
549
|
+
const root = node_path_1.default.resolve(options.brainDir || (0, brain_js_1.resolveBrainDir)({ ...env, HOME: home }, () => undefined));
|
|
550
|
+
const now = options.now ?? Date.now();
|
|
551
|
+
const [brain, versions, configs, semantic, backups, teams, hooks, daemon_leftovers, quarantine, triggers] = await Promise.all([
|
|
552
|
+
inspectBrain(root), inspectVersions(root), inspectConfigs(home, env, root, options.cwd),
|
|
553
|
+
inspectSemantic(root, home, env), inspectBackups(root, env, home, now), inspectTeams(root, env),
|
|
554
|
+
inspectHooks(home, env), inspectDaemonLeftovers(root, now), inspectQuarantine(root, env, now), inspectTriggers(root, home, env),
|
|
555
|
+
]);
|
|
556
|
+
// A problem anywhere decides; otherwise a brain that reads well is "ok".
|
|
557
|
+
// What could not be observed (no daemon running, semantic not installed, a
|
|
558
|
+
// client not installed) is listed as not checked, never as a fault.
|
|
559
|
+
const statuses = [brain.status, configs.status, semantic.status, backups.status, teams.status,
|
|
560
|
+
hooks.status, daemon_leftovers.status, quarantine.status, triggers.status];
|
|
561
|
+
const status = statuses.includes('error') ? 'error' : statuses.includes('warning') ? 'warning'
|
|
562
|
+
: brain.status === 'ok' ? 'ok' : 'unknown';
|
|
563
|
+
return { schema_version: 1, read_only: true, status, versions, brain, configs, semantic, backups, teams,
|
|
564
|
+
hooks, daemon_leftovers, quarantine, triggers };
|
|
565
|
+
}
|
|
566
|
+
/** What the report means, in three lists, each with the command that fixes it. */
|
|
567
|
+
function findings(report) {
|
|
568
|
+
const out = [];
|
|
569
|
+
const add = (level, text, action) => out.push({ level, text, action });
|
|
570
|
+
const b = report.brain;
|
|
571
|
+
if (b.exists === false)
|
|
572
|
+
add('unchecked', `no brain at ${b.path} yet`, 'npx crbro-memory init (or start any client: the first boot creates it)');
|
|
573
|
+
for (const f of b.invalid_json_paths)
|
|
574
|
+
add('problem', `invalid JSON: ${f}`, 'restore that file from a backup: npx crbro-memory backup list');
|
|
575
|
+
for (const f of b.unreadable_paths)
|
|
576
|
+
add('problem', `unreadable: ${f}`, 'check its permissions');
|
|
577
|
+
const k = report.backups;
|
|
578
|
+
if (k.status === 'error')
|
|
579
|
+
add('problem', `the newest backup is ${k.age_days} days old (${k.latest?.path})`, 'npx crbro-memory backup');
|
|
580
|
+
else if (k.status === 'warning' && k.latest === null)
|
|
581
|
+
add('warning', 'no backup yet', 'npx crbro-memory backup');
|
|
582
|
+
else if (k.status === 'warning')
|
|
583
|
+
add('warning', `the newest backup is ${k.age_days} day(s) old`, 'npx crbro-memory backup');
|
|
584
|
+
if (k.automatic.failing)
|
|
585
|
+
add('warning', `automatic backups look like they fail: consolidate ran on ${k.automatic.last_consolidation}, the newest copy is older`, `check that ${k.directory} is writable, then npx crbro-memory backup`);
|
|
586
|
+
for (const c of report.configs.clients) {
|
|
587
|
+
if (!c.registered)
|
|
588
|
+
continue;
|
|
589
|
+
if (c.command_exists === false)
|
|
590
|
+
add('warning', `${c.client}: the command its CRBRO registration runs does not exist (${c.path})`, 'npx crbro-memory install');
|
|
591
|
+
if (c.script_exists === false)
|
|
592
|
+
add('warning', `${c.client}: the script its CRBRO registration runs does not exist (${c.path})`, 'npx crbro-memory install');
|
|
593
|
+
if (c.client.startsWith('Codex') && (c.startup_timeout_sec === null || c.tool_timeout_sec === null)) {
|
|
594
|
+
add('warning', `${c.client}: [mcp_servers.crbro] has no ${c.startup_timeout_sec === null ? 'startup_timeout_sec' : 'tool_timeout_sec'}: a slow start loses the memory (${c.path})`, 'npx crbro-memory install-boot');
|
|
595
|
+
}
|
|
596
|
+
if (c.uses_npx)
|
|
597
|
+
add('unchecked', `${c.client} starts CRBRO through npx (works; a stable engine starts faster and keeps one daemon)`, 'npx crbro-memory install');
|
|
598
|
+
}
|
|
599
|
+
if (report.configs.different_brain_paths)
|
|
600
|
+
add('warning', `clients point at different brains: ${report.configs.distinct_brain_paths.join(', ')}`, 'set the same CRBRO_PATH in every client');
|
|
601
|
+
const h = report.hooks;
|
|
602
|
+
for (const c of h.installed) {
|
|
603
|
+
if (c.state === 'older_release')
|
|
604
|
+
add('warning', `hook from an older CRBRO: ${c.file}`, 'refreshed at the next crbro_boot, or now: npx crbro-memory install-hooks …');
|
|
605
|
+
if (c.state === 'modified')
|
|
606
|
+
add('warning', `hook edited by hand, not refreshed: ${c.file}`, 'keep it, or take this version (backup kept): run the install-hooks you used again');
|
|
607
|
+
}
|
|
608
|
+
for (const s of h.missing_scripts)
|
|
609
|
+
add('warning', `a hook runs ${s}, which does not exist: it fails in every session`, 'npx crbro-memory install-boot, or remove that hook');
|
|
610
|
+
if (h.npx_cache_references)
|
|
611
|
+
add('warning', `${h.npx_cache_references} CRBRO hook(s) run out of the npx cache, which disappears when it is cleaned`, 'npx crbro-memory install');
|
|
612
|
+
if (h.settings_unreadable)
|
|
613
|
+
add('warning', 'a client settings file with hooks is not valid JSON', 'fix the JSON by hand');
|
|
614
|
+
const d = report.daemon_leftovers;
|
|
615
|
+
if (d.dead_descriptors)
|
|
616
|
+
add('warning', `${d.dead_descriptors} daemon descriptor(s) of processes that are gone in ${d.directory}`, 'npx crbro-memory daemon status (removes them)');
|
|
617
|
+
if (d.orphan_tmp)
|
|
618
|
+
add('warning', `${d.orphan_tmp} unfinished temp file(s) in ${d.directory}`, 'safe to delete while no daemon starts');
|
|
619
|
+
const q = report.quarantine;
|
|
620
|
+
if (q.status === 'warning')
|
|
621
|
+
add('warning', `quarantine holds ${q.files} file(s), ${(q.bytes / 1048576).toFixed(1)} MB, the oldest ${q.oldest_days} day(s) old`, `review ${q.directory}`);
|
|
622
|
+
const t = report.triggers;
|
|
623
|
+
if (t.status === 'warning')
|
|
624
|
+
add('warning', t.exists ? `the guard's trigger index (${t.path}) is unreadable or an old format: the guard says nothing` : 'the guard is installed but its trigger index does not exist yet', 'npx crbro-memory guard --rebuild');
|
|
625
|
+
if (report.semantic.index.status === 'warning')
|
|
626
|
+
add('warning', 'the semantic vectors do not match their metadata', 'npx crbro-memory semantic build');
|
|
627
|
+
for (const s of report.teams.spaces)
|
|
628
|
+
if (s.status === 'warning')
|
|
629
|
+
add('warning', `team space ${s.path} is ${s.ahead} ahead / ${s.behind} behind`, 'crbro_boot syncs it');
|
|
630
|
+
if (report.versions.daemon_status !== 'ok')
|
|
631
|
+
add('unchecked', 'no daemon answered (normal when daemon mode is off or no client is open)');
|
|
632
|
+
if (report.semantic.index.status === 'unknown')
|
|
633
|
+
add('unchecked', 'semantic recall: no vectors yet (not installed, or never built)', 'npx crbro-memory semantic status');
|
|
634
|
+
add('unchecked', 'semantic operation and backup integrity: not_probed (doctor never loads the model or unpacks a backup)');
|
|
635
|
+
add('unchecked', 'whether each client runs crbro_boot on its own: unknown (doctor reads files, it does not start clients)');
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
function formatDoctor(report) {
|
|
639
|
+
const show = (v) => v === null || v === undefined ? 'unknown' : String(v);
|
|
640
|
+
const list = findings(report);
|
|
641
|
+
const block = (title, level) => {
|
|
642
|
+
const items = list.filter(f => f.level === level);
|
|
643
|
+
if (!items.length)
|
|
644
|
+
return [];
|
|
645
|
+
return [title, ...items.map(f => ` • ${f.text}${f.action ? `\n → ${f.action}` : ''}`), ''];
|
|
646
|
+
};
|
|
647
|
+
const verdict = report.status === 'ok' ? 'Everything observed is in order.'
|
|
648
|
+
: report.status === 'error' ? 'There are problems to fix (below).'
|
|
649
|
+
: report.status === 'warning' ? 'Working, with warnings (below).' : 'Not enough evidence to say (below).';
|
|
650
|
+
return [
|
|
651
|
+
`CRBRO doctor — read-only — ${report.status}`,
|
|
652
|
+
verdict,
|
|
653
|
+
'',
|
|
654
|
+
...block('Problems:', 'problem'),
|
|
655
|
+
...block('Warnings:', 'warning'),
|
|
656
|
+
...block('Not checked:', 'unchecked'),
|
|
657
|
+
'Details:',
|
|
658
|
+
`Installed: ${show(report.versions.installed)}`,
|
|
659
|
+
`Running daemons: ${report.versions.daemons.map(d => show(d.running_version)).join(', ') || 'unknown'} (authenticated status only)`,
|
|
660
|
+
`Brain: ${report.brain.path} [${report.brain.status}]`,
|
|
661
|
+
`Last boot: ${show(report.brain.last_boot)} (aggregate); client autorun: unknown`,
|
|
662
|
+
...report.brain.invalid_json_paths.map(file => `Invalid JSON: ${file}`),
|
|
663
|
+
...report.brain.unreadable_paths.map(file => `Unreadable: ${file}`),
|
|
664
|
+
...report.configs.clients.map(c => `${c.client}: ${c.path} [${c.status}]; registered=${show(c.registered)}; enabled=${show(c.enabled)}; brain=${c.brain_paths.join(', ') || 'unknown (inherited)'}`),
|
|
665
|
+
`Different configured brain paths: ${report.configs.different_brain_paths}`,
|
|
666
|
+
`Semantic: ${report.semantic.mode}; runtime=${show(report.semantic.runtime_present)}; model cache=${show(report.semantic.model_cache_present)}; index=${report.semantic.index.status}; operation=not_probed`,
|
|
667
|
+
`Latest backup: ${report.backups.latest?.path || 'unknown'}${report.backups.age_days !== null ? ` (${report.backups.age_days} day(s) old)` : ''}; integrity=not_probed`,
|
|
668
|
+
`Hooks: ${report.hooks.installed.map(h => `${node_path_1.default.basename(h.file)} (${h.client}) ${h.state}`).join(', ') || 'none installed'} [${report.hooks.status}]`,
|
|
669
|
+
`Daemon leftovers: ${report.daemon_leftovers.dead_descriptors} dead descriptor(s), ${report.daemon_leftovers.orphan_tmp} orphan temp file(s) [${report.daemon_leftovers.status}]`,
|
|
670
|
+
`Quarantine: ${report.quarantine.files} file(s), ${report.quarantine.bytes} bytes, oldest ${show(report.quarantine.oldest_days)} day(s) [${report.quarantine.status}]`,
|
|
671
|
+
`Trigger index: ${report.triggers.exists ? `v${show(report.triggers.version)}, ${show(report.triggers.entries)} lessons, built ${show(report.triggers.built)}` : 'none'}${report.triggers.guard_wired ? ' (guard wired)' : ''} [${report.triggers.status}]`,
|
|
672
|
+
...report.teams.spaces.map(s => `Team: ${s.path}; dirty=${show(s.dirty)}; worktree=${s.worktree_state}; ahead=${show(s.ahead)}; behind=${show(s.behind)}; remote=not_fetched`),
|
|
673
|
+
`Team remote state: not_fetched. Unknown values are not evidence of a working integration.`,
|
|
674
|
+
].join('\n');
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=doctor.js.map
|