claude-flow 3.21.0 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +23 -2
  3. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +56 -0
  4. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.d.ts +27 -0
  5. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
  6. package/v3/@claude-flow/cli/dist/src/commands/memory.js +4 -2
  7. package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
  8. package/v3/@claude-flow/cli/dist/src/index.js +22 -1
  9. package/v3/@claude-flow/cli/dist/src/init/executor.js +37 -0
  10. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
  11. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
  12. package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
  13. package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
  14. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
  15. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +89 -8
  16. package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.d.ts +53 -0
  17. package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.js +118 -0
  18. package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
  19. package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
  22. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
  23. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
  24. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
  25. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +115 -54
  26. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
  27. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
  28. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
  29. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
  30. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
  31. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
  32. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +22 -1
  33. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +117 -6
  34. package/v3/@claude-flow/cli/package.json +5 -2
@@ -365,11 +365,9 @@ async function getRegistry(dbPath) {
365
365
  * Replaces naive String.includes() with proper information retrieval scoring.
366
366
  * Parameters tuned for short memory entries (k1=1.2, b=0.75).
367
367
  */
368
- function bm25Score(queryTerms, docContent, avgDocLength, docCount, termDocFreqs) {
368
+ function bm25Score(queryTerms, docWords, docLength, avgDocLength, docCount, termDocFreqs) {
369
369
  const k1 = 1.2;
370
370
  const b = 0.75;
371
- const docWords = docContent.toLowerCase().split(/\s+/);
372
- const docLength = docWords.length;
373
371
  let score = 0;
374
372
  for (const term of queryTerms) {
375
373
  const tf = docWords.filter(w => w === term || w.includes(term)).length;
@@ -382,23 +380,27 @@ function bm25Score(queryTerms, docContent, avgDocLength, docCount, termDocFreqs)
382
380
  }
383
381
  return score;
384
382
  }
383
+ function tokenizeCorpus(rows) {
384
+ return rows.map(row => {
385
+ const contentLower = (row.content || '').toLowerCase();
386
+ return { contentLower, words: contentLower.split(/\s+/) };
387
+ });
388
+ }
385
389
  /**
386
- * Compute BM25 term document frequencies for a set of rows.
390
+ * Compute BM25 term document frequencies over an already-tokenized corpus.
387
391
  */
388
- function computeTermDocFreqs(queryTerms, rows) {
392
+ function computeTermDocFreqs(queryTerms, docs) {
389
393
  const termDocFreqs = new Map();
390
394
  let totalLength = 0;
391
- for (const row of rows) {
392
- const content = (row.content || '').toLowerCase();
393
- const words = content.split(/\s+/);
394
- totalLength += words.length;
395
+ for (const doc of docs) {
396
+ totalLength += doc.words.length;
395
397
  for (const term of queryTerms) {
396
- if (content.includes(term)) {
398
+ if (doc.contentLower.includes(term)) {
397
399
  termDocFreqs.set(term, (termDocFreqs.get(term) || 0) + 1);
398
400
  }
399
401
  }
400
402
  }
401
- return { termDocFreqs, avgDocLength: rows.length > 0 ? totalLength / rows.length : 1 };
403
+ return { termDocFreqs, avgDocLength: docs.length > 0 ? totalLength / docs.length : 1 };
402
404
  }
403
405
  // ===== Phase 2: TieredCache helpers =====
404
406
  /**
@@ -484,6 +486,10 @@ async function logAttestation(registry, operation, entryId, metadata) {
484
486
  // Non-fatal — attestation is observability, not correctness
485
487
  }
486
488
  }
489
+ // Tracks db handles whose schema DDL has already been ensured, so getDb()
490
+ // runs the CREATE…IF NOT EXISTS block at most once per handle instead of on
491
+ // every bridge call. WeakSet so handles GC without leaking.
492
+ const _schemaEnsuredDbs = new WeakSet();
487
493
  /**
488
494
  * Get the AgentDB database handle and ensure memory_entries table exists.
489
495
  * Returns null if not available.
@@ -493,35 +499,42 @@ function getDb(registry) {
493
499
  if (!agentdb?.database)
494
500
  return null;
495
501
  const db = agentdb.database;
496
- // Ensure memory_entries table exists (idempotent)
497
- try {
498
- db.exec(`CREATE TABLE IF NOT EXISTS memory_entries (
499
- id TEXT PRIMARY KEY,
500
- key TEXT NOT NULL,
501
- namespace TEXT DEFAULT 'default',
502
- content TEXT NOT NULL,
503
- type TEXT DEFAULT 'semantic',
504
- embedding TEXT,
505
- embedding_model TEXT DEFAULT 'local',
506
- embedding_dimensions INTEGER,
507
- tags TEXT,
508
- metadata TEXT,
509
- owner_id TEXT,
510
- created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
511
- updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
512
- expires_at INTEGER,
513
- last_accessed_at INTEGER,
514
- access_count INTEGER DEFAULT 0,
515
- status TEXT DEFAULT 'active',
516
- UNIQUE(namespace, key)
517
- )`);
518
- // Ensure indexes
519
- db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
520
- db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`);
521
- db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`);
522
- }
523
- catch {
524
- // Table already exists or db is read-only — that's fine
502
+ // Ensure memory_entries table exists (idempotent). The DDL is run at most
503
+ // once per db handle — re-parsing 4× CREATE…IF NOT EXISTS on every bridge
504
+ // call (store/search/get) was pure per-op overhead. Keyed by handle via a
505
+ // WeakSet so a new db instance re-ensures without a stale global flag.
506
+ if (!_schemaEnsuredDbs.has(db)) {
507
+ try {
508
+ db.exec(`CREATE TABLE IF NOT EXISTS memory_entries (
509
+ id TEXT PRIMARY KEY,
510
+ key TEXT NOT NULL,
511
+ namespace TEXT DEFAULT 'default',
512
+ content TEXT NOT NULL,
513
+ type TEXT DEFAULT 'semantic',
514
+ embedding TEXT,
515
+ embedding_model TEXT DEFAULT 'local',
516
+ embedding_dimensions INTEGER,
517
+ tags TEXT,
518
+ metadata TEXT,
519
+ owner_id TEXT,
520
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
521
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
522
+ expires_at INTEGER,
523
+ last_accessed_at INTEGER,
524
+ access_count INTEGER DEFAULT 0,
525
+ status TEXT DEFAULT 'active',
526
+ UNIQUE(namespace, key)
527
+ )`);
528
+ // Ensure indexes
529
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
530
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`);
531
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`);
532
+ _schemaEnsuredDbs.add(db);
533
+ }
534
+ catch {
535
+ // Table already exists or db is read-only — that's fine. Don't mark
536
+ // ensured on failure so a later writable call can retry.
537
+ }
525
538
  }
526
539
  // ─── #2256-followup: rescue agentdb.embedder when its transformers.js
527
540
  // path fell through to mock embeddings.
@@ -637,6 +650,7 @@ export async function bridgeStoreEntry(options) {
637
650
  }
638
651
  // Generate embedding via AgentDB's embedder
639
652
  let embeddingJson = null;
653
+ let embeddingArr = null;
640
654
  let dimensions = 0;
641
655
  let model = 'local';
642
656
  if (options.generateEmbeddingFlag !== false && value.length > 0) {
@@ -645,7 +659,8 @@ export async function bridgeStoreEntry(options) {
645
659
  if (embedder) {
646
660
  const emb = await embedder.embed(value);
647
661
  if (emb) {
648
- embeddingJson = JSON.stringify(Array.from(emb));
662
+ embeddingArr = Array.from(emb);
663
+ embeddingJson = JSON.stringify(embeddingArr);
649
664
  dimensions = emb.length;
650
665
  model = 'Xenova/all-MiniLM-L6-v2';
651
666
  }
@@ -680,6 +695,31 @@ export async function bridgeStoreEntry(options) {
680
695
  catch { /* vector_indexes may not exist on legacy DBs — fall through */ }
681
696
  const stmt = ctx.db.prepare(insertSql);
682
697
  stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, ttl ? now + (ttl * 1000) : null);
698
+ // #2558: keep `vector_indexes.total_vectors` accurate so status/tooling
699
+ // stop reporting "HNSW index: 0 vectors" while embedded entries exist.
700
+ try {
701
+ ctx.db
702
+ .prepare(`UPDATE vector_indexes SET
703
+ total_vectors = (SELECT COUNT(*) FROM memory_entries
704
+ WHERE namespace = ? AND embedding IS NOT NULL),
705
+ updated_at = ?
706
+ WHERE name = ?`)
707
+ .run(namespace, now, namespace);
708
+ }
709
+ catch { /* vector_indexes may not exist on legacy DBs — non-fatal */ }
710
+ // #2558: better-sqlite3 opens the DB in WAL mode and, for the small write
711
+ // volumes typical of CLI usage, may never reach the auto-checkpoint
712
+ // threshold — leaving committed rows only in the -wal file. WAL-blind
713
+ // readers (the sql.js fallback search path; the statusline's read-only
714
+ // `sqlite3` vector count) then see a stale/empty main DB file and report
715
+ // "0 vectors" / empty search. A PASSIVE checkpoint flushes committed pages
716
+ // into the main file without blocking writers. Best-effort, never fatal.
717
+ try {
718
+ if (typeof ctx.db.pragma === 'function') {
719
+ ctx.db.pragma('wal_checkpoint(PASSIVE)');
720
+ }
721
+ }
722
+ catch { /* non-WAL, busy, or unsupported — non-fatal */ }
683
723
  // Phase 2: Write-through to TieredCache
684
724
  const safeNs = String(namespace).replace(/:/g, '_');
685
725
  const safeKey = String(key).replace(/:/g, '_');
@@ -691,7 +731,7 @@ export async function bridgeStoreEntry(options) {
691
731
  success: true,
692
732
  id,
693
733
  embedding: embeddingJson ? { dimensions, model } : undefined,
694
- rawEmbedding: embeddingJson ? JSON.parse(embeddingJson) : undefined,
734
+ rawEmbedding: embeddingArr ?? undefined,
695
735
  guarded: true,
696
736
  cached: true,
697
737
  attested: true,
@@ -746,12 +786,17 @@ export async function bridgeSearchEntries(options) {
746
786
  catch {
747
787
  return null;
748
788
  }
749
- // Phase 2: Compute BM25 term stats for the corpus
789
+ // Phase 2: Compute BM25 term stats for the corpus. Tokenize the corpus a
790
+ // single time and reuse the per-doc `{contentLower, words}` for term-freq,
791
+ // BM25 scoring, and the coverage floor below.
750
792
  const queryTerms = queryStr.toLowerCase().split(/\s+/).filter(t => t.length > 1);
751
- const { termDocFreqs, avgDocLength } = computeTermDocFreqs(queryTerms, rows);
793
+ const docs = tokenizeCorpus(rows);
794
+ const { termDocFreqs, avgDocLength } = computeTermDocFreqs(queryTerms, docs);
752
795
  const docCount = rows.length;
753
796
  const results = [];
754
- for (const row of rows) {
797
+ for (let i = 0; i < rows.length; i++) {
798
+ const row = rows[i];
799
+ const doc = docs[i];
755
800
  let semanticScore = 0;
756
801
  let bm25ScoreVal = 0;
757
802
  // Semantic scoring via cosine similarity
@@ -766,21 +811,37 @@ export async function bridgeSearchEntries(options) {
766
811
  }
767
812
  // Phase 2: BM25 keyword scoring (replaces String.includes fallback)
768
813
  if (queryTerms.length > 0 && row.content) {
769
- bm25ScoreVal = bm25Score(queryTerms, row.content, avgDocLength, docCount, termDocFreqs);
814
+ bm25ScoreVal = bm25Score(queryTerms, doc.words, doc.words.length, avgDocLength, docCount, termDocFreqs);
770
815
  // Normalize BM25 to 0-1 range (cap at 10 for normalization)
771
816
  bm25ScoreVal = Math.min(bm25ScoreVal / 10, 1.0);
772
817
  }
773
- // Reciprocal rank fusion: combine semantic and BM25
774
- // Weight: 0.7 semantic + 0.3 BM25 when both embeddings present
775
- // Fall back to BM25-only when either query or row lacks an embedding
776
- const score = semanticScore > 0
777
- ? (0.7 * semanticScore + 0.3 * bm25ScoreVal)
778
- : bm25ScoreVal;
818
+ // #2558: keyword-coverage floor for the lexical signal.
819
+ // BM25's IDF collapses toward zero when a term appears in most/all
820
+ // documents (routine on small memory corpora), and the /10 normalization
821
+ // crushed exact-keyword hits well below the default 0.3 threshold — so
822
+ // `memory search` recalled NOTHING even when the content literally
823
+ // contained the query term (issue #2558: "keyword recall random"). The
824
+ // pre-BM25 fallback guaranteed keyword recall via matchCount/words*0.5;
825
+ // this restores that guarantee. `coverage` is the fraction of query
826
+ // terms present in the document — a full-coverage hit must always be
827
+ // recallable regardless of IDF.
828
+ const contentLower = doc.contentLower;
829
+ const matchedTerms = queryTerms.filter(t => contentLower.includes(t)).length;
830
+ const coverage = queryTerms.length > 0 ? matchedTerms / queryTerms.length : 0;
831
+ const lexicalScore = Math.max(bm25ScoreVal, coverage);
832
+ // Recall-friendly fusion: a strong semantic OR lexical signal alone must
833
+ // clear the threshold. `blended` (0.6 semantic + 0.4 lexical) drives
834
+ // ranking; taking max() with the raw semantic score means (a) a genuinely
835
+ // similar entry is never dropped just because it lacks the query's exact
836
+ // words, and (b) a full-coverage keyword hit (lexical=1 → blended≥0.4) is
837
+ // never dropped just because its embedding cosine is low or negative.
838
+ const blended = 0.6 * Math.max(0, semanticScore) + 0.4 * lexicalScore;
839
+ const score = Math.max(blended, semanticScore);
779
840
  if (score >= threshold) {
780
841
  // Phase 4: ExplainableRecall provenance
781
842
  const provenance = queryEmbedding
782
- ? `semantic:${semanticScore.toFixed(3)}+bm25:${bm25ScoreVal.toFixed(3)}`
783
- : `bm25:${bm25ScoreVal.toFixed(3)}`;
843
+ ? `semantic:${semanticScore.toFixed(3)}+lexical:${lexicalScore.toFixed(3)}`
844
+ : `lexical:${lexicalScore.toFixed(3)}`;
784
845
  results.push({
785
846
  id: String(row.id).substring(0, 12),
786
847
  key: row.key || String(row.id).substring(0, 15),
@@ -203,6 +203,77 @@ export declare function checkAndMigrateLegacy(options: {
203
203
  migrated?: boolean;
204
204
  migratedCount?: number;
205
205
  }>;
206
+ /**
207
+ * Self-heal an EXISTING memory database that is missing the `vector_indexes`
208
+ * table or per-namespace rows.
209
+ *
210
+ * Why this exists: fresh installs create `vector_indexes` + seed rows, but a
211
+ * DB written by an older CLI or by agentdb directly may have thousands of
212
+ * embedded rows in `memory_entries` and NO `vector_indexes` table at all.
213
+ * Two things break as a result:
214
+ * 1. The statusline's vector count read collapsed to `0` (the count query
215
+ * referenced the missing table and failed whole — now split, but the
216
+ * HNSW flag still needs the table).
217
+ * 2. #1941 — `memory_search` routes per namespace via `vector_indexes`; a
218
+ * namespace with no row returns 0 results even when entries exist.
219
+ *
220
+ * This is idempotent and conservative:
221
+ * - Does NOTHING (no writes) when the table already exists and every embedded
222
+ * namespace already has a row — the common already-healed path, hit on
223
+ * every MCP start, must not write to the live DB unnecessarily.
224
+ * - Before ANY write, runs `PRAGMA quick_check`; if the DB reports structural
225
+ * corruption it SKIPS the repair entirely (returns `corrupt:true`) rather
226
+ * than writing into a malformed btree and risking making it worse. The
227
+ * caller/user should recover via `sqlite3 old.db .recover | sqlite3 new.db`.
228
+ * - Does NOT checkpoint. mode=ro readers already see committed WAL frames, and
229
+ * forcing a checkpoint on a DB with a torn WAL could persist latent damage.
230
+ *
231
+ * When a repair IS needed and the DB is healthy: creates the table if absent,
232
+ * seeds the fresh-install default rows, and backfills an accurate
233
+ * `total_vectors` per namespace. Runs on the existing-DB path of
234
+ * `initializeMemoryDatabase` (MCP start / `memory init`) and from `ruflo init`.
235
+ *
236
+ * Uses better-sqlite3 (WAL-safe, native). If the native module is unavailable
237
+ * it is a silent no-op — the split statusline query already prevents the count
238
+ * from zeroing; only the HNSW flag and namespace routing stay degraded.
239
+ */
240
+ /**
241
+ * Auto-recover a structurally-corrupt memory DB into a clean one, universally
242
+ * (better-sqlite3 only — no dependency on the external `sqlite3` CLI, which is
243
+ * absent on many npx hosts). Safe by construction:
244
+ * 1. Confirms corruption (quick_check) — no-op on a healthy DB.
245
+ * 2. Acquires an EXCLUSIVE lock (BEGIN IMMEDIATE). If another process is
246
+ * writing, it SKIPS (returns reason:'writer-active') rather than racing a
247
+ * writer and losing its in-flight writes — the mistake that must not recur.
248
+ * 3. Rebuilds a fresh DB table-by-table (schema + rows), skipping any single
249
+ * table whose pages won't scan so one bad table can't abort the whole
250
+ * rebuild.
251
+ * 4. VERIFIES the rebuild (integrity_check == ok AND recovered
252
+ * memory_entries count >= the readable source count) BEFORE touching the
253
+ * original.
254
+ * 5. Backs up the corrupt DB to `<db>.corrupt-<ts>.bak`, then atomically
255
+ * renames the verified rebuild into place and drops stale -wal/-shm.
256
+ * On any failure the original + backup are left intact — never destructive.
257
+ */
258
+ export declare function recoverMemoryDatabase(dbPath: string, opts?: {
259
+ verbose?: boolean;
260
+ }): Promise<{
261
+ recovered: boolean;
262
+ backupPath?: string;
263
+ rows?: number;
264
+ reason?: string;
265
+ }>;
266
+ export declare function repairVectorIndexes(dbPath: string, opts?: {
267
+ verbose?: boolean;
268
+ autoRecover?: boolean;
269
+ }): Promise<{
270
+ repaired: boolean;
271
+ tableCreated: boolean;
272
+ namespaces: string[];
273
+ corrupt?: boolean;
274
+ recovered?: boolean;
275
+ backupPath?: string;
276
+ }>;
206
277
  /**
207
278
  * Initialize the memory database properly using sql.js
208
279
  */