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
@@ -9,6 +9,8 @@ import { distillCommand } from './memory-distill.js';
9
9
  import { backupCommand } from './memory-backup.js';
10
10
  import { countSiblingStoreRows } from '../memory/sibling-store.js';
11
11
  import { resolveDbPath } from '../memory/memory-initializer.js';
12
+ import { existsSync } from 'node:fs';
13
+ import { siblingAgentDbPath } from '../memory/memory-bridge.js';
12
14
  /**
13
15
  * #3228: a miss in one store is not a miss in the memory.
14
16
  *
@@ -26,6 +28,24 @@ async function warnIfSiblingHasRows(pathFlag) {
26
28
  `That store is written by the MCP/AgentDB path; read it with --path ${unread.path}.`);
27
29
  }
28
30
  }
31
+ /** Default CLI writes may be mirrored into AgentDB; remove both copies. */
32
+ function removalDbTargets(pathFlag) {
33
+ const primary = resolveDbPath(pathFlag);
34
+ // An explicit file selection is a single-store operation, as documented by
35
+ // --path. Do not widen a caller's requested destructive scope.
36
+ if (pathFlag || process.env.CLAUDE_FLOW_DB_PATH)
37
+ return [{ dbPath: primary }];
38
+ const sibling = siblingAgentDbPath(primary);
39
+ // The AgentDB mirror is consumed by native SQLite and must stay plaintext.
40
+ // Pass this per operation, including preview migrations; never toggle the
41
+ // process encryption setting while asynchronous operations are in flight.
42
+ const targets = [
43
+ { dbPath: primary },
44
+ ...(sibling ? [{ dbPath: sibling, encryptWrites: false }] : []),
45
+ ].filter(target => existsSync(target.dbPath));
46
+ // Preserve the existing missing-database error when neither store exists.
47
+ return targets.length ? targets : [{ dbPath: primary }];
48
+ }
29
49
  // Memory backends
30
50
  const BACKENDS = [
31
51
  { value: 'agentdb', label: 'AgentDB', hint: 'Vector database with HNSW indexing (150x-12,500x faster)' },
@@ -820,7 +840,7 @@ function formatRelativeTime(isoDate) {
820
840
  const deleteCommand = {
821
841
  name: 'delete',
822
842
  aliases: ['rm'],
823
- description: 'Delete memory entry',
843
+ description: 'Delete a memory entry from the default store and its AgentDB mirror (use --path to select one store)',
824
844
  options: [
825
845
  {
826
846
  name: 'key',
@@ -870,16 +890,24 @@ const deleteCommand = {
870
890
  }
871
891
  // Use sql.js directly for consistent data access (Issue #980)
872
892
  try {
873
- const { deleteEntry, resolveDbPath: _rdbDelete } = await import('../memory/memory-initializer.js');
874
- const dbPathDelete = _rdbDelete(ctx.flags.path);
875
- const result = await deleteEntry({ key, namespace, dbPath: dbPathDelete });
876
- if (!result.success) {
877
- output.printError(result.error || 'Failed to delete');
878
- return { success: false, exitCode: 1 };
893
+ const { deleteEntry } = await import('../memory/memory-initializer.js');
894
+ const paths = removalDbTargets(ctx.flags.path);
895
+ const stores = [];
896
+ for (const target of paths) {
897
+ const { dbPath } = target;
898
+ const entry = await deleteEntry({ key, namespace, ...target });
899
+ if (!entry.success)
900
+ throw new Error(`${dbPath}: ${entry.error || 'Failed to delete'}`);
901
+ stores.push({ dbPath, ...entry });
879
902
  }
903
+ const result = {
904
+ success: true, key, namespace, stores,
905
+ deleted: stores.some(entry => entry.deleted),
906
+ remainingEntries: stores.reduce((total, entry) => total + entry.remainingEntries, 0),
907
+ };
880
908
  if (result.deleted) {
881
909
  output.printSuccess(`Deleted "${key}" from namespace "${namespace}"`);
882
- output.printInfo(`Remaining entries: ${result.remainingEntries}`);
910
+ output.printInfo(`Remaining entries across ${paths.length} store(s): ${result.remainingEntries}`);
883
911
  }
884
912
  else {
885
913
  output.printWarning(`Key not found: "${key}" in namespace "${namespace}"`);
@@ -901,7 +929,7 @@ const deleteCommand = {
901
929
  // either interactive confirmation or --force.
902
930
  const purgeCommand = {
903
931
  name: 'purge',
904
- description: 'Permanently delete every entry in a namespace (hard delete — not the soft delete/tombstone that `memory delete` uses)',
932
+ description: 'Permanently delete a namespace from the default store and its AgentDB mirror (use --path to select one store)',
905
933
  options: [
906
934
  {
907
935
  name: 'namespace',
@@ -939,17 +967,22 @@ const purgeCommand = {
939
967
  return { success: false, exitCode: 1 };
940
968
  }
941
969
  try {
942
- const { listEntries, purgeNamespace, resolveDbPath: _rdbPurge } = await import('../memory/memory-initializer.js');
943
- const resolvedDbPath = _rdbPurge(dbPath);
944
- const preview = await listEntries({ namespace, limit: 1, dbPath: resolvedDbPath });
945
- const previewCount = preview.total ?? preview.entries?.length ?? 0;
970
+ const { listEntries, purgeNamespace } = await import('../memory/memory-initializer.js');
971
+ const paths = removalDbTargets(dbPath);
972
+ let previewCount = 0;
973
+ for (const target of paths) {
974
+ const preview = await listEntries({ namespace, limit: 1, ...target });
975
+ if (!preview.success)
976
+ throw new Error(`${target.dbPath}: ${preview.error || 'Failed to read purge target'}`);
977
+ previewCount += preview.total ?? preview.entries?.length ?? 0;
978
+ }
946
979
  if (dryRun) {
947
- output.printInfo(`Would permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}" (dry run — nothing deleted)`);
980
+ output.printInfo(`Would permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}" across ${paths.length} store(s) (dry run — nothing deleted)`);
948
981
  return { success: true, data: { namespace, wouldDelete: previewCount } };
949
982
  }
950
983
  if (!force && ctx.interactive) {
951
984
  const confirmed = await confirm({
952
- message: `Permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}"? This is a hard delete — not reversible with \`memory delete\`'s soft-undo.`,
985
+ message: `Permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}" across ${paths.length} store(s)? This is a hard delete — not reversible with \`memory delete\`'s soft-undo.`,
953
986
  default: false
954
987
  });
955
988
  if (!confirmed) {
@@ -961,13 +994,20 @@ const purgeCommand = {
961
994
  output.printError(`Refusing to purge namespace "${namespace}" without --force in non-interactive mode`);
962
995
  return { success: false, exitCode: 1 };
963
996
  }
964
- const result = await purgeNamespace({ namespace, dbPath: resolvedDbPath });
965
- if (!result.success) {
966
- output.printError(result.error || 'Failed to purge');
967
- return { success: false, exitCode: 1 };
997
+ const stores = [];
998
+ for (const target of paths) {
999
+ const purged = await purgeNamespace({ namespace, ...target });
1000
+ if (!purged.success)
1001
+ throw new Error(`${target.dbPath}: ${purged.error || 'Failed to purge'}`);
1002
+ stores.push({ dbPath: target.dbPath, ...purged });
968
1003
  }
1004
+ const result = {
1005
+ success: true, stores,
1006
+ deletedCount: stores.reduce((total, store) => total + store.deletedCount, 0),
1007
+ remainingEntries: stores.reduce((total, store) => total + store.remainingEntries, 0),
1008
+ };
969
1009
  output.printSuccess(`Purged ${result.deletedCount} entr${result.deletedCount === 1 ? 'y' : 'ies'} from namespace "${namespace}"`);
970
- output.printInfo(`Remaining entries (all namespaces): ${result.remainingEntries}`);
1010
+ output.printInfo(`Remaining entries (all namespaces, ${paths.length} store(s)): ${result.remainingEntries}`);
971
1011
  return { success: true, data: result };
972
1012
  }
973
1013
  catch (error) {
@@ -984,8 +1024,16 @@ const statsCommand = {
984
1024
  action: async (ctx) => {
985
1025
  // Call MCP memory/stats tool for real statistics
986
1026
  try {
987
- const statsResult = await callMCPTool('memory_stats', {});
1027
+ const dbPath = resolveDbPath(ctx.flags.path);
1028
+ const statsResult = await callMCPTool('memory_stats', { dbPath });
1029
+ if (statsResult.available === false) {
1030
+ output.printError(`Failed to get stats: ${statsResult.error || 'Store unavailable'}`);
1031
+ return { success: false, exitCode: 1 };
1032
+ }
1033
+ const unreadStore = await countSiblingStoreRows(dbPath);
988
1034
  const stats = {
1035
+ ...(unreadStore ? { unreadStore } : {}),
1036
+ ...(statsResult.truncated ? { truncated: true } : {}),
989
1037
  backend: statsResult.backend,
990
1038
  entries: {
991
1039
  total: statsResult.totalEntries,
@@ -993,7 +1041,7 @@ const statsCommand = {
993
1041
  text: statsResult.totalEntries
994
1042
  },
995
1043
  storage: {
996
- total: statsResult.totalSize,
1044
+ total: statsResult.totalSize ?? 'unknown',
997
1045
  location: statsResult.location
998
1046
  },
999
1047
  version: statsResult.version,
@@ -1004,6 +1052,9 @@ const statsCommand = {
1004
1052
  output.printJson(stats);
1005
1053
  return { success: true, data: stats };
1006
1054
  }
1055
+ if (unreadStore) {
1056
+ output.printWarning(`${unreadStore.rows} entries are in ${unreadStore.path} and were not read here. Read them with --path ${unreadStore.path}.`);
1057
+ }
1007
1058
  output.writeln();
1008
1059
  output.writeln(output.bold('Memory Statistics'));
1009
1060
  output.writeln();
@@ -1245,19 +1296,27 @@ const cleanupCommand = {
1245
1296
  { command: 'claude-flow memory cleanup --expired-only', description: 'Clean expired entries' }
1246
1297
  ],
1247
1298
  action: async (ctx) => {
1248
- const dryRun = ctx.flags.dryRun;
1249
- const force = ctx.flags.force;
1299
+ const dryRun = ctx.flags.dryRun === true;
1300
+ const force = ctx.flags.force === true;
1301
+ if (ctx.flags.format === 'json' && !dryRun && !force) {
1302
+ output.printJson({ success: false, error: 'Use --dry-run to preview or --force to authorize cleanup with --format json.' });
1303
+ return { success: false, exitCode: 1 };
1304
+ }
1250
1305
  if (dryRun) {
1251
1306
  output.writeln(output.warning('DRY RUN - No changes will be made'));
1252
1307
  }
1253
1308
  output.printInfo('Analyzing memory for cleanup...');
1254
1309
  try {
1255
- const result = await callMCPTool('memory_cleanup', {
1256
- dryRun,
1310
+ const selectors = {
1257
1311
  olderThan: ctx.flags.olderThan,
1258
1312
  expiredOnly: ctx.flags.expiredOnly,
1259
1313
  lowQualityThreshold: ctx.flags.lowQuality,
1260
1314
  namespace: ctx.flags.namespace,
1315
+ };
1316
+ // Preview before asking: dryRun:false executes deletion inside the tool.
1317
+ let result = await callMCPTool('memory_cleanup', {
1318
+ ...selectors,
1319
+ dryRun: dryRun || !force,
1261
1320
  });
1262
1321
  if (ctx.flags.format === 'json') {
1263
1322
  output.printJson(result);
@@ -1286,6 +1345,7 @@ const cleanupCommand = {
1286
1345
  output.printInfo('Cleanup cancelled');
1287
1346
  return { success: true, data: result };
1288
1347
  }
1348
+ result = await callMCPTool('memory_cleanup', { ...selectors, dryRun: false });
1289
1349
  }
1290
1350
  if (!dryRun) {
1291
1351
  output.writeln();
@@ -1525,7 +1585,8 @@ const importCommand = {
1525
1585
  short: 'n',
1526
1586
  description: 'Import into specific namespace',
1527
1587
  type: 'string'
1528
- }
1588
+ },
1589
+ DB_PATH_OPTION
1529
1590
  ],
1530
1591
  examples: [
1531
1592
  { command: 'claude-flow memory import -i ./backup.json', description: 'Import from file' },
@@ -1543,6 +1604,7 @@ const importCommand = {
1543
1604
  inputPath,
1544
1605
  merge: ctx.flags.merge ?? true,
1545
1606
  namespace: ctx.flags.namespace,
1607
+ dbPath: resolveDbPath(ctx.flags.path),
1546
1608
  });
1547
1609
  output.printSuccess(`Imported from ${result.inputPath}`);
1548
1610
  output.printList([
@@ -279,6 +279,7 @@ export const metaharnessCommand = {
279
279
  'verbose', 'v',
280
280
  'quiet', 'q',
281
281
  'help', 'h',
282
+ 'interactive', 'i', // CLI-only default; plugin scripts are non-interactive
282
283
  ]);
283
284
  // Parser normalizes kebab-case → camelCase (--per-dimension → perDimension).
284
285
  // Plugin scripts expect kebab-case at argv, so we re-kebab here.
@@ -13,6 +13,7 @@ import { createBuiltinAIDefence } from '../security/builtin-aidefence.js';
13
13
  // for both validation and the traversal-depth maps below.
14
14
  const SCAN_DEPTHS = ['quick', 'standard', 'deep'];
15
15
  const SCAN_TYPES = ['code', 'deps', 'all'];
16
+ const SCAN_OUTPUTS = ['text', 'json', 'sarif'];
16
17
  // Real type predicates, not `as` casts. Array.includes() does not narrow a
17
18
  // string to a literal union on its own, so without these the depth-map lookups
18
19
  // below need an unchecked assertion — and an unchecked assertion would let a
@@ -23,6 +24,37 @@ const SCAN_TYPES = ['code', 'deps', 'all'];
23
24
  // is being fixed for.
24
25
  const isScanDepth = (v) => SCAN_DEPTHS.includes(v);
25
26
  const isScanType = (v) => SCAN_TYPES.includes(v);
27
+ const isScanOutput = (v) => SCAN_OUTPUTS.includes(v);
28
+ function scanSarif(findings) {
29
+ const ruleId = (type) => `RUFLO-${type.toUpperCase().replace(/[^A-Z0-9]+/g, '-')}`;
30
+ const level = (severity) => severity === 'critical' || severity === 'high' ? 'error' :
31
+ severity === 'medium' ? 'warning' : 'note';
32
+ const rules = [...new Map(findings.map(finding => [finding.type, {
33
+ id: ruleId(finding.type),
34
+ name: finding.type,
35
+ shortDescription: { text: finding.type },
36
+ }])).values()];
37
+ return {
38
+ version: '2.1.0',
39
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
40
+ runs: [{
41
+ tool: { driver: { name: 'Ruflo Security Scan', rules } },
42
+ results: findings.map(finding => {
43
+ const lineMatch = /^(.*):(\d+)$/.exec(finding.location);
44
+ const file = lineMatch?.[1] ?? (finding.location.startsWith('package.json:') ? 'package.json' : finding.location);
45
+ return {
46
+ ruleId: ruleId(finding.type),
47
+ level: level(finding.severity),
48
+ message: { text: finding.description },
49
+ locations: [{ physicalLocation: {
50
+ artifactLocation: { uri: file.replace(/\\/g, '/') },
51
+ ...(lineMatch ? { region: { startLine: Number(lineMatch[2]) } } : {}),
52
+ } }],
53
+ };
54
+ }),
55
+ }],
56
+ };
57
+ }
26
58
  // `full` was never a supported depth, but the CLI itself printed it — the
27
59
  // statusline insight, the announcement, the release-notes blurb, the CLAUDE.md
28
60
  // that `init` generates, and two shipped agent definitions all tell people to
@@ -62,6 +94,7 @@ const scanCommand = {
62
94
  const target = ctx.flags.target || '.';
63
95
  const requestedDepth = ctx.flags.depth || 'standard';
64
96
  const scanType = ctx.flags.type || 'all';
97
+ const requestedOutput = ctx.flags.output || 'text';
65
98
  const fix = ctx.flags.fix;
66
99
  // A security scanner must never silently degrade on an unrecognised enum
67
100
  // value. An unknown --depth used to fall through to the shallowest
@@ -91,6 +124,11 @@ const scanCommand = {
91
124
  output.printError(`Invalid --type '${scanType}'. Expected one of: ${SCAN_TYPES.join(', ')}.`);
92
125
  return { success: false, exitCode: 1 };
93
126
  }
127
+ if (!isScanOutput(requestedOutput)) {
128
+ output.printError(`Invalid --output '${requestedOutput}'. Expected one of: ${SCAN_OUTPUTS.join(', ')}.`);
129
+ return { success: false, exitCode: 1 };
130
+ }
131
+ const outputFormat = requestedOutput;
94
132
  // --target names WHAT gets scanned, and was never validated. A path that
95
133
  // does not exist (or is a file, not a directory) made every phase read
96
134
  // nothing, and the swallowed dir-read catches turned that into zero
@@ -106,11 +144,14 @@ const scanCommand = {
106
144
  output.printError(`Target is not a directory: ${resolvedTarget}`);
107
145
  return { success: false, exitCode: 1 };
108
146
  }
109
- output.writeln();
110
- output.writeln(output.bold('Security Scan'));
111
- output.writeln(output.dim('─'.repeat(50)));
147
+ if (outputFormat === 'text') {
148
+ output.writeln();
149
+ output.writeln(output.bold('Security Scan'));
150
+ output.writeln(output.dim('─'.repeat(50)));
151
+ }
112
152
  const spinner = output.createSpinner({ text: `Scanning ${target}...`, spinner: 'dots' });
113
- spinner.start();
153
+ if (outputFormat === 'text')
154
+ spinner.start();
114
155
  const findings = [];
115
156
  let criticalCount = 0, highCount = 0, mediumCount = 0, lowCount = 0;
116
157
  try {
@@ -151,9 +192,9 @@ const scanCommand = {
151
192
  else
152
193
  lowCount++;
153
194
  findings.push({
154
- severity: sev === 'critical' ? output.error('CRITICAL') :
155
- sev === 'high' ? output.warning('HIGH') :
156
- sev === 'moderate' || sev === 'medium' ? output.warning('MEDIUM') : output.info('LOW'),
195
+ severity: sev === 'critical' ? 'critical' :
196
+ sev === 'high' ? 'high' :
197
+ sev === 'moderate' || sev === 'medium' ? 'medium' : 'low',
157
198
  type: 'Dependency CVE',
158
199
  location: `package.json:${pkg}`,
159
200
  description: title.substring(0, 35),
@@ -206,7 +247,7 @@ const scanCommand = {
206
247
  if (pattern.test(lines[i])) {
207
248
  highCount++;
208
249
  findings.push({
209
- severity: output.warning('HIGH'),
250
+ severity: 'high',
210
251
  type: 'Hardcoded Secret',
211
252
  location: `${path.relative(target, fullPath)}:${i + 1}`,
212
253
  description: type,
@@ -261,7 +302,7 @@ const scanCommand = {
261
302
  else
262
303
  mediumCount++;
263
304
  findings.push({
264
- severity: severity === 'high' ? output.warning('HIGH') : output.warning('MEDIUM'),
305
+ severity: severity === 'high' ? 'high' : 'medium',
265
306
  type,
266
307
  location: `${path.relative(target, fullPath)}:${i + 1}`,
267
308
  description: desc,
@@ -280,35 +321,57 @@ const scanCommand = {
280
321
  const scanDepth = CODE_SCAN_DEPTH[depth];
281
322
  scanCodeDir(path.resolve(target), scanDepth);
282
323
  }
283
- spinner.succeed('Scan complete');
324
+ if (outputFormat === 'text')
325
+ spinner.succeed('Scan complete');
284
326
  // Display results
285
- output.writeln();
286
- if (findings.length > 0) {
287
- output.printTable({
288
- columns: [
289
- { key: 'severity', header: 'Severity', width: 12 },
290
- { key: 'type', header: 'Type', width: 18 },
291
- { key: 'location', header: 'Location', width: 25 },
292
- { key: 'description', header: 'Description', width: 35 },
293
- ],
294
- data: findings.slice(0, 20), // Show first 20
295
- });
296
- if (findings.length > 20) {
297
- output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
327
+ if (outputFormat === 'text') {
328
+ output.writeln();
329
+ if (findings.length > 0) {
330
+ output.printTable({
331
+ columns: [
332
+ { key: 'severity', header: 'Severity', width: 12 },
333
+ { key: 'type', header: 'Type', width: 18 },
334
+ { key: 'location', header: 'Location', width: 25 },
335
+ { key: 'description', header: 'Description', width: 35 },
336
+ ],
337
+ data: findings.slice(0, 20).map(finding => ({
338
+ ...finding,
339
+ severity: finding.severity === 'critical' ? output.error('CRITICAL') :
340
+ finding.severity === 'high' ? output.warning('HIGH') :
341
+ finding.severity === 'medium' ? output.warning('MEDIUM') : output.info('LOW'),
342
+ })), // Show first 20
343
+ });
344
+ if (findings.length > 20) {
345
+ output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
346
+ }
298
347
  }
348
+ else {
349
+ output.writeln(output.success('No security issues found!'));
350
+ }
351
+ output.writeln();
352
+ output.printBox([
353
+ `Target: ${target}`,
354
+ `Depth: ${depth}`,
355
+ `Type: ${scanType}`,
356
+ ``,
357
+ `Critical: ${criticalCount} High: ${highCount} Medium: ${mediumCount} Low: ${lowCount}`,
358
+ `Total Issues: ${findings.length}`,
359
+ ].join('\n'), 'Scan Summary');
299
360
  }
300
- else {
301
- output.writeln(output.success('No security issues found!'));
302
- }
303
- output.writeln();
304
- output.printBox([
305
- `Target: ${target}`,
306
- `Depth: ${depth}`,
307
- `Type: ${scanType}`,
308
- ``,
309
- `Critical: ${criticalCount} High: ${highCount} Medium: ${mediumCount} Low: ${lowCount}`,
310
- `Total Issues: ${findings.length}`,
311
- ].join('\n'), 'Scan Summary');
361
+ const record = {
362
+ timestamp: new Date().toISOString(),
363
+ target,
364
+ depth,
365
+ type: scanType,
366
+ summary: {
367
+ critical: criticalCount,
368
+ high: highCount,
369
+ medium: mediumCount,
370
+ low: lowCount,
371
+ total: findings.length,
372
+ },
373
+ findings,
374
+ };
312
375
  // Persist the scan result so downstream consumers (the statusline's
313
376
  // getSecurityStatus in funnel/local-signals.ts, which reads
314
377
  // `.claude/security-scans/*.json`) reflect real scan state. Best-effort:
@@ -316,20 +379,6 @@ const scanCommand = {
316
379
  try {
317
380
  const scanDirOut = path.join(path.resolve(target), '.claude', 'security-scans');
318
381
  fs.mkdirSync(scanDirOut, { recursive: true });
319
- const record = {
320
- timestamp: new Date().toISOString(),
321
- target,
322
- depth,
323
- type: scanType,
324
- summary: {
325
- critical: criticalCount,
326
- high: highCount,
327
- medium: mediumCount,
328
- low: lowCount,
329
- total: findings.length,
330
- },
331
- findings,
332
- };
333
382
  // Deterministic name keyed on scan config so repeated runs overwrite
334
383
  // rather than accumulate stale reports.
335
384
  const outFile = path.join(scanDirOut, `scan-${scanType}-${depth}.json`);
@@ -340,24 +389,33 @@ const scanCommand = {
340
389
  }
341
390
  // Auto-fix if requested
342
391
  if (fix && criticalCount + highCount > 0) {
343
- output.writeln();
392
+ if (outputFormat === 'text')
393
+ output.writeln();
344
394
  const fixSpinner = output.createSpinner({ text: 'Attempting to fix vulnerabilities...', spinner: 'dots' });
345
- fixSpinner.start();
395
+ if (outputFormat === 'text')
396
+ fixSpinner.start();
346
397
  try {
347
398
  try {
348
399
  execSync('npm audit fix', { cwd: path.resolve(target), encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
349
400
  }
350
401
  catch { /* npm audit fix may exit non-zero */ }
351
- fixSpinner.succeed('Applied available fixes (run scan again to verify)');
402
+ if (outputFormat === 'text')
403
+ fixSpinner.succeed('Applied available fixes (run scan again to verify)');
352
404
  }
353
405
  catch {
354
- fixSpinner.fail('Some fixes could not be applied automatically');
406
+ if (outputFormat === 'text')
407
+ fixSpinner.fail('Some fixes could not be applied automatically');
355
408
  }
356
409
  }
410
+ if (outputFormat === 'json')
411
+ output.printJson(record);
412
+ else if (outputFormat === 'sarif')
413
+ output.printJson(scanSarif(findings));
357
414
  return { success: findings.length === 0 || (criticalCount === 0 && highCount === 0) };
358
415
  }
359
416
  catch (error) {
360
- spinner.fail('Scan failed');
417
+ if (outputFormat === 'text')
418
+ spinner.fail('Scan failed');
361
419
  output.printError(`Error: ${error}`);
362
420
  return { success: false };
363
421
  }
@@ -1039,7 +1097,7 @@ const defendCommand = {
1039
1097
  medium: output.info,
1040
1098
  low: output.dim,
1041
1099
  }[threat.severity] || output.dim;
1042
- output.writeln(` ${severityColor(`[${threat.severity.toUpperCase()}]`)} ${threat.type}`);
1100
+ output.writeln(` ${severityColor.call(output, `[${threat.severity.toUpperCase()}]`)} ${threat.type}`);
1043
1101
  output.writeln(` ${output.dim(threat.description)}`);
1044
1102
  output.writeln(` Confidence: ${(threat.confidence * 100).toFixed(1)}%`);
1045
1103
  output.writeln();
@@ -324,18 +324,18 @@ const restoreCommand = {
324
324
  data: [
325
325
  {
326
326
  component: 'Memory',
327
- status: result.restored.memory ? output.success('Restored') : output.dim('Skipped'),
328
- count: result.stats.memoryEntriesRestored
327
+ status: result.restoredComponents.memory ? output.success('Restored') : output.dim('Skipped'),
328
+ count: result.restoredComponents.memory ? result.stats.memoryEntries : 0
329
329
  },
330
330
  {
331
331
  component: 'Agents',
332
- status: result.restored.agents ? output.success('Restored') : output.dim('Skipped'),
333
- count: result.stats.agentsRestored
332
+ status: result.restoredComponents.agents ? output.success('Restored') : output.dim('Skipped'),
333
+ count: result.restoredComponents.agents ? result.stats.agents : 0
334
334
  },
335
335
  {
336
336
  component: 'Tasks',
337
- status: result.restored.tasks ? output.success('Restored') : output.dim('Skipped'),
338
- count: result.stats.tasksRestored
337
+ status: result.restoredComponents.tasks ? output.success('Restored') : output.dim('Skipped'),
338
+ count: result.restoredComponents.tasks ? result.stats.tasks : 0
339
339
  }
340
340
  ]
341
341
  });
@@ -154,7 +154,7 @@ const createCommand = {
154
154
  type: taskType,
155
155
  description,
156
156
  priority: priority || 'normal',
157
- assignedTo: ctx.flags.assign ? [ctx.flags.assign] : undefined,
157
+ assignTo: ctx.flags.assign ? String(ctx.flags.assign).split(',').map(id => id.trim()).filter(Boolean) : undefined,
158
158
  parentId: ctx.flags.parent,
159
159
  dependencies,
160
160
  tags,
@@ -245,14 +245,15 @@ const listCommand = {
245
245
  }
246
246
  ],
247
247
  action: async (ctx) => {
248
- const status = ctx.flags.all ? 'all' : ctx.flags.status || 'pending,running';
248
+ const requestedStatus = ctx.flags.all ? 'all' : ctx.flags.status || 'pending,in_progress';
249
+ const status = requestedStatus === 'all' ? undefined : requestedStatus.replace(/\brunning\b/g, 'in_progress');
249
250
  const limit = ctx.flags.limit;
250
251
  try {
251
252
  const result = await callMCPTool('task_list', {
252
253
  status,
253
254
  type: ctx.flags.type,
254
255
  priority: ctx.flags.priority,
255
- agentId: ctx.flags.agent,
256
+ assignedTo: ctx.flags.agent,
256
257
  limit,
257
258
  offset: 0
258
259
  });
@@ -277,7 +278,7 @@ const listCommand = {
277
278
  { key: 'progress', header: 'Progress', width: 10 }
278
279
  ],
279
280
  data: result.tasks.map(t => ({
280
- id: t.id,
281
+ id: t.taskId,
281
282
  type: t.type,
282
283
  description: t.description.length > 27
283
284
  ? t.description.slice(0, 27) + '...'
@@ -1005,12 +1005,14 @@ export function generateAutoMemoryHook() {
1005
1005
  */
1006
1006
 
1007
1007
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
1008
- import { join, dirname } from 'path';
1008
+ import { join, dirname, resolve } from 'path';
1009
1009
  import { fileURLToPath } from 'url';
1010
1010
 
1011
1011
  const __filename = fileURLToPath(import.meta.url);
1012
1012
  const __dirname = dirname(__filename);
1013
- const PROJECT_ROOT = join(__dirname, '../..');
1013
+ const PROJECT_ROOT = process.env.CLAUDE_PROJECT_DIR
1014
+ ? resolve(process.env.CLAUDE_PROJECT_DIR)
1015
+ : join(__dirname, '../..');
1014
1016
  const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
1015
1017
  const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
1016
1018
 
@@ -27,6 +27,12 @@ import { fileURLToPath } from 'url';
27
27
  import { dirname } from 'path';
28
28
  import { trackRequest } from './mcp-tools/request-tracker.js';
29
29
  import { isPolicyEnforcementEnabled, loadMcpPolicy, evaluateToolCall, } from './mcp-tools/policy-enforcer.js';
30
+ /** A failed task can be valid data; only an explicit error denotes tool failure. */
31
+ function hasToolError(result) {
32
+ return result !== null && typeof result === 'object'
33
+ && typeof result.error === 'string'
34
+ && result.error.trim().length > 0;
35
+ }
30
36
  // ESM-compatible __dirname
31
37
  const __filename = fileURLToPath(import.meta.url);
32
38
  const __dirname = dirname(__filename);
@@ -666,11 +672,12 @@ export class MCPServerManager extends EventEmitter {
666
672
  }
667
673
  try {
668
674
  const result = await callMCPTool(toolName, toolParams, { sessionId });
669
- trackRequest(toolName, true);
675
+ const isError = hasToolError(result);
676
+ trackRequest(toolName, !isError);
670
677
  return {
671
678
  jsonrpc: '2.0',
672
679
  id: message.id,
673
- result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] },
680
+ result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError },
674
681
  };
675
682
  }
676
683
  catch (error) {
@@ -741,7 +748,7 @@ export class MCPServerManager extends EventEmitter {
741
748
  handler: async (input, context) => {
742
749
  try {
743
750
  const result = await callMCPTool(tool.name, input || {}, { sessionId: context?.sessionId || fallbackSessionId });
744
- trackRequest(tool.name, true);
751
+ trackRequest(tool.name, !hasToolError(result));
745
752
  return result;
746
753
  }
747
754
  catch (error) {