claude-flow 3.47.0 → 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 (52) 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/hive-mind-worker.d.ts +88 -0
  18. package/v3/@claude-flow/cli/dist/src/commands/hive-mind-worker.js +242 -0
  19. package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +24 -41
  20. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +15 -5
  21. package/v3/@claude-flow/cli/dist/src/commands/memory.js +89 -27
  22. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
  23. package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
  24. package/v3/@claude-flow/cli/dist/src/commands/session.js +6 -6
  25. package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
  26. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
  27. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
  28. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
  29. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +0 -4
  30. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +139 -37
  31. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +89 -35
  32. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
  33. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
  34. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +34 -8
  35. package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +73 -53
  36. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
  37. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
  38. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
  39. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
  40. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
  41. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
  42. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
  43. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +41 -31
  44. package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
  45. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
  46. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
  47. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
  48. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
  49. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
  50. package/v3/@claude-flow/cli/package.json +9 -9
  51. package/v3/@claude-flow/guidance/package.json +5 -6
  52. package/v3/@claude-flow/shared/package.json +6 -3
@@ -1,3 +1,4 @@
1
+ import { feedbackPatternsSchema, validateFeedbackPatterns } from '../memory/feedback-patterns.js';
1
2
  /**
2
3
  * Hooks MCP Tools
3
4
  * Provides intelligent hooks functionality via MCP protocol
@@ -410,8 +411,75 @@ async function getLoRAAdapter() {
410
411
  }
411
412
  return loraAdapter;
412
413
  }
413
- // In-memory trajectory tracking (persisted on end)
414
+ // Cache pending trajectories; the existing memory store also persists checkpoints.
414
415
  const activeTrajectories = new Map();
416
+ const dirtyTrajectories = new Set();
417
+ const trajectoryOperations = new Map();
418
+ /** Serialize read/modify/checkpoint operations for a trajectory in this process. */
419
+ async function withTrajectoryOperation(id, operation) {
420
+ const previous = trajectoryOperations.get(id) ?? Promise.resolve();
421
+ let release;
422
+ const gate = new Promise(resolve => { release = resolve; });
423
+ const current = previous.then(() => gate);
424
+ trajectoryOperations.set(id, current);
425
+ await previous;
426
+ try {
427
+ return await operation();
428
+ }
429
+ finally {
430
+ release();
431
+ if (trajectoryOperations.get(id) === current)
432
+ trajectoryOperations.delete(id);
433
+ }
434
+ }
435
+ async function persistPendingTrajectory(trajectory) {
436
+ dirtyTrajectories.add(trajectory.id);
437
+ try {
438
+ const store = await getRealStoreFunction();
439
+ const result = await store?.({
440
+ key: `trajectory-pending-${trajectory.id}`, value: JSON.stringify(trajectory),
441
+ namespace: 'trajectories', tags: [trajectory.agent, 'pending', 'sona-trajectory'], upsert: true,
442
+ });
443
+ if (result?.success === true)
444
+ dirtyTrajectories.delete(trajectory.id);
445
+ return result?.success === true;
446
+ }
447
+ catch {
448
+ return false;
449
+ }
450
+ }
451
+ async function loadPendingTrajectory(id) {
452
+ try {
453
+ const { getEntry } = await import('../memory/memory-initializer.js');
454
+ // A completed record is authoritative even if pending-record cleanup failed
455
+ // or another process still has an old cache entry.
456
+ const completed = await getEntry({ key: `trajectory-${id}`, namespace: 'trajectories' });
457
+ if (completed.success && completed.found) {
458
+ activeTrajectories.delete(id);
459
+ dirtyTrajectories.delete(id);
460
+ return undefined;
461
+ }
462
+ // Failed checkpoints must not erase already-acknowledged live steps.
463
+ // Clean caches still reload so another process's later checkpoint is seen.
464
+ if (dirtyTrajectories.has(id) && activeTrajectories.has(id))
465
+ return activeTrajectories.get(id);
466
+ const pending = await getEntry({ key: `trajectory-pending-${id}`, namespace: 'trajectories' });
467
+ if (pending.success && pending.found) {
468
+ const value = JSON.parse(pending.entry?.content ?? 'null');
469
+ if (!value || value.id !== id || typeof value.task !== 'string' || typeof value.agent !== 'string'
470
+ || typeof value.startedAt !== 'string' || !Number.isFinite(Date.parse(value.startedAt))
471
+ || !Array.isArray(value.steps) || !value.steps.every((step) => step
472
+ && typeof step.action === 'string' && typeof step.result === 'string'
473
+ && typeof step.quality === 'number' && Number.isFinite(step.quality)
474
+ && typeof step.timestamp === 'string'))
475
+ return undefined;
476
+ activeTrajectories.set(id, value);
477
+ return value;
478
+ }
479
+ }
480
+ catch { /* store unavailable: retain same-process best-effort tracking */ }
481
+ return activeTrajectories.get(id);
482
+ }
415
483
  const MEMORY_DIR = '.claude-flow/memory';
416
484
  const MEMORY_FILE = 'store.json';
417
485
  function getMemoryPath() {
@@ -897,13 +965,19 @@ export const hooksPostCommand = {
897
965
  properties: {
898
966
  command: { type: 'string', description: 'Executed command' },
899
967
  exitCode: { type: 'number', description: 'Command exit code' },
968
+ success: { type: 'boolean', description: 'Explicit execution outcome; false records a failure even without a nonzero exit code' },
969
+ ttl: { type: 'integer', minimum: 1, maximum: 2147483647, description: 'Command history lifetime in seconds (default: 30 days)' },
900
970
  },
901
971
  required: ['command'],
902
972
  },
903
973
  handler: async (params) => {
904
974
  const command = params.command;
905
975
  const exitCode = params.exitCode || 0;
906
- const success = exitCode === 0;
976
+ const success = exitCode === 0 && params.success !== false;
977
+ const ttl = params.ttl ?? 30 * 24 * 60 * 60;
978
+ if (typeof ttl !== 'number' || !Number.isSafeInteger(ttl) || ttl < 1 || ttl > 2147483647) {
979
+ return { success: false, recorded: false, error: 'ttl must be a positive integer number of seconds (at most 2147483647)' };
980
+ }
907
981
  {
908
982
  const v = validateText(command, 'command');
909
983
  if (!v.valid)
@@ -916,21 +990,35 @@ export const hooksPostCommand = {
916
990
  let _storedIn = 'none';
917
991
  try {
918
992
  const bridge = await import('../memory/memory-bridge.js');
919
- await bridge.bridgeStoreEntry({
993
+ const stored = await bridge.bridgeStoreEntry({
920
994
  key: `cmd-${Date.now()}`,
921
995
  value: JSON.stringify({ command, exitCode, success }),
922
996
  namespace: 'commands',
923
997
  tags: [success ? 'success' : 'error'],
998
+ ttl,
999
+ generateEmbeddingFlag: false,
924
1000
  });
1001
+ if (!stored?.success)
1002
+ throw new Error(stored?.error || 'Native bridge did not store the command');
925
1003
  _storedIn = 'agentdb';
926
1004
  }
927
1005
  catch {
928
1006
  // AgentDB not available — store in JSON
929
1007
  try {
930
1008
  const store = loadMemoryStore();
931
- const key = `cmd-${Date.now()}`;
932
- store.entries[key] = { key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands', createdAt: new Date().toISOString() };
933
- const memDir = resolve(MEMORY_DIR);
1009
+ const now = Date.now();
1010
+ for (const [entryKey, entry] of Object.entries(store.entries)) {
1011
+ if (entry.namespace === 'commands' && entry.expiresAt && Date.parse(entry.expiresAt) <= now) {
1012
+ delete store.entries[entryKey];
1013
+ }
1014
+ }
1015
+ const key = `cmd-${now}`;
1016
+ store.entries[key] = {
1017
+ key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands',
1018
+ storedAt: new Date(now).toISOString(), lastAccessed: new Date(now).toISOString(), accessCount: 0,
1019
+ expiresAt: new Date(now + ttl * 1000).toISOString(),
1020
+ };
1021
+ const memDir = dirname(getMemoryPath());
934
1022
  if (!existsSync(memDir))
935
1023
  mkdirSync(memDir, { recursive: true });
936
1024
  writeFileSync(getMemoryPath(), JSON.stringify(store, null, 2), 'utf-8');
@@ -1444,9 +1532,10 @@ export const hooksPostTask = {
1444
1532
  type: 'object',
1445
1533
  properties: {
1446
1534
  taskId: { type: 'string', description: 'Task identifier' },
1535
+ patterns: feedbackPatternsSchema,
1447
1536
  success: { type: 'boolean', description: 'Whether task was successful' },
1448
1537
  agent: { type: 'string', description: 'Agent that completed the task' },
1449
- quality: { type: 'number', description: 'Quality score (0-1)' },
1538
+ quality: { type: 'number', minimum: 0, maximum: 1, description: 'Quality score (0-1)' },
1450
1539
  task: { type: 'string', description: 'Task description text (used for learning keyword extraction)' },
1451
1540
  duration: { type: 'number', description: 'Observed task duration in milliseconds (used by pheromone-adaptive topology)' },
1452
1541
  latencyBudgetMs: { type: 'number', description: 'Latency budget used to normalize duration (default 60000ms)' },
@@ -1463,8 +1552,19 @@ export const hooksPostTask = {
1463
1552
  const taskId = params.taskId;
1464
1553
  const success = params.success !== false;
1465
1554
  const agent = params.agent;
1466
- const quality = params.quality || (success ? 0.85 : 0.3);
1555
+ if (params.quality !== undefined && (typeof params.quality !== 'number'
1556
+ || !Number.isFinite(params.quality) || params.quality < 0 || params.quality > 1)) {
1557
+ return { success: false, error: 'quality must be a finite number between 0 and 1' };
1558
+ }
1559
+ const quality = params.quality === undefined ? (success ? 0.85 : 0.3) : params.quality;
1467
1560
  const startTime = Date.now();
1561
+ let patterns;
1562
+ try {
1563
+ patterns = validateFeedbackPatterns(params.patterns);
1564
+ }
1565
+ catch (error) {
1566
+ return { success: false, error: error.message };
1567
+ }
1468
1568
  {
1469
1569
  const v = validateIdentifier(taskId, 'taskId');
1470
1570
  if (!v.valid)
@@ -1502,7 +1602,7 @@ export const hooksPostTask = {
1502
1602
  quality,
1503
1603
  agent,
1504
1604
  duration: params.duration || undefined,
1505
- patterns: params.patterns || undefined,
1605
+ patterns,
1506
1606
  // ADR-147 P2: forward spawn-tree lineage so it lands in feedback + memory
1507
1607
  parentAgentId,
1508
1608
  depth,
@@ -2754,9 +2854,8 @@ export const hooksIntelligenceReset = {
2754
2854
  // Directory read failed
2755
2855
  }
2756
2856
  }
2757
- // Clear in-memory trajectories
2758
- cleared.trajectories = activeTrajectories.size;
2759
- activeTrajectories.clear();
2857
+ // Reset learned state without abandoning tasks still recording outcomes.
2858
+ cleared.trajectories = 0;
2760
2859
  return {
2761
2860
  reset: true,
2762
2861
  cleared,
@@ -2815,27 +2914,15 @@ export const hooksTrajectoryStart = {
2815
2914
  getTrajectoryTree().openTrajectory({ sessionId, trajectoryId, task, agent });
2816
2915
  }
2817
2916
  catch { /* prototype path — never blocks trajectory recording */ }
2818
- // Persist pending trajectory to disk so it survives MCP restarts
2819
- const storeFn = await getRealStoreFunction();
2820
- if (storeFn) {
2821
- try {
2822
- await storeFn({
2823
- key: `trajectory-pending-${trajectoryId}`,
2824
- value: JSON.stringify(trajectory),
2825
- namespace: 'trajectories',
2826
- tags: [agent, 'pending', 'sona-trajectory'],
2827
- });
2828
- }
2829
- catch {
2830
- // Best-effort persistence — trajectory still lives in-memory
2831
- }
2832
- }
2917
+ // Checkpoint before handing the id to another process.
2918
+ const persisted = await persistPendingTrajectory(trajectory);
2833
2919
  return {
2834
2920
  trajectoryId,
2835
2921
  task,
2836
2922
  agent,
2837
2923
  started: startedAt,
2838
2924
  status: 'recording',
2925
+ persisted,
2839
2926
  implementation: 'real-trajectory-tracking',
2840
2927
  activeCount: activeTrajectories.size,
2841
2928
  };
@@ -2850,17 +2937,21 @@ export const hooksTrajectoryStep = {
2850
2937
  trajectoryId: { type: 'string', description: 'Trajectory ID' },
2851
2938
  action: { type: 'string', description: 'Action taken' },
2852
2939
  result: { type: 'string', description: 'Action result' },
2853
- quality: { type: 'number', description: 'Quality score (0-1)' },
2940
+ quality: { type: 'number', minimum: 0, maximum: 1, description: 'Quality score (0-1)' },
2854
2941
  },
2855
2942
  required: ['trajectoryId', 'action'],
2856
2943
  },
2857
- handler: async (params) => {
2944
+ handler: async (params) => withTrajectoryOperation(params.trajectoryId, async () => {
2858
2945
  const trajectoryId = params.trajectoryId;
2859
2946
  // #14: scrub extended-thinking blocks so reasoning tokens don't contaminate
2860
2947
  // the learning signal (DISTILL embeds this text).
2861
2948
  const action = scrubReasoningBlocks(params.action);
2862
2949
  const result = scrubReasoningBlocks(params.result || 'success');
2863
- const quality = params.quality || 0.85;
2950
+ if (params.quality !== undefined && (typeof params.quality !== 'number'
2951
+ || !Number.isFinite(params.quality) || params.quality < 0 || params.quality > 1)) {
2952
+ return { success: false, error: 'quality must be a finite number between 0 and 1' };
2953
+ }
2954
+ const quality = params.quality === undefined ? 0.85 : params.quality;
2864
2955
  const timestamp = new Date().toISOString();
2865
2956
  const stepId = `step-${Date.now()}`;
2866
2957
  {
@@ -2874,7 +2965,7 @@ export const hooksTrajectoryStep = {
2874
2965
  return { success: false, error: v.error };
2875
2966
  }
2876
2967
  // Add step to real trajectory if it exists
2877
- const trajectory = activeTrajectories.get(trajectoryId);
2968
+ const trajectory = await loadPendingTrajectory(trajectoryId);
2878
2969
  if (trajectory) {
2879
2970
  trajectory.steps.push({
2880
2971
  action,
@@ -2883,6 +2974,7 @@ export const hooksTrajectoryStep = {
2883
2974
  timestamp,
2884
2975
  });
2885
2976
  }
2977
+ const persisted = trajectory ? await persistPendingTrajectory(trajectory) : false;
2886
2978
  // MAGE-style execution-state-tree mirror (prototype, non-fatal)
2887
2979
  try {
2888
2980
  const { getTrajectoryTree } = await import('../ruvector/trajectory-tree.js');
@@ -2914,11 +3006,12 @@ export const hooksTrajectoryStep = {
2914
3006
  result,
2915
3007
  quality,
2916
3008
  recorded: !!trajectory,
3009
+ persisted,
2917
3010
  timestamp,
2918
3011
  totalSteps: trajectory?.steps.length || 0,
2919
3012
  implementation: trajectory ? 'real-step-recording' : 'trajectory-not-found',
2920
3013
  };
2921
- },
3014
+ }),
2922
3015
  };
2923
3016
  export const hooksTrajectoryEnd = {
2924
3017
  name: 'hooks_intelligence_trajectory-end',
@@ -2932,7 +3025,7 @@ export const hooksTrajectoryEnd = {
2932
3025
  },
2933
3026
  required: ['trajectoryId'],
2934
3027
  },
2935
- handler: async (params) => {
3028
+ handler: async (params) => withTrajectoryOperation(params.trajectoryId, async () => {
2936
3029
  const trajectoryId = params.trajectoryId;
2937
3030
  {
2938
3031
  const v = validateIdentifier(trajectoryId, 'trajectoryId');
@@ -2944,7 +3037,7 @@ export const hooksTrajectoryEnd = {
2944
3037
  const endedAt = new Date().toISOString();
2945
3038
  const startTime = Date.now();
2946
3039
  // Get and finalize real trajectory
2947
- const trajectory = activeTrajectories.get(trajectoryId);
3040
+ const trajectory = await loadPendingTrajectory(trajectoryId);
2948
3041
  let persistResult = { success: false };
2949
3042
  if (trajectory) {
2950
3043
  trajectory.success = success;
@@ -2970,8 +3063,17 @@ export const hooksTrajectoryEnd = {
2970
3063
  persistResult = { success: false, error: error instanceof Error ? error.message : String(error) };
2971
3064
  }
2972
3065
  }
2973
- // Remove from active trajectories
2974
- activeTrajectories.delete(trajectoryId);
3066
+ // Failed writes remain retryable. A completed row prevents stale pending
3067
+ // data from being replayed even if cleanup itself fails.
3068
+ if (persistResult.success) {
3069
+ activeTrajectories.delete(trajectoryId);
3070
+ dirtyTrajectories.delete(trajectoryId);
3071
+ try {
3072
+ const { deleteEntry } = await import('../memory/memory-initializer.js');
3073
+ await deleteEntry({ key: `trajectory-pending-${trajectoryId}`, namespace: 'trajectories' });
3074
+ }
3075
+ catch { /* completed record is the replay guard */ }
3076
+ }
2975
3077
  }
2976
3078
  // MAGE-style execution-state-tree mirror (prototype, non-fatal)
2977
3079
  try {
@@ -3187,7 +3289,7 @@ export const hooksTrajectoryEnd = {
3187
3289
  ? `SONA learned pattern "${sonaResult.patternKey}" with ${(sonaResult.confidence * 100).toFixed(1)}% confidence`
3188
3290
  : (persistResult.success ? 'Trajectory persisted for future learning' : (persistResult.error || 'Trajectory not found')),
3189
3291
  };
3190
- },
3292
+ }),
3191
3293
  };
3192
3294
  // Pattern store/search hooks - REAL implementation using storeEntry
3193
3295
  export const hooksPatternStore = {
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module v3/cli/mcp-tools/memory-tools
11
11
  */
12
- import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
13
13
  import { homedir } from 'os';
14
14
  import { join, resolve } from 'path';
15
15
  import { createHash } from 'crypto';
@@ -273,11 +273,11 @@ const MEMORY_STATS_MAX_PAGES = 200;
273
273
  * breakdown and embedding coverage computed from a prefix, and a listing
274
274
  * that reported `success: false` was read as an empty one.
275
275
  */
276
- async function collectAllEntries(listEntries) {
276
+ async function collectAllEntries(listEntries, dbPath) {
277
277
  const entries = [];
278
278
  let total = 0;
279
279
  for (let page = 0; page < MEMORY_STATS_MAX_PAGES; page++) {
280
- const result = await listEntries({ limit: MEMORY_STATS_PAGE, offset: entries.length });
280
+ const result = await listEntries({ limit: MEMORY_STATS_PAGE, offset: entries.length, dbPath });
281
281
  if (!result.success) {
282
282
  return { success: false, entries, total: result.total ?? total, error: result.error };
283
283
  }
@@ -300,10 +300,10 @@ function memoryStatsUnavailable(error) {
300
300
  * WAL frames, so it is allowed to fail without that meaning the store is
301
301
  * missing — which is exactly the conflation #3311 reports.
302
302
  */
303
- async function readMemoryStatusLabels() {
303
+ async function readMemoryStatusLabels(dbPath) {
304
304
  try {
305
305
  const { checkMemoryInitialization } = await getMemoryFunctions();
306
- const status = await checkMemoryInitialization();
306
+ const status = await checkMemoryInitialization(dbPath);
307
307
  return { version: status.version, features: status.features };
308
308
  }
309
309
  catch {
@@ -315,37 +315,43 @@ async function readMemoryStatusLabels() {
315
315
  * #1606: Wrapped in try/catch to prevent process-level crashes that kill
316
316
  * the stdio MCP transport on Windows/Codex.
317
317
  */
318
- async function ensureInitialized() {
318
+ async function ensureInitialized(dbPath) {
319
319
  try {
320
320
  const { initializeMemoryDatabase, checkMemoryInitialization, storeEntry } = await getMemoryFunctions();
321
321
  // Check if already initialized
322
- const status = await checkMemoryInitialization();
322
+ const status = await checkMemoryInitialization(dbPath);
323
323
  if (!status.initialized) {
324
- await initializeMemoryDatabase({ force: false, verbose: false });
324
+ await initializeMemoryDatabase({ force: false, verbose: false, dbPath });
325
325
  }
326
326
  // Migrate legacy JSON data if exists (from old .claude-flow/memory/ location)
327
+ // A custom import target must not consume the one-time migration marker
328
+ // for the project's normal memory store.
327
329
  if (hasLegacyStore()) {
328
- const legacyStore = loadLegacyStore();
329
- if (legacyStore && Object.keys(legacyStore.entries).length > 0) {
330
- console.error('[MCP Memory] Migrating legacy JSON store to sql.js...');
331
- let migrated = 0;
332
- for (const [key, entry] of Object.entries(legacyStore.entries)) {
333
- try {
334
- const value = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value);
335
- await storeEntry({
336
- key,
337
- value,
338
- namespace: 'default',
339
- generateEmbeddingFlag: true,
340
- });
341
- migrated++;
342
- }
343
- catch (e) {
344
- console.error(`[MCP Memory] Failed to migrate key "${key}":`, e);
330
+ const { resolveDbPath } = await import('../memory/memory-initializer.js');
331
+ if (!dbPath || dbPath === resolveDbPath()) {
332
+ const legacyStore = loadLegacyStore();
333
+ if (legacyStore && Object.keys(legacyStore.entries).length > 0) {
334
+ console.error('[MCP Memory] Migrating legacy JSON store to sql.js...');
335
+ let migrated = 0;
336
+ for (const [key, entry] of Object.entries(legacyStore.entries)) {
337
+ try {
338
+ const value = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value);
339
+ await storeEntry({
340
+ key,
341
+ value,
342
+ namespace: 'default',
343
+ generateEmbeddingFlag: true,
344
+ dbPath,
345
+ });
346
+ migrated++;
347
+ }
348
+ catch (e) {
349
+ console.error(`[MCP Memory] Failed to migrate key "${key}":`, e);
350
+ }
345
351
  }
352
+ console.error(`[MCP Memory] Migrated ${migrated}/${Object.keys(legacyStore.entries).length} entries`);
353
+ markMigrationComplete();
346
354
  }
347
- console.error(`[MCP Memory] Migrated ${migrated}/${Object.keys(legacyStore.entries).length} entries`);
348
- markMigrationComplete();
349
355
  }
350
356
  }
351
357
  }
@@ -792,10 +798,18 @@ export const memoryTools = [
792
798
  category: 'memory',
793
799
  inputSchema: {
794
800
  type: 'object',
795
- properties: {},
801
+ properties: {
802
+ dbPath: { type: 'string', description: 'Database file to inspect; omitted uses the MCP store default' },
803
+ },
796
804
  },
797
- handler: async () => {
798
- await ensureInitialized();
805
+ handler: async (input) => {
806
+ if (input.dbPath !== undefined && (typeof input.dbPath !== 'string' || !input.dbPath.trim())) {
807
+ return memoryStatsUnavailable('dbPath must be a non-empty string');
808
+ }
809
+ const dbPath = typeof input.dbPath === 'string' ? resolve(input.dbPath) : undefined;
810
+ // An explicit read must not initialize or migrate the unrelated default store.
811
+ if (!dbPath)
812
+ await ensureInitialized();
799
813
  const { listEntries } = await getMemoryFunctions();
800
814
  // #3311: the store's own listing decides whether memory is there.
801
815
  // `checkMemoryInitialization` opens a whole-image sql.js snapshot of
@@ -806,7 +820,7 @@ export const memoryTools = [
806
820
  // but it no longer gets to overrule a working store.
807
821
  let listing;
808
822
  try {
809
- listing = await collectAllEntries(listEntries);
823
+ listing = await collectAllEntries(listEntries, dbPath);
810
824
  }
811
825
  catch (error) {
812
826
  return memoryStatsUnavailable(error instanceof Error ? error.message : 'Unknown error');
@@ -822,16 +836,49 @@ export const memoryTools = [
822
836
  // from the breakdown while still being counted in the total.
823
837
  const namespaces = Object.create(null);
824
838
  let withEmbeddings = 0;
839
+ let oldest = Infinity;
840
+ let newest = -Infinity;
825
841
  for (const entry of listing.entries) {
826
842
  namespaces[entry.namespace] = (namespaces[entry.namespace] || 0) + 1;
827
843
  if (entry.hasEmbedding)
828
844
  withEmbeddings++;
845
+ // AgentDB returns INTEGER epoch milliseconds; legacy sql.js rows
846
+ // may expose ISO strings or numeric strings from SQLite TEXT affinity.
847
+ const rawCreated = entry.createdAt;
848
+ const millis = typeof rawCreated === 'number'
849
+ ? rawCreated
850
+ : typeof rawCreated === 'string' && /^-?\d+$/.test(rawCreated)
851
+ ? Number(rawCreated)
852
+ : Date.parse(rawCreated ?? '');
853
+ const created = new Date(millis).getTime();
854
+ if (Number.isFinite(created)) {
855
+ oldest = Math.min(oldest, created);
856
+ newest = Math.max(newest, created);
857
+ }
829
858
  }
830
859
  const counted = listing.entries.length;
831
- const status = await readMemoryStatusLabels();
860
+ const status = await readMemoryStatusLabels(dbPath);
861
+ let totalSize = null;
862
+ if (dbPath) {
863
+ try {
864
+ let bytes = statSync(dbPath).size;
865
+ try {
866
+ bytes += statSync(dbPath + '-wal').size;
867
+ }
868
+ catch (error) {
869
+ if (error.code !== 'ENOENT')
870
+ throw error;
871
+ }
872
+ totalSize = `${bytes} B`;
873
+ }
874
+ catch { /* unavailable file metadata is unknown, not zero */ }
875
+ }
832
876
  return {
833
877
  initialized: true,
834
878
  totalEntries: listing.total,
879
+ ...(dbPath ? { location: dbPath, totalSize } : {}),
880
+ oldestEntry: counted >= listing.total && Number.isFinite(oldest) ? new Date(oldest).toISOString() : null,
881
+ newestEntry: counted >= listing.total && Number.isFinite(newest) ? new Date(newest).toISOString() : null,
835
882
  entriesCounted: counted,
836
883
  // The breakdown below covers `entriesCounted` rows, which is every
837
884
  // row unless the listing was truncated; say so rather than letting
@@ -1484,11 +1531,15 @@ export const memoryTools = [
1484
1531
  inputPath: { type: 'string', description: 'Path to the JSON export file' },
1485
1532
  merge: { type: 'boolean', description: 'Merge into existing entries (upsert) vs. fail on conflict (default true)' },
1486
1533
  namespace: { type: 'string', description: 'Override the namespace for all imported entries' },
1534
+ dbPath: { type: 'string', description: 'Database file to import into (defaults to the MCP memory store)' },
1487
1535
  },
1488
1536
  required: ['inputPath'],
1489
1537
  },
1490
1538
  handler: async (input) => {
1491
- await ensureInitialized();
1539
+ const dbPath = typeof input.dbPath === 'string' && input.dbPath.trim()
1540
+ ? resolve(input.dbPath)
1541
+ : undefined;
1542
+ await ensureInitialized(dbPath);
1492
1543
  const { storeEntry } = await getMemoryFunctions();
1493
1544
  const t0 = Date.now();
1494
1545
  const inputPath = String(input.inputPath ?? '');
@@ -1517,8 +1568,11 @@ export const memoryTools = [
1517
1568
  }
1518
1569
  const value = typeof e.value === 'string' ? e.value : JSON.stringify(e.value ?? null);
1519
1570
  try {
1520
- await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false });
1521
- imported++;
1571
+ const result = await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false, dbPath });
1572
+ if (result.success)
1573
+ imported++;
1574
+ else
1575
+ skipped++;
1522
1576
  }
1523
1577
  catch {
1524
1578
  skipped++;
@@ -62,6 +62,12 @@ export interface McpPolicy {
62
62
  harnessId?: string;
63
63
  defaultDeny?: boolean;
64
64
  auditLog?: boolean;
65
+ /** Explicit destination; takes precedence over the environment and project default. */
66
+ auditLogPath?: string;
67
+ /** Maximum bytes per audit segment. Default 10 MiB; oversized records fail closed. */
68
+ auditLogMaxBytes?: number;
69
+ /** Rotated segments retained in addition to the active log. Default 5, maximum 100. */
70
+ auditLogMaxFiles?: number;
65
71
  requireApprovalForDangerous?: boolean;
66
72
  toolTimeoutMs?: number;
67
73
  /**
@@ -103,10 +109,11 @@ export interface AuditLogEntry {
103
109
  toolName: string;
104
110
  allowed: boolean;
105
111
  reason?: string;
112
+ projectPath?: string;
106
113
  }
107
114
  /** Test-only: redirect the audit log to a temp file instead of the default path. */
108
115
  export declare function setAuditLogPathForTesting(p: string | null): void;
109
- export declare function getAuditLogPath(): string;
116
+ export declare function getAuditLogPath(options?: Pick<McpPolicy, 'auditLogPath'>): string;
110
117
  /**
111
118
  * Appends one JSONL audit record. Returns `true` if `policy.auditLog` is not
112
119
  * set (nothing was required) or the write succeeded; `false` only when