@claude-flow/cli 3.30.0 → 3.30.2
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/.claude/helpers/.helpers-version +1 -1
- package/.claude/helpers/helpers.manifest.json +3 -3
- package/.claude/helpers/statusline.cjs +88 -14
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/doctor.js +253 -2
- package/dist/src/init/statusline-generator.js +63 -932
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.30.1
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.30.
|
|
3
|
+
"version": "3.30.2",
|
|
4
4
|
"files": {
|
|
5
5
|
"auto-memory-hook.mjs": "e3e1033b24704992ddef6b31c7fa9dd7fcd9e1af7935dd77ef73402b916b31e6",
|
|
6
6
|
"hook-handler.cjs": "dc926a1a585abac85941347894632a800a9c4394e99166726017643e5b3c5950",
|
|
7
7
|
"intelligence.cjs": "5a55d979cb7ba5c8c4f27f3b2e6d686fbb1045d180023b803da672a37e05b915",
|
|
8
|
-
"statusline.cjs": "
|
|
8
|
+
"statusline.cjs": "7d7bdee732e1c75b863e409afb86ce0d712f4919245e990d806da6752015bffa"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"signature": "
|
|
11
|
+
"signature": "8F6VOTqOQJWReV9dx8QTOaGZ9A9wz1ZXjfrWM/4Gdr3t7cAZeGboa/DWseGUpt8zpyrOoYkuomJBdSC05B/zBA==",
|
|
12
12
|
"algorithm": "ed25519"
|
|
13
13
|
}
|
|
@@ -42,12 +42,18 @@ const CONFIG = {
|
|
|
42
42
|
const CWD = process.cwd();
|
|
43
43
|
|
|
44
44
|
// ─── Delegation cache ───────────────────────────────────────────
|
|
45
|
-
// Cache the CLI JSON result
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
45
|
+
// Cache the CLI JSON result so rapid prompt re-renders (Claude Code
|
|
46
|
+
// refreshes the statusline several times a second while streaming) don't
|
|
47
|
+
// re-invoke the CLI each time.
|
|
48
|
+
// #2337 bumped 10s → 60s.
|
|
49
|
+
// Followup for anthropics/claude-code#70200 (Windows console-flash bug —
|
|
50
|
+
// claude.exe spawns hook/statusline subprocesses without CREATE_NO_WINDOW,
|
|
51
|
+
// producing a visible cmd flash on every render): bumped 60s → 300s to
|
|
52
|
+
// reduce the flash rate 5x on Windows until the upstream fix ships.
|
|
53
|
+
// Tradeoff: stat/git counters update every 5min instead of every 1min;
|
|
54
|
+
// promo/insight row still rotates on its own tighter 20s promoFresh clock.
|
|
49
55
|
const CACHE_FILE = path.join(os.tmpdir(), 'ruflo-statusline-cache-' + require('crypto').createHash('md5').update(CWD).digest('hex').slice(0, 8) + '.json');
|
|
50
|
-
const CACHE_TTL_MS =
|
|
56
|
+
const CACHE_TTL_MS = 300000;
|
|
51
57
|
|
|
52
58
|
// The promo/insight row is designed to rotate on a 20s cadence (funnel/
|
|
53
59
|
// rotation.ts's ROTATION_SLOT_MS / funnel/promo.ts's insight-slot check —
|
|
@@ -229,7 +235,7 @@ function getStatuslineData() {
|
|
|
229
235
|
try {
|
|
230
236
|
const raw = execSync(
|
|
231
237
|
cmd,
|
|
232
|
-
{ encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'], cwd: CWD }
|
|
238
|
+
{ encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'], cwd: CWD, windowsHide: true }
|
|
233
239
|
).trim();
|
|
234
240
|
// The CLI may emit preamble lines before the JSON — find the first '{'.
|
|
235
241
|
const jsonStart = raw.indexOf('{');
|
|
@@ -445,6 +451,11 @@ function safeExec(cmd, timeoutMs) {
|
|
|
445
451
|
encoding: 'utf-8',
|
|
446
452
|
timeout: timeoutMs || 2000,
|
|
447
453
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
454
|
+
// Windows: without this, every execSync spawns cmd.exe /d /s /c which
|
|
455
|
+
// flashes a visible console window every render (~1/min via the 60s
|
|
456
|
+
// cache TTL). windowsHide runs the child in a hidden window instead.
|
|
457
|
+
// No-op on POSIX. Fix for #2XXX (user report: "cmd prompt keeps opening").
|
|
458
|
+
windowsHide: true,
|
|
448
459
|
}).trim();
|
|
449
460
|
} catch {
|
|
450
461
|
return '';
|
|
@@ -868,12 +879,16 @@ function getPromoRow(d) {
|
|
|
868
879
|
if (/^(0|false|off|no)$/i.test(String(process.env.RUFLO_FUNNEL || ''))) return null;
|
|
869
880
|
const promo = d && d.promo;
|
|
870
881
|
if (!promo || typeof promo.text !== 'string') return null;
|
|
871
|
-
// Strip control chars / ANSI / bidi overrides
|
|
872
|
-
//
|
|
873
|
-
|
|
882
|
+
// Strip control chars / ANSI / bidi overrides — promo copy is data and
|
|
883
|
+
// must never emit its own terminal sequences. Hard-cap length AFTER the
|
|
884
|
+
// strip; append an ellipsis when the cap fires so the row visibly reads
|
|
885
|
+
// as truncated instead of chopping a word mid-character (was: silent
|
|
886
|
+
// slice(0,100) that could produce output that looked like corrupt data).
|
|
887
|
+
const MAX_LEN = 100;
|
|
888
|
+
const sanitized = promo.text
|
|
874
889
|
.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '')
|
|
875
|
-
|
|
876
|
-
|
|
890
|
+
;
|
|
891
|
+
const text = (sanitized.length > MAX_LEN ? sanitized.slice(0, MAX_LEN - 1).trimEnd() + '…' : sanitized).trim();
|
|
877
892
|
if (text.length === 0) return null;
|
|
878
893
|
// Split the label from the trailing "· manage: ruflo settings" instruction
|
|
879
894
|
// so each part gets styling that matches what it actually IS:
|
|
@@ -896,9 +911,68 @@ function getPromoRow(d) {
|
|
|
896
911
|
const DIM_OFF = '\u001b[22m';
|
|
897
912
|
const BOLD_ON = '\u001b[1m';
|
|
898
913
|
const BOLD_OFF = '\u001b[22m';
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
914
|
+
const FG_BRIGHT_WHITE = '[97m';
|
|
915
|
+
// Reset FG to default so the caller's row-color code resumes coloring the
|
|
916
|
+
// rest of the row after the command portion. Without this the row-color
|
|
917
|
+
// escape wouldn't visibly re-apply because we already emitted an explicit FG.
|
|
918
|
+
const FG_DEFAULT = '[39m';
|
|
919
|
+
// Some hosts (Claude Code's Windows UI, cmd.exe, older mintty) don't
|
|
920
|
+
// render OSC 8 hyperlinks as clickable — the label just underlines and
|
|
921
|
+
// clicks do nothing. Append a "(domain)" suffix so the destination is
|
|
922
|
+
// visible/copyable everywhere. Wrap the suffix in OSC 8 too so terminals
|
|
923
|
+
// that DO support hyperlinks give users TWO click targets (label AND
|
|
924
|
+
// domain hint) instead of one — some Windows hosts render one but not
|
|
925
|
+
// the other depending on how the statusline row is parsed.
|
|
926
|
+
// Only for URLs (not educational tips), and only when the label doesn't
|
|
927
|
+
// already end in the domain to avoid duplication.
|
|
928
|
+
let visibleUrlHint = '';
|
|
929
|
+
if (promo.url) {
|
|
930
|
+
try {
|
|
931
|
+
const host = new URL(promo.url).hostname.replace(/^www\./, '');
|
|
932
|
+
// Strip the click-redirect wrapper so users see the FINAL destination,
|
|
933
|
+
// not funnel.ruv.io. If the URL is /v1/click/<id>?to=<encoded>, pull the target.
|
|
934
|
+
let displayHost = host;
|
|
935
|
+
try {
|
|
936
|
+
const to = new URL(promo.url).searchParams.get('to');
|
|
937
|
+
if (to) displayHost = new URL(to).hostname.replace(/^www\./, '');
|
|
938
|
+
} catch { /* not a click-redirect, keep the raw host */ }
|
|
939
|
+
if (displayHost && !label.toLowerCase().endsWith(displayHost.toLowerCase())) {
|
|
940
|
+
// safeTerminalLink returns the plain string if URL isn't allowlisted
|
|
941
|
+
// or the terminal can't do OSC 8 — either way the domain stays visible.
|
|
942
|
+
const clickableDomain = safeTerminalLink(displayHost, promo.url);
|
|
943
|
+
visibleUrlHint = DIM_ON + ' (' + clickableDomain + ')' + DIM_OFF;
|
|
944
|
+
}
|
|
945
|
+
} catch { /* malformed URL — omit hint, never break the row */ }
|
|
946
|
+
}
|
|
947
|
+
// "Entire row clickable" (user request) — wrap the whole assembled
|
|
948
|
+
// string in ONE OSC 8 hyperlink instead of just the label. The command
|
|
949
|
+
// portion keeps its bold + bright-white treatment (no underline) so it
|
|
950
|
+
// still VISUALLY reads as a shell command the user should type, not a
|
|
951
|
+
// link — but if the user clicks anywhere on the row (label, domain
|
|
952
|
+
// hint, connector, even the command text), the terminal opens the URL.
|
|
953
|
+
// Clicking DOES NOT execute the command; it just opens the target URL,
|
|
954
|
+
// which is safe. Terminals that ignore OSC 8 render the whole row as
|
|
955
|
+
// styled text and no click behavior — the previous fallback (visible
|
|
956
|
+
// domain suffix) still keeps the destination readable.
|
|
957
|
+
const wrapWholeRowInHyperlink = (assembled) => {
|
|
958
|
+
if (!promo.url) return assembled;
|
|
959
|
+
if (!terminalSupportsHyperlinks()) return assembled;
|
|
960
|
+
let parsed;
|
|
961
|
+
try { parsed = new URL(promo.url); } catch { return assembled; }
|
|
962
|
+
if (parsed.protocol !== 'https:') return assembled;
|
|
963
|
+
if (!PROMO_LINK_HOSTS.has(parsed.hostname)) return assembled;
|
|
964
|
+
const ESC = '';
|
|
965
|
+
return ESC + ']8;;' + parsed.href + ESC + '\\' + assembled + ESC + ']8;;' + ESC + '\\';
|
|
966
|
+
};
|
|
967
|
+
// Visual styling stays per-part. We only add the OSC 8 wrap around the
|
|
968
|
+
// combined string, so the whole row is one click target.
|
|
969
|
+
const labelStyled = promo.url ? UL_ON + label + UL_OFF : label;
|
|
970
|
+
if (!command) return wrapWholeRowInHyperlink(labelStyled + visibleUrlHint);
|
|
971
|
+
return wrapWholeRowInHyperlink(
|
|
972
|
+
labelStyled + visibleUrlHint
|
|
973
|
+
+ DIM_ON + manageWord + DIM_OFF
|
|
974
|
+
+ BOLD_ON + FG_BRIGHT_WHITE + command + FG_DEFAULT + BOLD_OFF
|
|
975
|
+
);
|
|
902
976
|
} catch (e) {
|
|
903
977
|
return null; // the promo row must never break the statusline
|
|
904
978
|
}
|
package/catalog-manifest.json
CHANGED
|
@@ -247,6 +247,244 @@ async function checkMemoryDatabase() {
|
|
|
247
247
|
}
|
|
248
248
|
return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
|
|
249
249
|
}
|
|
250
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
251
|
+
// #2677 — memory doctor: functional checks (stuinfla)
|
|
252
|
+
//
|
|
253
|
+
// The existing `checkMemoryDatabase` above asserts existence + statability
|
|
254
|
+
// only, so it CANNOT distinguish a healthy DB from a 99.97%-empty or
|
|
255
|
+
// SQLite-malformed one. Stuinfla reported both cases live (81-store fleet).
|
|
256
|
+
// The three checks below layer functional assertions on top, ordered so
|
|
257
|
+
// the earliest chain-break is always the first red the user sees:
|
|
258
|
+
// 1. Integrity — can sql.js open it AND does PRAGMA integrity_check pass?
|
|
259
|
+
// 2. Content — do most memory_entries rows carry non-empty content?
|
|
260
|
+
// 3. Embedding coverage — do most rows have a vector? (unembedded rows are
|
|
261
|
+
// both unrecallable AND undistillable per ADR-174)
|
|
262
|
+
// Ordering matters: content ratio is meaningless on a DB that can't open;
|
|
263
|
+
// embedding coverage is meaningless on rows with no content. First red wins.
|
|
264
|
+
//
|
|
265
|
+
// Recall probe (stuinfla check 4) requires actual write+search+delete round
|
|
266
|
+
// trips through the CLI's own memory pipeline — deferred to a follow-up PR
|
|
267
|
+
// to keep this one purely additive and safe.
|
|
268
|
+
//
|
|
269
|
+
// Design rules (also from stuinfla's report):
|
|
270
|
+
// - "A check that cannot fail protects nothing" — every check has a
|
|
271
|
+
// demonstrable red state.
|
|
272
|
+
// - "UNKNOWN is never PASS" — if the check can't RUN, report warn/fail,
|
|
273
|
+
// never a reassuring pass. Encrypted-DB case gets warn with the caveat
|
|
274
|
+
// spelled out; corruption gets fail.
|
|
275
|
+
// - "Print the measurement, not a checkmark" — messages include the
|
|
276
|
+
// ratios so operators see the shape of the problem.
|
|
277
|
+
/** Resolve the same DB path checkMemoryDatabase above uses. Returns null
|
|
278
|
+
* if no candidate exists (in which case none of these checks should
|
|
279
|
+
* fire — checkMemoryDatabase will already have surfaced the missing DB). */
|
|
280
|
+
async function resolveMemoryDbPath() {
|
|
281
|
+
const candidates = [];
|
|
282
|
+
try {
|
|
283
|
+
const { getMemoryRoot } = await import('../memory/memory-initializer.js');
|
|
284
|
+
candidates.push(join(getMemoryRoot(), 'memory.db'));
|
|
285
|
+
}
|
|
286
|
+
catch { /* fall through to legacy candidates */ }
|
|
287
|
+
candidates.push('.swarm/memory.db', '.claude-flow/memory.db', 'data/memory/memory.db', 'data/memory.db');
|
|
288
|
+
for (const p of candidates)
|
|
289
|
+
if (existsSync(p))
|
|
290
|
+
return p;
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
/** Open a sql.js Database over an on-disk file, returning null when the
|
|
294
|
+
* file can't be opened as a SQLite database (encrypted / corrupted / not
|
|
295
|
+
* a database). Callers decide whether that's warn or fail. */
|
|
296
|
+
async function tryOpenSqlJs(dbPath) {
|
|
297
|
+
try {
|
|
298
|
+
const initSqlJs = (await import('sql.js')).default ?? (await import('sql.js'));
|
|
299
|
+
const SQL = await (typeof initSqlJs === 'function' ? initSqlJs() : initSqlJs.default());
|
|
300
|
+
const { readFileSync } = await import('fs');
|
|
301
|
+
const buf = readFileSync(dbPath);
|
|
302
|
+
return new SQL.Database(new Uint8Array(buf));
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Check 1 — sql.js can open it AND PRAGMA integrity_check returns 'ok'.
|
|
309
|
+
// Two fail modes handled distinctly per "UNKNOWN is never PASS":
|
|
310
|
+
// - Open fails: warn ("cannot open; encrypted DB or corrupt — doctor
|
|
311
|
+
// can't distinguish from this side")
|
|
312
|
+
// - Open succeeds but pragma != 'ok': fail (definite corruption)
|
|
313
|
+
async function checkMemoryIntegrity() {
|
|
314
|
+
const dbPath = await resolveMemoryDbPath();
|
|
315
|
+
if (!dbPath)
|
|
316
|
+
return { name: 'Memory Integrity', status: 'warn', message: 'no memory.db found (see Memory Database check above)' };
|
|
317
|
+
const db = await tryOpenSqlJs(dbPath);
|
|
318
|
+
if (!db) {
|
|
319
|
+
return {
|
|
320
|
+
name: 'Memory Integrity',
|
|
321
|
+
status: 'warn',
|
|
322
|
+
message: `${dbPath} — sql.js can't open (encrypted DB or corrupt; doctor can't tell which from outside)`,
|
|
323
|
+
fix: 'if encrypted: expected. if not: back up + `claude-flow memory init --force` to rebuild',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const res = db.exec('PRAGMA integrity_check');
|
|
328
|
+
const rows = res[0]?.values?.map((v) => String(v[0])) ?? [];
|
|
329
|
+
if (rows.length === 1 && rows[0] === 'ok') {
|
|
330
|
+
return { name: 'Memory Integrity', status: 'pass', message: `${dbPath} — PRAGMA integrity_check: ok` };
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
name: 'Memory Integrity',
|
|
334
|
+
status: 'fail',
|
|
335
|
+
message: `${dbPath} — PRAGMA integrity_check: ${rows.slice(0, 3).join('; ')}${rows.length > 3 ? ` (+${rows.length - 3} more)` : ''}`,
|
|
336
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
const msg = e.message || String(e);
|
|
341
|
+
const encryptedOrCorrupt = msg.includes('file is not a database') || msg.includes('malformed');
|
|
342
|
+
return {
|
|
343
|
+
name: 'Memory Integrity',
|
|
344
|
+
status: 'warn',
|
|
345
|
+
message: encryptedOrCorrupt
|
|
346
|
+
? `${dbPath} — DB refused query: ${msg} (encrypted DB or corruption; see Memory Integrity above)`
|
|
347
|
+
: `${dbPath} — probe threw: ${msg}`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
try {
|
|
352
|
+
db.close();
|
|
353
|
+
}
|
|
354
|
+
catch { /* best-effort */ }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Check 2 — memory_entries rows should mostly carry non-empty content.
|
|
358
|
+
// Stuinfla's live case: 11,133 rows, 3 with content (0.03%). Threshold 95%
|
|
359
|
+
// is the number he proposed. Values below → fail with the exact ratio in
|
|
360
|
+
// the message ("Print the measurement, not a checkmark").
|
|
361
|
+
async function checkMemoryContent() {
|
|
362
|
+
const dbPath = await resolveMemoryDbPath();
|
|
363
|
+
if (!dbPath)
|
|
364
|
+
return { name: 'Memory Content', status: 'warn', message: 'no memory.db found' };
|
|
365
|
+
const db = await tryOpenSqlJs(dbPath);
|
|
366
|
+
if (!db)
|
|
367
|
+
return { name: 'Memory Content', status: 'warn', message: 'DB unreadable (see Memory Integrity)' };
|
|
368
|
+
try {
|
|
369
|
+
const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries'")[0]?.values?.map((v) => String(v[0])) ?? [];
|
|
370
|
+
if (tables.length === 0)
|
|
371
|
+
return { name: 'Memory Content', status: 'warn', message: 'no memory_entries table in DB — schema mismatch or empty init' };
|
|
372
|
+
const r = db.exec("SELECT count(*), sum(CASE WHEN length(trim(coalesce(content,'')))>0 THEN 1 ELSE 0 END) FROM memory_entries");
|
|
373
|
+
const total = Number(r[0]?.values?.[0]?.[0] ?? 0);
|
|
374
|
+
const populated = Number(r[0]?.values?.[0]?.[1] ?? 0);
|
|
375
|
+
if (total === 0)
|
|
376
|
+
return { name: 'Memory Content', status: 'pass', message: `${dbPath} — 0 rows (fresh DB, expected)` };
|
|
377
|
+
const ratio = populated / total;
|
|
378
|
+
const pct = (ratio * 100).toFixed(2);
|
|
379
|
+
const detail = `content ${populated}/${total} (${pct}%)`;
|
|
380
|
+
if (ratio < 0.95) {
|
|
381
|
+
return {
|
|
382
|
+
name: 'Memory Content',
|
|
383
|
+
status: 'fail',
|
|
384
|
+
message: `${dbPath} — ${detail} below 95% floor`,
|
|
385
|
+
fix: 'schema drift likely (rename of value→content or similar). check migration state via `claude-flow migrate status`',
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return { name: 'Memory Content', status: 'pass', message: `${dbPath} — ${detail}` };
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
const msg = e.message || String(e);
|
|
392
|
+
const encryptedOrCorrupt = msg.includes('file is not a database') || msg.includes('malformed');
|
|
393
|
+
return {
|
|
394
|
+
name: 'Memory Content',
|
|
395
|
+
status: 'warn',
|
|
396
|
+
message: encryptedOrCorrupt
|
|
397
|
+
? `${dbPath} — DB refused query: ${msg} (encrypted DB or corruption; see Memory Integrity above)`
|
|
398
|
+
: `${dbPath} — probe threw: ${msg}`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
try {
|
|
403
|
+
db.close();
|
|
404
|
+
}
|
|
405
|
+
catch { /* best-effort */ }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Check 3 — most memory_entries with content should also carry an embedding
|
|
409
|
+
// vector. Rows without a vector are BOTH unrecallable (no similarity search
|
|
410
|
+
// can find them) AND undistillable (ADR-174's distill skips rows with no
|
|
411
|
+
// parseable vector). Same 95% threshold + fail-with-ratio pattern as check 2.
|
|
412
|
+
//
|
|
413
|
+
// Embedding storage varies across ruflo installs (agentdb migrations, HNSW
|
|
414
|
+
// index vs inline vector column). Discovers the shape via schema probes,
|
|
415
|
+
// falls back to warn if the schema is unrecognized (better than a false
|
|
416
|
+
// pass, per "UNKNOWN is never PASS").
|
|
417
|
+
async function checkMemoryEmbeddingCoverage() {
|
|
418
|
+
const dbPath = await resolveMemoryDbPath();
|
|
419
|
+
if (!dbPath)
|
|
420
|
+
return { name: 'Memory Embedding Coverage', status: 'warn', message: 'no memory.db found' };
|
|
421
|
+
const db = await tryOpenSqlJs(dbPath);
|
|
422
|
+
if (!db)
|
|
423
|
+
return { name: 'Memory Embedding Coverage', status: 'warn', message: 'DB unreadable (see Memory Integrity)' };
|
|
424
|
+
try {
|
|
425
|
+
// Schema-shape discovery. Three candidate columns / tables we've seen
|
|
426
|
+
// across the agentdb / ruvector history — first match wins.
|
|
427
|
+
const cols = db.exec("PRAGMA table_info(memory_entries)");
|
|
428
|
+
const colNames = new Set((cols[0]?.values ?? []).map((v) => String(v[1])));
|
|
429
|
+
let vectorPredicate = null;
|
|
430
|
+
if (colNames.has('embedding'))
|
|
431
|
+
vectorPredicate = "embedding IS NOT NULL AND length(embedding) > 0";
|
|
432
|
+
else if (colNames.has('vector'))
|
|
433
|
+
vectorPredicate = "vector IS NOT NULL AND length(vector) > 0";
|
|
434
|
+
if (!vectorPredicate) {
|
|
435
|
+
const otherTables = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('vector_indexes','embeddings','memory_embeddings')")[0]?.values?.map((v) => String(v[0])) ?? [];
|
|
436
|
+
if (otherTables.length === 0) {
|
|
437
|
+
return {
|
|
438
|
+
name: 'Memory Embedding Coverage',
|
|
439
|
+
status: 'warn',
|
|
440
|
+
message: 'no embedding column/table recognized (agentdb schema mismatch or older format) — doctor cannot measure',
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const tbl = otherTables[0];
|
|
444
|
+
const r = db.exec(`SELECT count(*) FROM memory_entries m WHERE EXISTS (SELECT 1 FROM ${tbl} e WHERE e.memory_id = m.id OR e.entry_id = m.id OR e.id = m.id)`);
|
|
445
|
+
const withEmbedding = Number(r[0]?.values?.[0]?.[0] ?? 0);
|
|
446
|
+
const total = Number(db.exec("SELECT count(*) FROM memory_entries WHERE length(trim(coalesce(content,'')))>0")[0]?.values?.[0]?.[0] ?? 0);
|
|
447
|
+
if (total === 0)
|
|
448
|
+
return { name: 'Memory Embedding Coverage', status: 'pass', message: `${dbPath} — 0 content rows (nothing to embed)` };
|
|
449
|
+
const ratio = withEmbedding / total;
|
|
450
|
+
const pct = (ratio * 100).toFixed(2);
|
|
451
|
+
const detail = `embedded ${withEmbedding}/${total} (${pct}%) via ${tbl}`;
|
|
452
|
+
if (ratio < 0.95)
|
|
453
|
+
return { name: 'Memory Embedding Coverage', status: 'fail', message: `${dbPath} — ${detail} below 95% floor`, fix: 'unembedded rows are unrecallable + undistillable — re-run `claude-flow memory embed --namespace <name>` for populated namespaces' };
|
|
454
|
+
return { name: 'Memory Embedding Coverage', status: 'pass', message: `${dbPath} — ${detail}` };
|
|
455
|
+
}
|
|
456
|
+
// Inline embedding column
|
|
457
|
+
const r = db.exec(`SELECT count(*), sum(CASE WHEN ${vectorPredicate} THEN 1 ELSE 0 END) FROM memory_entries WHERE length(trim(coalesce(content,'')))>0`);
|
|
458
|
+
const total = Number(r[0]?.values?.[0]?.[0] ?? 0);
|
|
459
|
+
const withEmb = Number(r[0]?.values?.[0]?.[1] ?? 0);
|
|
460
|
+
if (total === 0)
|
|
461
|
+
return { name: 'Memory Embedding Coverage', status: 'pass', message: `${dbPath} — 0 content rows (nothing to embed)` };
|
|
462
|
+
const ratio = withEmb / total;
|
|
463
|
+
const pct = (ratio * 100).toFixed(2);
|
|
464
|
+
const detail = `embedded ${withEmb}/${total} (${pct}%)`;
|
|
465
|
+
if (ratio < 0.95) {
|
|
466
|
+
return { name: 'Memory Embedding Coverage', status: 'fail', message: `${dbPath} — ${detail} below 95% floor`, fix: 'unembedded rows are unrecallable + undistillable — re-run `claude-flow memory embed --namespace <name>` for populated namespaces' };
|
|
467
|
+
}
|
|
468
|
+
return { name: 'Memory Embedding Coverage', status: 'pass', message: `${dbPath} — ${detail}` };
|
|
469
|
+
}
|
|
470
|
+
catch (e) {
|
|
471
|
+
const msg = e.message || String(e);
|
|
472
|
+
const encryptedOrCorrupt = msg.includes('file is not a database') || msg.includes('malformed');
|
|
473
|
+
return {
|
|
474
|
+
name: 'Memory Embedding Coverage',
|
|
475
|
+
status: 'warn',
|
|
476
|
+
message: encryptedOrCorrupt
|
|
477
|
+
? `${dbPath} — DB refused query: ${msg} (encrypted DB or corruption; see Memory Integrity above)`
|
|
478
|
+
: `${dbPath} — probe threw: ${msg}`,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
try {
|
|
483
|
+
db.close();
|
|
484
|
+
}
|
|
485
|
+
catch { /* best-effort */ }
|
|
486
|
+
}
|
|
487
|
+
}
|
|
250
488
|
// #2545: Check that the self-learning bridge can actually load @claude-flow/memory
|
|
251
489
|
// the SAME way the SessionStart auto-memory hook does. On the documented `npx ruflo`
|
|
252
490
|
// path the package lands in the npx cache — unreachable from the project — so the
|
|
@@ -1210,6 +1448,13 @@ export const doctorCommand = {
|
|
|
1210
1448
|
checkFunnel, // ADR-305 — effective funnel state + deciding precedence source
|
|
1211
1449
|
checkProxy, // ADR-313 — Meta LLM Proxy sponsored-downtime health
|
|
1212
1450
|
];
|
|
1451
|
+
// #2677: `--component memory` now runs the whole memory-health suite,
|
|
1452
|
+
// not just the existence check. Values can be a single check or an
|
|
1453
|
+
// array — expanded at execution time. Stuinfla's report showed the
|
|
1454
|
+
// existence-only check reporting PASS on a 99.97%-empty and even a
|
|
1455
|
+
// SQLite-malformed DB; the array here layers integrity → content →
|
|
1456
|
+
// embedding coverage over the existing existence probe, ordered so
|
|
1457
|
+
// the earliest chain-break is always the first red the user sees.
|
|
1213
1458
|
const componentMap = {
|
|
1214
1459
|
'version': checkVersionFreshness,
|
|
1215
1460
|
'freshness': checkVersionFreshness,
|
|
@@ -1219,7 +1464,12 @@ export const doctorCommand = {
|
|
|
1219
1464
|
'config': checkConfigFile,
|
|
1220
1465
|
'stale-settings': checkStaleSettingsNpx, // #2448
|
|
1221
1466
|
'daemon': checkDaemonStatus,
|
|
1222
|
-
'memory':
|
|
1467
|
+
'memory': [
|
|
1468
|
+
checkMemoryDatabase, // existing: exists + statable (unchanged)
|
|
1469
|
+
checkMemoryIntegrity, // #2677 check 1: sql.js open + PRAGMA integrity_check
|
|
1470
|
+
checkMemoryContent, // #2677 check 2: memory_entries content coverage
|
|
1471
|
+
checkMemoryEmbeddingCoverage, // #2677 check 3: vector coverage on populated rows
|
|
1472
|
+
],
|
|
1223
1473
|
'learning': checkLearningBridge, // #2545
|
|
1224
1474
|
'learning-bridge': checkLearningBridge, // #2545
|
|
1225
1475
|
'api': checkApiKeys,
|
|
@@ -1238,7 +1488,8 @@ export const doctorCommand = {
|
|
|
1238
1488
|
};
|
|
1239
1489
|
let checksToRun = allChecks;
|
|
1240
1490
|
if (component && componentMap[component]) {
|
|
1241
|
-
|
|
1491
|
+
const entry = componentMap[component];
|
|
1492
|
+
checksToRun = Array.isArray(entry) ? entry : [entry];
|
|
1242
1493
|
}
|
|
1243
1494
|
const results = [];
|
|
1244
1495
|
const fixes = [];
|