claude-flow 3.47.1 → 3.48.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 (48) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +6 -2
  2. package/.claude/helpers/learning-service.mjs +4 -2
  3. package/.claude/helpers/metrics-db.mjs +4 -2
  4. package/node_modules/@claude-flow/codex/package.json +1 -1
  5. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  6. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
  7. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  8. package/node_modules/@claude-flow/mcp/package.json +6 -3
  9. package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
  10. package/node_modules/@claude-flow/security/package.json +2 -3
  11. package/package.json +7 -7
  12. package/v3/@claude-flow/cli/README.md +0 -2
  13. package/v3/@claude-flow/cli/bin/cli.js +4 -1
  14. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  15. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
  16. package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
  17. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +15 -5
  18. package/v3/@claude-flow/cli/dist/src/commands/memory.js +89 -27
  19. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
  20. package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
  21. package/v3/@claude-flow/cli/dist/src/commands/session.js +6 -6
  22. package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
  23. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
  24. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
  25. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
  26. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +0 -4
  27. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +139 -37
  28. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +89 -35
  29. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
  30. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
  31. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +34 -8
  32. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
  33. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
  34. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
  35. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
  36. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
  37. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
  38. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
  39. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +41 -31
  40. package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
  41. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
  42. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
  43. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
  44. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
  45. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
  46. package/v3/@claude-flow/cli/package.json +9 -9
  47. package/v3/@claude-flow/guidance/package.json +5 -6
  48. package/v3/@claude-flow/shared/package.json +6 -3
@@ -205,7 +205,9 @@ export interface MemoryInitResult {
205
205
  * Ensure memory_entries table has all required columns
206
206
  * Adds missing columns for older databases (e.g., 'content' column)
207
207
  */
208
- export declare function ensureSchemaColumns(dbPath: string): Promise<{
208
+ export declare function ensureSchemaColumns(dbPath: string, options?: {
209
+ encryptWrites?: boolean;
210
+ }): Promise<{
209
211
  success: boolean;
210
212
  columnsAdded: string[];
211
213
  error?: string;
@@ -496,6 +498,8 @@ export declare function listEntries(options: {
496
498
  limit?: number;
497
499
  offset?: number;
498
500
  dbPath?: string;
501
+ /** Internal native-mirror writes must remain plaintext, including schema migration. */
502
+ encryptWrites?: boolean;
499
503
  /** #2073: When true, include the entry's full `content` string in each result. */
500
504
  includeContent?: boolean;
501
505
  /** ADR-323: restrict rows to these provenance types. */
@@ -549,6 +553,8 @@ export declare function deleteEntry(options: {
549
553
  key: string;
550
554
  namespace?: string;
551
555
  dbPath?: string;
556
+ /** Internal native-mirror writes must remain plaintext, including schema migration. */
557
+ encryptWrites?: boolean;
552
558
  }): Promise<{
553
559
  success: boolean;
554
560
  deleted: boolean;
@@ -573,6 +579,8 @@ export declare function withMemoryDbLock<T>(dbPath: string, fn: () => Promise<T>
573
579
  export declare function purgeNamespace(options: {
574
580
  namespace: string;
575
581
  dbPath?: string;
582
+ /** Internal native-mirror writes must remain plaintext, including schema migration. */
583
+ encryptWrites?: boolean;
576
584
  }): Promise<{
577
585
  success: boolean;
578
586
  deletedCount: number;
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * @module v3/cli/memory-initializer
10
10
  */
11
+ import { liveMemoryRowSql } from './live-memory-row.js';
11
12
  import * as fs from 'fs';
12
13
  import * as path from 'path';
13
14
  import { AsyncLocalStorage } from 'node:async_hooks';
@@ -208,14 +209,14 @@ async function getBridge() {
208
209
  * missing better-sqlite3. Appending the recorded reason turns an unactionable
209
210
  * message into a diagnosis.
210
211
  */
211
- async function walRefusalError(operation) {
212
+ async function walRefusalError(operation, bridgeDbPath) {
212
213
  const base = 'memory database has an active native WAL connection '
213
214
  + '(found -wal/-shm sidecar files) — refusing an unsafe sql.js '
214
215
  + `whole-image ${operation}. Retry once the native writer completes, or `
215
216
  + 'restore the native better-sqlite3 bridge.';
216
217
  try {
217
218
  const bridge = await getBridge();
218
- const reason = bridge?.getBridgeFailureReason?.();
219
+ const reason = bridge?.getBridgeFailureReason?.(bridgeDbPath);
219
220
  if (reason)
220
221
  return `${base} Bridge unavailable: ${reason}`;
221
222
  }
@@ -1141,7 +1142,7 @@ INSERT OR IGNORE INTO vector_indexes (id, name, dimensions) VALUES
1141
1142
  * Ensure memory_entries table has all required columns
1142
1143
  * Adds missing columns for older databases (e.g., 'content' column)
1143
1144
  */
1144
- export async function ensureSchemaColumns(dbPath) {
1145
+ export async function ensureSchemaColumns(dbPath, options = {}) {
1145
1146
  const columnsAdded = [];
1146
1147
  try {
1147
1148
  if (!fs.existsSync(dbPath)) {
@@ -1213,7 +1214,7 @@ export async function ensureSchemaColumns(dbPath) {
1213
1214
  if (modified) {
1214
1215
  // Save updated database
1215
1216
  const data = db.export();
1216
- writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
1217
+ writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
1217
1218
  }
1218
1219
  db.close();
1219
1220
  return { success: true, columnsAdded };
@@ -1312,13 +1313,12 @@ async function activateControllerRegistry(verbose) {
1312
1313
  if (!bridge) {
1313
1314
  return { activated, failed, initTimeMs: performance.now() - startTime };
1314
1315
  }
1315
- const registry = await bridge.getControllerRegistry();
1316
- if (!registry) {
1316
+ const controllers = await bridge.bridgeListControllers();
1317
+ if (!controllers) {
1317
1318
  return { activated, failed, initTimeMs: performance.now() - startTime };
1318
1319
  }
1319
1320
  // Collect controller status from the registry
1320
- if (typeof registry.listControllers === 'function') {
1321
- const controllers = registry.listControllers();
1321
+ if (controllers) {
1322
1322
  for (const ctrl of controllers) {
1323
1323
  if (ctrl.enabled) {
1324
1324
  activated.push(ctrl.name);
@@ -1879,7 +1879,7 @@ export async function checkMemoryInitialization(dbPath) {
1879
1879
  // Try to load with sql.js
1880
1880
  const initSqlJs = (await import('sql.js')).default;
1881
1881
  const SQL = await initSqlJs();
1882
- const fileBuffer = fs.readFileSync(path_);
1882
+ const fileBuffer = readFileMaybeEncrypted(path_, null);
1883
1883
  db = new SQL.Database(fileBuffer);
1884
1884
  // Check for metadata table
1885
1885
  const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table'");
@@ -2579,7 +2579,7 @@ export async function storeEntry(options) {
2579
2579
  return {
2580
2580
  success: false,
2581
2581
  id: '',
2582
- error: await walRefusalError('write'),
2582
+ error: await walRefusalError('write', options.dbPath),
2583
2583
  };
2584
2584
  }
2585
2585
  const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
@@ -2751,8 +2751,8 @@ export async function searchEntries(options) {
2751
2751
  // query (no extra round-trip) so a provenance-filtered search
2752
2752
  // still gets RaBitQ's speedup instead of falling back to brute
2753
2753
  // force.
2754
- const stmt = db.prepare('SELECT content, embedding, provenance_type FROM memory_entries WHERE id = ? AND status = ?');
2755
- stmt.bind([candidate.id, 'active']);
2754
+ const stmt = db.prepare(`SELECT content, embedding, provenance_type FROM memory_entries WHERE id = ? AND ${liveMemoryRowSql()}`);
2755
+ stmt.bind([candidate.id]);
2756
2756
  if (stmt.step()) {
2757
2757
  const [content, embeddingJson, provenanceTypeVal] = stmt.get();
2758
2758
  if (provenanceFilter?.length && !provenanceFilter.includes(provenanceTypeVal || 'unknown')) {
@@ -2812,29 +2812,28 @@ export async function searchEntries(options) {
2812
2812
  const db = new SQL.Database(fileBuffer);
2813
2813
  const provenanceByKey = new Map();
2814
2814
  for (const r of filtered) {
2815
- const stmt = db.prepare('SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? LIMIT 1');
2815
+ const stmt = db.prepare(`SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? AND ${liveMemoryRowSql()} LIMIT 1`);
2816
2816
  stmt.bind([r.namespace, r.key]);
2817
2817
  if (stmt.step()) {
2818
- provenanceByKey.set(`${r.namespace}::${r.key}`, stmt.get()[0] || 'unknown');
2818
+ provenanceByKey.set(JSON.stringify([r.namespace, r.key]), stmt.get()[0] || 'unknown');
2819
2819
  }
2820
2820
  stmt.free();
2821
2821
  }
2822
2822
  db.close();
2823
2823
  filtered = filtered
2824
- .map(r => ({ ...r, provenanceType: provenanceByKey.get(`${r.namespace}::${r.key}`) || 'unknown' }));
2824
+ .filter(r => provenanceByKey.has(JSON.stringify([r.namespace, r.key])))
2825
+ .map(r => ({ ...r, provenanceType: provenanceByKey.get(JSON.stringify([r.namespace, r.key])) || 'unknown' }));
2825
2826
  if (provenanceFilter?.length) {
2826
2827
  filtered = filtered.filter(r => provenanceFilter.includes(r.provenanceType));
2827
2828
  }
2828
2829
  }
2829
2830
  catch {
2830
- // A requested trust filter fails closed. Unfiltered callers retain
2831
- // backward-compatible results with an explicit unknown label.
2832
- filtered = provenanceFilter?.length
2833
- ? []
2834
- : filtered.map(r => ({ ...r, provenanceType: 'unknown' }));
2831
+ // An ANN hit alone cannot prove the row is still live. Fall back
2832
+ // to the authoritative SQL scan if liveness cannot be checked.
2833
+ filtered = [];
2835
2834
  }
2836
2835
  }
2837
- if (!provenanceFilter?.length || filtered.length >= limit) {
2836
+ if (filtered.length >= limit) {
2838
2837
  return {
2839
2838
  success: true,
2840
2839
  results: filtered.slice(0, limit),
@@ -2853,7 +2852,7 @@ export async function searchEntries(options) {
2853
2852
  // Get entries with embeddings
2854
2853
  // ADR-323: build the WHERE clause incrementally so namespace and
2855
2854
  // provenance filters compose (both, either, or neither).
2856
- const whereClauses = [`status = 'active'`];
2855
+ const whereClauses = [liveMemoryRowSql()];
2857
2856
  const whereParams = [];
2858
2857
  if (effectiveNamespace !== 'all') {
2859
2858
  whereClauses.push('namespace = ?');
@@ -2980,8 +2979,14 @@ export async function listEntries(options) {
2980
2979
  if (!fs.existsSync(dbPath)) {
2981
2980
  return { success: false, entries: [], total: 0, error: 'Database not found' };
2982
2981
  }
2982
+ // Listing can migrate/backfill the schema, so it is also a whole-image
2983
+ // writer. The newly selected native mirror may still have a live WAL.
2984
+ await releaseOwnNativeHandle(dbPath);
2985
+ if (hasNativeWalSidecars(dbPath)) {
2986
+ return { success: false, entries: [], total: 0, error: await walRefusalError('read/write') };
2987
+ }
2983
2988
  // Ensure schema has all required columns (migration for older DBs)
2984
- await ensureSchemaColumns(dbPath);
2989
+ await ensureSchemaColumns(dbPath, options);
2985
2990
  const initSqlJs = (await import('sql.js')).default;
2986
2991
  const SQL = await initSqlJs();
2987
2992
  const fileBuffer = readFileMaybeEncrypted(dbPath, null);
@@ -2990,7 +2995,7 @@ export async function listEntries(options) {
2990
2995
  // that predate the status column may have NULL after migration.
2991
2996
  // See memory-bridge.ts:bridgeListEntries for full context.
2992
2997
  // Get total count
2993
- const whereClauses = [ACTIVE_MEMORY_ROW_SQL];
2998
+ const whereClauses = [liveMemoryRowSql()];
2994
2999
  const whereParams = [];
2995
3000
  if (namespace) {
2996
3001
  whereClauses.push('namespace = ?');
@@ -3088,7 +3093,7 @@ export async function getEntry(options) {
3088
3093
  return {
3089
3094
  success: false,
3090
3095
  found: false,
3091
- error: await walRefusalError('read/write'),
3096
+ error: await walRefusalError('read/write', options.dbPath),
3092
3097
  };
3093
3098
  }
3094
3099
  // #2878: this is a mutator, not a reader — the access_count bump below
@@ -3107,7 +3112,7 @@ export async function getEntry(options) {
3107
3112
  const getStmt = db.prepare(`
3108
3113
  SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
3109
3114
  FROM memory_entries
3110
- WHERE ${ACTIVE_MEMORY_ROW_SQL}
3115
+ WHERE ${liveMemoryRowSql()}
3111
3116
  AND key = ?
3112
3117
  AND namespace = ?
3113
3118
  LIMIT 1
@@ -3211,14 +3216,14 @@ export async function deleteEntry(options) {
3211
3216
  key,
3212
3217
  namespace,
3213
3218
  remainingEntries: 0,
3214
- error: await walRefusalError('write'),
3219
+ error: await walRefusalError('write', options.dbPath),
3215
3220
  };
3216
3221
  }
3217
3222
  // #2878: whole-image read-modify-write — without the lock a concurrent
3218
3223
  // writer's flush resurrects the row this call just tombstoned.
3219
3224
  return await withMemoryDbLock(dbPath, async () => {
3220
3225
  // Ensure schema has all required columns (migration for older DBs)
3221
- await ensureSchemaColumns(dbPath);
3226
+ await ensureSchemaColumns(dbPath, options);
3222
3227
  const initSqlJs = (await import('sql.js')).default;
3223
3228
  const SQL = await initSqlJs();
3224
3229
  const fileBuffer = readFileMaybeEncrypted(dbPath, null);
@@ -3268,7 +3273,7 @@ export async function deleteEntry(options) {
3268
3273
  const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
3269
3274
  // Save updated database
3270
3275
  const data = db.export();
3271
- writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
3276
+ writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
3272
3277
  db.close();
3273
3278
  // Clean up in-memory HNSW index so ghost vectors don't appear in searches.
3274
3279
  // Remove the entry from the HNSW entries map and invalidate the index.
@@ -3420,7 +3425,12 @@ export async function purgeNamespace(options) {
3420
3425
  if (!fs.existsSync(dbPath)) {
3421
3426
  return { success: false, deletedCount: 0, remainingEntries: 0, error: 'Database not found' };
3422
3427
  }
3423
- await ensureSchemaColumns(dbPath);
3428
+ // Recheck at mutation time even when the CLI already read a preview.
3429
+ await releaseOwnNativeHandle(dbPath);
3430
+ if (hasNativeWalSidecars(dbPath)) {
3431
+ return { success: false, deletedCount: 0, remainingEntries: 0, error: await walRefusalError('write') };
3432
+ }
3433
+ await ensureSchemaColumns(dbPath, options);
3424
3434
  const initSqlJs = (await import('sql.js')).default;
3425
3435
  const SQL = await initSqlJs();
3426
3436
  const fileBuffer = readFileMaybeEncrypted(dbPath, null);
@@ -3431,7 +3441,7 @@ export async function purgeNamespace(options) {
3431
3441
  const countResult = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE status = 'active'`);
3432
3442
  const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
3433
3443
  const data = db.export();
3434
- writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
3444
+ writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
3435
3445
  db.close();
3436
3446
  if (deletedCount > 0 && hnswIndex?.entries) {
3437
3447
  for (const [id, entry] of hnswIndex.entries) {
@@ -96,7 +96,10 @@ async function loadRuVector() {
96
96
  function extractImports(content, _filePath) {
97
97
  const imports = [];
98
98
  // ES6 import statements
99
- const esImportRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*\s*from\s*['"]([^'"]+)['"]/g;
99
+ // Keep the import clause's shape finite. Repeating an optional separator
100
+ // around alternatives lets ordinary prose such as "import one two three ..."
101
+ // backtrack exponentially when no `from` clause follows.
102
+ const esImportRegex = /\bimport\s+(?:type\s+)?(?:[\w$]+\s*,\s*)?(?:\{[^}]*\}|\*\s+as\s+[\w$]+|[\w$]+)\s+from\s*['"]([^'"]+)['"]/g;
100
103
  let match;
101
104
  while ((match = esImportRegex.exec(content)) !== null) {
102
105
  imports.push({ path: match[1], type: 'import' });
@@ -80,12 +80,23 @@ export class ConfigFileManager {
80
80
  }
81
81
  try {
82
82
  const content = fs.readFileSync(this.configPath, 'utf-8');
83
- this.config = JSON.parse(content);
83
+ const parsed = JSON.parse(content);
84
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
85
+ throw new Error('Config file must contain a JSON object');
86
+ }
87
+ this.config = parsed;
84
88
  return this.config;
85
89
  }
86
- catch {
90
+ catch (error) {
87
91
  this.config = null;
88
- return null;
92
+ if (error.code === 'ENOENT') {
93
+ this.configPath = null;
94
+ return null;
95
+ }
96
+ // A parse/read failure is not a missing config. In particular, set()
97
+ // must never replace a recoverable file with fallback defaults.
98
+ const detail = error instanceof Error ? error.message : String(error);
99
+ throw new Error(`Failed to load config ${this.configPath}: ${detail}`);
89
100
  }
90
101
  }
91
102
  /** Get the current config, loading if needed */
@@ -98,12 +109,23 @@ export class ConfigFileManager {
98
109
  /** Get a nested config value by dot-separated key */
99
110
  get(cwd, key) {
100
111
  const config = this.getConfig(cwd);
101
- return getNestedValue(config, key);
112
+ return Object.hasOwn(config, key) ? config[key] : getNestedValue(config, key);
102
113
  }
103
114
  /** Set a nested config value by dot-separated key */
104
115
  set(cwd, key, value) {
105
- const config = this.getConfig(cwd);
106
- setNestedValue(config, key, value);
116
+ // Reject the entire path before touching cached config or disk.
117
+ if (key.split('.').some(part => part === '__proto__' || part === 'constructor' || part === 'prototype')) {
118
+ throw new Error(`Unsafe configuration key: ${key}`);
119
+ }
120
+ // A targeted update must not persist unrelated defaults (notably the
121
+ // default memory path, which would relocate an existing memory store).
122
+ const config = this.config ?? this.load(cwd) ?? {};
123
+ if (Object.hasOwn(config, key)) {
124
+ config[key] = value;
125
+ }
126
+ else {
127
+ setNestedValue(config, key, value);
128
+ }
107
129
  this.config = config;
108
130
  const targetPath = this.configPath ?? path.resolve(cwd, CONFIG_FILENAMES[0]);
109
131
  this.writeAtomic(targetPath, config);
@@ -181,7 +203,7 @@ function getNestedValue(obj, key) {
181
203
  const parts = key.split('.');
182
204
  let current = obj;
183
205
  for (const part of parts) {
184
- if (current === null || current === undefined || typeof current !== 'object') {
206
+ if (current === null || current === undefined || typeof current !== 'object' || !Object.hasOwn(current, part)) {
185
207
  return undefined;
186
208
  }
187
209
  current = current[part];
@@ -194,7 +216,7 @@ function setNestedValue(obj, key, value) {
194
216
  let current = obj;
195
217
  for (let i = 0; i < parts.length - 1; i++) {
196
218
  const part = parts[i];
197
- if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
219
+ if (!Object.hasOwn(current, part) || typeof current[part] !== 'object' || current[part] === null) {
198
220
  current[part] = {};
199
221
  }
200
222
  current = current[part];
@@ -25,7 +25,7 @@ export interface RestoreResult {
25
25
  restored: boolean;
26
26
  /** The backup file that was restored. */
27
27
  from?: string;
28
- /** memory_entries count in the restored DB (-1 if it couldn't be verified). */
28
+ /** Verified memory_entries count in the restored DB. */
29
29
  rows?: number;
30
30
  /** Where the corrupt live DB was parked before the swap. */
31
31
  corruptBackupPath?: string;
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import * as fs from 'fs';
16
16
  import * as path from 'path';
17
+ import { readFileMaybeEncrypted } from '../fs-secure.js';
17
18
  export function defaultMemoryDbPath(cwd = process.cwd()) {
18
19
  return path.join(cwd, '.swarm', 'memory.db');
19
20
  }
@@ -21,6 +22,19 @@ export function defaultMemoryDbPath(cwd = process.cwd()) {
21
22
  function fileStamp(ms) {
22
23
  return new Date(ms).toISOString().replace(/[:.]/g, '-');
23
24
  }
25
+ /** Keep legacy memory.db names; give every other basename its own snapshot set. */
26
+ function snapshotPrefix(dbPath) {
27
+ const name = path.basename(dbPath);
28
+ // A distinct namespace prevents an extensionless `memory` database from
29
+ // colliding with the historical `memory.db` prefix. Encoding is injective
30
+ // within the nondefault namespace, including names that contain `%`.
31
+ return name === 'memory.db' ? 'memory-' : `store-${encodeURIComponent(name)}-`;
32
+ }
33
+ function isSnapshotFor(file, dbPath) {
34
+ const prefix = snapshotPrefix(dbPath);
35
+ return file.startsWith(prefix)
36
+ && /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.db$/.test(file.slice(prefix.length));
37
+ }
24
38
  export async function backupMemoryDb(opts = {}) {
25
39
  const dbPath = opts.dbPath ?? defaultMemoryDbPath();
26
40
  if (!dbPath || !fs.existsSync(dbPath))
@@ -38,7 +52,7 @@ export async function backupMemoryDb(opts = {}) {
38
52
  fs.mkdirSync(destDir, { recursive: true });
39
53
  }
40
54
  catch { /* */ }
41
- const destPath = path.join(destDir, `memory-${fileStamp(opts.timestamp ?? Date.now())}.db`);
55
+ const destPath = path.join(destDir, `${snapshotPrefix(dbPath)}${fileStamp(opts.timestamp ?? Date.now())}.db`);
42
56
  // WAL-safe online backup: read-only source, consistent snapshot to destPath.
43
57
  let db;
44
58
  try {
@@ -68,7 +82,7 @@ export async function backupMemoryDb(opts = {}) {
68
82
  const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
69
83
  const rotatedAway = [];
70
84
  try {
71
- const snaps = fs.readdirSync(destDir).filter(f => /^memory-.*\.db$/.test(f)).sort();
85
+ const snaps = fs.readdirSync(destDir).filter(f => isSnapshotFor(f, dbPath)).sort();
72
86
  while (snaps.length > keep) {
73
87
  const old = snaps.shift();
74
88
  try {
@@ -107,7 +121,7 @@ export async function backupMemoryDb(opts = {}) {
107
121
  const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
108
122
  const rotatedAway = [];
109
123
  try {
110
- const snaps = fs.readdirSync(destDir).filter(f => /^memory-.*\.db$/.test(f)).sort();
124
+ const snaps = fs.readdirSync(destDir).filter(f => isSnapshotFor(f, dbPath)).sort();
111
125
  while (snaps.length > keep) {
112
126
  const old = snaps.shift();
113
127
  try {
@@ -136,6 +150,48 @@ export async function backupMemoryDb(opts = {}) {
136
150
  }
137
151
  return { backedUp: true, path: destPath, sizeBytes, rotatedAway, gcsUri };
138
152
  }
153
+ /** Verify a snapshot without modifying it, including RFE1 images and WASM-only hosts. */
154
+ async function verifiedBackupRows(file, Database) {
155
+ if (Database) {
156
+ let db;
157
+ try {
158
+ db = new Database(file, { readonly: true });
159
+ if (String(db.pragma('integrity_check', { simple: true })).toLowerCase() !== 'ok')
160
+ return null;
161
+ const rows = Number(db.prepare('SELECT COUNT(*) AS c FROM memory_entries').get()?.c ?? 0);
162
+ return rows > 0 ? rows : null;
163
+ }
164
+ catch {
165
+ // Native SQLite cannot open encrypted RFE1 images. Verify the decrypted
166
+ // bytes below; a failed native open is never evidence of a good backup.
167
+ }
168
+ finally {
169
+ try {
170
+ db?.close();
171
+ }
172
+ catch { /* best effort */ }
173
+ }
174
+ }
175
+ let db;
176
+ try {
177
+ const initSqlJs = (await import('sql.js')).default;
178
+ const SQL = await initSqlJs();
179
+ db = new SQL.Database(readFileMaybeEncrypted(file, null));
180
+ const integrity = db.exec('PRAGMA integrity_check')[0]?.values?.[0]?.[0];
181
+ const rows = Number(db.exec('SELECT COUNT(*) FROM memory_entries')[0]?.values?.[0]?.[0] ?? 0);
182
+ return String(integrity).toLowerCase() === 'ok' && rows > 0 ? rows : null;
183
+ }
184
+ catch {
185
+ // No verifier, wrong key, failed authentication, or invalid SQLite image.
186
+ return null;
187
+ }
188
+ finally {
189
+ try {
190
+ db?.close();
191
+ }
192
+ catch { /* best effort */ }
193
+ }
194
+ }
139
195
  /**
140
196
  * Restore the newest integrity-ok backup over a corrupt/malformed memory DB
141
197
  * (issue #2584).
@@ -158,7 +214,7 @@ export async function restoreMemoryDbFromBackup(dbPath, opts = {}) {
158
214
  try {
159
215
  snaps = fs
160
216
  .readdirSync(destDir)
161
- .filter(f => /^memory-.*\.db$/.test(f))
217
+ .filter(f => isSnapshotFor(f, dbPath))
162
218
  .map(f => path.join(destDir, f))
163
219
  .sort() // ISO-stamped names sort chronologically
164
220
  .reverse(); // newest first
@@ -168,39 +224,23 @@ export async function restoreMemoryDbFromBackup(dbPath, opts = {}) {
168
224
  }
169
225
  if (!snaps.length)
170
226
  return { restored: false, skipped: 'no-backups' };
171
- // better-sqlite3 verifies a candidate's integrity. Absent (WASM-only host) →
172
- // accept the newest non-empty snapshot, flagged rows=-1 (unverified).
227
+ // Prefer native verification for plaintext snapshots. sql.js verifies
228
+ // encrypted snapshots and provides the same checks on WASM-only hosts.
173
229
  let Database = null;
174
230
  try {
175
231
  const mod = 'better-sqlite3';
176
232
  Database = (await import(mod)).default;
177
233
  }
178
234
  catch {
179
- /* verifier unavailable — trust newest non-empty */
235
+ /* use the sql.js verifier */
180
236
  }
181
237
  let chosen = null;
182
238
  for (const file of snaps) {
183
- try {
184
- if (Database) {
185
- const db = new Database(file, { readonly: true });
186
- const integ = String(db.pragma('integrity_check', { simple: true }) ?? '');
187
- let rows = 0;
188
- try {
189
- rows = db.prepare('SELECT COUNT(*) AS c FROM memory_entries').get()?.c ?? 0;
190
- }
191
- catch { /* no entries table — not a usable memory DB */ }
192
- db.close();
193
- if (integ.toLowerCase() === 'ok' && rows > 0) {
194
- chosen = { file, rows };
195
- break;
196
- }
197
- }
198
- else if (fs.statSync(file).size > 0) {
199
- chosen = { file, rows: -1 };
200
- break;
201
- }
239
+ const rows = await verifiedBackupRows(file, Database);
240
+ if (rows !== null) {
241
+ chosen = { file, rows };
242
+ break;
202
243
  }
203
- catch { /* unreadable snapshot — try the next-older one */ }
204
244
  }
205
245
  if (!chosen)
206
246
  return { restored: false, skipped: 'no-integrity-ok-backup' };
@@ -124,8 +124,8 @@ export declare class WorkerDaemon extends EventEmitter {
124
124
  static isWslEnvironment(): boolean;
125
125
  static getEffectiveCpuCount(): number;
126
126
  /**
127
- * Read daemon-specific config from .claude-flow/config.{json,yaml,yml}.
128
- * Supports dot-notation keys like 'daemon.resourceThresholds.maxCpuLoad'.
127
+ * Read daemon-specific config from CLI JSON paths or .claude-flow/config.{yaml,yml}.
128
+ * Supports dotted and nested keys like 'daemon.resourceThresholds.maxCpuLoad'.
129
129
  * #1844: prefer JSON when both exist (existing behavior) but fall back
130
130
  * to YAML so operators using the v3 canonical YAML format aren't silently
131
131
  * ignored. The chosen path (or a parse warning) is returned rather than
@@ -10,6 +10,7 @@
10
10
  * causal_edges (30 min interval, ADR-174; RUFLO_DAEMON_NO_DISTILL=1 / --no-distill to skip)
11
11
  * - testgaps: Test coverage analysis (20 min interval)
12
12
  */
13
+ import { ConfigFileManager } from './config-file-manager.js';
13
14
  import { EventEmitter } from 'events';
14
15
  import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unlinkSync } from 'fs';
15
16
  import { cpus } from 'os';
@@ -339,8 +340,8 @@ export class WorkerDaemon extends EventEmitter {
339
340
  return cpus().length || 1;
340
341
  }
341
342
  /**
342
- * Read daemon-specific config from .claude-flow/config.{json,yaml,yml}.
343
- * Supports dot-notation keys like 'daemon.resourceThresholds.maxCpuLoad'.
343
+ * Read daemon-specific config from CLI JSON paths or .claude-flow/config.{yaml,yml}.
344
+ * Supports dotted and nested keys like 'daemon.resourceThresholds.maxCpuLoad'.
344
345
  * #1844: prefer JSON when both exist (existing behavior) but fall back
345
346
  * to YAML so operators using the v3 canonical YAML format aren't silently
346
347
  * ignored. The chosen path (or a parse warning) is returned rather than
@@ -352,13 +353,14 @@ export class WorkerDaemon extends EventEmitter {
352
353
  * The caller logs these once `this.config` exists.
353
354
  */
354
355
  readDaemonConfigFromFile(claudeFlowDir) {
355
- const jsonPath = join(claudeFlowDir, 'config.json');
356
+ // Share the CLI's JSON search order, including root and explicit config files.
357
+ const jsonPath = new ConfigFileManager().findConfig(join(claudeFlowDir, '..'));
356
358
  const yamlPath = join(claudeFlowDir, 'config.yaml');
357
359
  const ymlPath = join(claudeFlowDir, 'config.yml');
358
360
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
359
361
  let raw;
360
362
  let chosenPath;
361
- if (existsSync(jsonPath)) {
363
+ if (jsonPath) {
362
364
  try {
363
365
  raw = JSON.parse(readFileSync(jsonPath, 'utf-8'));
364
366
  chosenPath = jsonPath;
@@ -390,21 +392,28 @@ export class WorkerDaemon extends EventEmitter {
390
392
  return {};
391
393
  }
392
394
  try {
393
- // Support both flat keys at root and nested under scopes.project
394
- const cfg = raw?.scopes?.project ?? raw;
395
- const rawCpuLoad = cfg['daemon.resourceThresholds.maxCpuLoad'] ?? raw['daemon.resourceThresholds.maxCpuLoad'];
396
- const rawMinMem = cfg['daemon.resourceThresholds.minFreeMemoryPercent'] ?? raw['daemon.resourceThresholds.minFreeMemoryPercent'];
397
- const rawMaxConcurrent = cfg['daemon.maxConcurrent'] ?? raw['daemon.maxConcurrent'];
398
- const rawTimeout = cfg['daemon.workerTimeoutMs'] ?? raw['daemon.workerTimeoutMs'];
395
+ // The CLI writes nested objects; legacy files use dotted keys. Scoped
396
+ // values retain precedence, including explicit false and zero.
397
+ const read = (obj, key) => {
398
+ if (!obj)
399
+ return undefined;
400
+ return obj[key] ?? key.split('.').reduce((value, part) => value && typeof value === 'object' && Object.hasOwn(value, part) ? value[part] : undefined, obj);
401
+ };
402
+ const get = (key) => read(raw?.scopes?.project, key) ?? read(raw, key);
403
+ const rawAutoStart = get('daemon.autoStart');
404
+ const rawCpuLoad = get('daemon.resourceThresholds.maxCpuLoad');
405
+ const rawMinMem = get('daemon.resourceThresholds.minFreeMemoryPercent');
406
+ const rawMaxConcurrent = get('daemon.maxConcurrent');
407
+ const rawTimeout = get('daemon.workerTimeoutMs');
399
408
  // #2356 — lifecycle limits are configured in SECONDS in config.json
400
409
  // (`daemon.ttlSecs` / `daemon.idleSecs`) for parity with the CLI flag
401
410
  // and env var; stored internally as ms. An explicit 0 disables.
402
- const rawTtl = cfg['daemon.ttlSecs'] ?? raw['daemon.ttlSecs'];
403
- const rawIdle = cfg['daemon.idleSecs'] ?? raw['daemon.idleSecs'];
411
+ const rawTtl = get('daemon.ttlSecs');
412
+ const rawIdle = get('daemon.idleSecs');
404
413
  // #2661 — explicit opt-in for scheduled AI workers.
405
- const rawAiEnabled = cfg['daemon.aiWorkers.enabled'] ?? raw['daemon.aiWorkers.enabled'];
414
+ const rawAiEnabled = get('daemon.aiWorkers.enabled');
406
415
  return {
407
- autoStart: typeof raw['daemon.autoStart'] === 'boolean' ? raw['daemon.autoStart'] : undefined,
416
+ autoStart: typeof rawAutoStart === 'boolean' ? rawAutoStart : undefined,
408
417
  maxConcurrent: (typeof rawMaxConcurrent === 'number' && rawMaxConcurrent > 0) ? rawMaxConcurrent : undefined,
409
418
  workerTimeoutMs: (typeof rawTimeout === 'number' && rawTimeout > 0) ? rawTimeout : undefined,
410
419
  maxCpuLoad: (typeof rawCpuLoad === 'number' && rawCpuLoad > 0 && rawCpuLoad < 1000) ? rawCpuLoad : undefined,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.47.1",
3
+ "version": "3.48.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -98,13 +98,13 @@
98
98
  "vitest": "^4.1.0"
99
99
  },
100
100
  "dependencies": {
101
- "@claude-flow/cli-core": "3.7.0-alpha.6",
101
+ "@claude-flow/cli-core": "3.7.0",
102
102
  "@claude-flow/codex": "3.0.3",
103
- "@claude-flow/mcp": "3.0.0-alpha.10",
104
- "@claude-flow/neural": "3.0.0-alpha.10",
105
- "@claude-flow/plugin-agent-federation": "1.0.0-alpha.18",
106
- "@claude-flow/security": "3.0.0-alpha.14",
107
- "@claude-flow/shared": "3.0.0-alpha.7",
103
+ "@claude-flow/mcp": "3.0.0",
104
+ "@claude-flow/neural": "3.0.0",
105
+ "@claude-flow/plugin-agent-federation": "1.0.0",
106
+ "@claude-flow/security": "3.0.0",
107
+ "@claude-flow/shared": "3.0.0",
108
108
  "@iarna/toml": "^2.2.5",
109
109
  "@noble/ed25519": "2.3.0",
110
110
  "@ruvector/rabitq-wasm": "0.1.0",
@@ -125,11 +125,11 @@
125
125
  "ws": "^8.21.0",
126
126
  "yaml": "^2.8.0",
127
127
  "zod": "^3.22.0",
128
- "@claude-flow/memory": "3.0.0-alpha.26"
128
+ "@claude-flow/memory": "3.0.0"
129
129
  },
130
130
  "optionalDependencies": {
131
131
  "@agntcy/slim-bindings": "2.0.0-alpha.5",
132
- "@claude-flow/memory": "3.0.0-alpha.26",
132
+ "@claude-flow/memory": "3.0.0",
133
133
  "@metaharness/darwin": "~0.10.2",
134
134
  "@metaharness/flywheel": "~0.1.10",
135
135
  "@metaharness/radio": "~0.1.0",