claude-flow 3.47.1 → 3.49.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 (84) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/auto-memory-hook.mjs +6 -2
  3. package/.claude/helpers/hook-handler.cjs +7 -4
  4. package/.claude/helpers/learning-service.mjs +4 -2
  5. package/.claude/helpers/memory.cjs +1 -1
  6. package/.claude/helpers/metrics-db.mjs +4 -2
  7. package/.claude/helpers/router.cjs +1 -1
  8. package/.claude/helpers/session.cjs +1 -1
  9. package/.claude/proven-config.json +42 -0
  10. package/.claude-plugin/marketplace.json +16 -1
  11. package/README.md +1 -53
  12. package/README.zh-CN.md +1 -53
  13. package/node_modules/@claude-flow/codex/package.json +1 -1
  14. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  15. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
  16. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  17. package/node_modules/@claude-flow/mcp/package.json +6 -3
  18. package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
  19. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
  20. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  21. package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
  22. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  23. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
  24. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  25. package/node_modules/@claude-flow/security/package.json +2 -3
  26. package/package.json +7 -7
  27. package/v3/@claude-flow/cli/README.md +1 -53
  28. package/v3/@claude-flow/cli/bin/cli.js +4 -1
  29. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  30. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
  32. package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
  33. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +69 -8
  34. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +22 -9
  35. package/v3/@claude-flow/cli/dist/src/commands/memory.js +119 -34
  36. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
  37. package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
  38. package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
  39. package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
  40. package/v3/@claude-flow/cli/dist/src/commands/session.js +132 -25
  41. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
  42. package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
  43. package/v3/@claude-flow/cli/dist/src/index.js +10 -1
  44. package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
  45. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
  46. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
  47. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
  48. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
  49. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
  50. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
  51. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +14 -12
  52. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
  53. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
  54. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -7
  55. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +226 -51
  56. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +131 -40
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +270 -58
  62. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
  63. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
  64. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
  65. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
  66. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
  67. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
  68. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
  69. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +47 -34
  70. package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
  71. package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
  72. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
  73. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
  74. package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
  75. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
  76. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
  77. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
  78. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
  79. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +27 -2
  80. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
  81. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
  82. package/v3/@claude-flow/cli/package.json +9 -9
  83. package/v3/@claude-flow/guidance/package.json +5 -6
  84. package/v3/@claude-flow/shared/package.json +6 -3
@@ -9,6 +9,10 @@ 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';
14
+ import { validateIdentifier } from '../mcp-tools/validate-input.js';
15
+ import { memoryKeyError } from '../mcp-tools/memory-tools.js';
12
16
  /**
13
17
  * #3228: a miss in one store is not a miss in the memory.
14
18
  *
@@ -19,16 +23,39 @@ import { resolveDbPath } from '../memory/memory-initializer.js';
19
23
  * `found:false`. A confident negative is worse than an error, because nothing
20
24
  * prompts anyone to look further.
21
25
  */
22
- async function warnIfSiblingHasRows(pathFlag) {
26
+ async function warnIfSiblingHasRows(pathFlag, hits) {
23
27
  const unread = await countSiblingStoreRows(resolveDbPath(pathFlag));
24
28
  if (unread && unread.rows > 0) {
25
- output.printWarning(`This read covered one store. ${unread.rows} entries are in ${unread.path} and were not searched. ` +
29
+ // #3566: disclose on hits too. A partial positive ("Found 1 results") invites
30
+ // no second look, so it is the more dangerous case, not the safer one.
31
+ const lead = hits === undefined
32
+ ? 'This read covered one store.'
33
+ : `Partial result: ${hits} ${hits === 1 ? 'match' : 'matches'} came from the store read here.`;
34
+ output.printWarning(`${lead} ${unread.rows} entries are in ${unread.path} and were not searched. ` +
26
35
  `That store is written by the MCP/AgentDB path; read it with --path ${unread.path}.`);
27
36
  }
28
37
  }
38
+ /** Default CLI writes may be mirrored into AgentDB; remove both copies. */
39
+ function removalDbTargets(pathFlag) {
40
+ const primary = resolveDbPath(pathFlag);
41
+ // An explicit file selection is a single-store operation, as documented by
42
+ // --path. Do not widen a caller's requested destructive scope.
43
+ if (pathFlag || process.env.CLAUDE_FLOW_DB_PATH)
44
+ return [{ dbPath: primary }];
45
+ const sibling = siblingAgentDbPath(primary);
46
+ // The AgentDB mirror is consumed by native SQLite and must stay plaintext.
47
+ // Pass this per operation, including preview migrations; never toggle the
48
+ // process encryption setting while asynchronous operations are in flight.
49
+ const targets = [
50
+ { dbPath: primary },
51
+ ...(sibling ? [{ dbPath: sibling, encryptWrites: false }] : []),
52
+ ].filter(target => existsSync(target.dbPath));
53
+ // Preserve the existing missing-database error when neither store exists.
54
+ return targets.length ? targets : [{ dbPath: primary }];
55
+ }
29
56
  // Memory backends
30
57
  const BACKENDS = [
31
- { value: 'agentdb', label: 'AgentDB', hint: 'Vector database with HNSW indexing (150x-12,500x faster)' },
58
+ { value: 'agentdb', label: 'AgentDB', hint: 'Vector database with HNSW indexing' },
32
59
  { value: 'sqlite', label: 'SQLite', hint: 'Lightweight local storage' },
33
60
  { value: 'hybrid', label: 'Hybrid', hint: 'SQLite + AgentDB (recommended)' },
34
61
  { value: 'memory', label: 'In-Memory', hint: 'Fast but non-persistent' }
@@ -162,6 +189,18 @@ const storeCommand = {
162
189
  output.printError('Value is required. Use --value');
163
190
  return { success: false, exitCode: 1 };
164
191
  }
192
+ // #3570: reject a traversal namespace before persisting, as export/purge do.
193
+ const vNs = validateIdentifier(namespace, 'namespace');
194
+ if (!vNs.valid) {
195
+ output.printError(vNs.error);
196
+ return { success: false, exitCode: 1 };
197
+ }
198
+ // #3570 follow-up: the same key rule MCP memory_store enforces.
199
+ const keyError = memoryKeyError(key);
200
+ if (keyError) {
201
+ output.printError(keyError);
202
+ return { success: false, exitCode: 1 };
203
+ }
165
204
  // #2752 MemPoison gate — scan before persist when opted in.
166
205
  // CLI flag ctx.flags.scanContent takes precedence over RUFLO_MEMORY_SCAN_ON_WRITE env var
167
206
  // (ADR-125 §"CLI flag wins" / ADR-130 §env-var-config-precedence — fix for #2794).
@@ -308,6 +347,9 @@ const retrieveCommand = {
308
347
  return { success: false, exitCode: 1, data: { key, found: false } };
309
348
  }
310
349
  const entry = result.entry;
350
+ // #3566: the same key may also live in the sibling store. Goes to stderr,
351
+ // so --value-only / --format json stdout stays parseable.
352
+ await warnIfSiblingHasRows(ctx.flags.path, 1);
311
353
  // #2073: --value-only emits just the raw value (no decoration) for
312
354
  // piping into JSON.parse / jq / other downstream parsers without
313
355
  // any cleanup.
@@ -391,7 +433,7 @@ const searchCommand = {
391
433
  },
392
434
  {
393
435
  name: 'build-hnsw',
394
- description: 'Build/rebuild HNSW index before searching (enables 150x-12,500x speedup)',
436
+ description: 'Build/rebuild HNSW index before searching',
395
437
  type: 'boolean',
396
438
  default: false
397
439
  },
@@ -492,7 +534,6 @@ const searchCommand = {
492
534
  const status = getHNSWStatus();
493
535
  output.printSuccess(`HNSW index built (${status.entryCount} vectors, ${buildTime}ms)`);
494
536
  output.writeln(output.dim(` Dimensions: ${status.dimensions}, Metric: cosine`));
495
- output.writeln(output.dim(` Search speedup: ${status.entryCount > 10000 ? '12,500x' : status.entryCount > 1000 ? '150x' : '10x'}`));
496
537
  }
497
538
  else {
498
539
  output.printWarning('HNSW index not available (install @ruvector/core for acceleration)');
@@ -556,6 +597,7 @@ const searchCommand = {
556
597
  // Pure-keyword mode returns directly; skip the semantic path entirely.
557
598
  if (ctx.flags.format === 'json') {
558
599
  output.printJson({ query, searchType, results: keywordResults, searchTime: '0ms' });
600
+ await warnIfSiblingHasRows(ctx.flags.path, keywordResults.length);
559
601
  return { success: true, data: keywordResults };
560
602
  }
561
603
  output.writeln();
@@ -563,6 +605,7 @@ const searchCommand = {
563
605
  for (const r of keywordResults) {
564
606
  output.writeln(` ${r.key} (${r.namespace}, score=${r.score.toFixed(2)}) — ${r.preview.slice(0, 80)}${r.preview.length > 80 ? '…' : ''}`);
565
607
  }
608
+ await warnIfSiblingHasRows(ctx.flags.path, keywordResults.length);
566
609
  return { success: true, data: keywordResults };
567
610
  }
568
611
  // Hybrid mode: keyword hits will be MERGED after semantic runs below.
@@ -674,6 +717,7 @@ const searchCommand = {
674
717
  }
675
718
  if (ctx.flags.format === 'json') {
676
719
  output.printJson({ query, searchType, results, searchTime: `${searchTimeMs}ms`, ...(smartStats ? { stats: smartStats } : {}) });
720
+ await warnIfSiblingHasRows(ctx.flags.path, results.length);
677
721
  return { success: true, data: results };
678
722
  }
679
723
  // Performance stats
@@ -700,6 +744,7 @@ const searchCommand = {
700
744
  });
701
745
  output.writeln();
702
746
  output.printInfo(`Found ${results.length} results`);
747
+ await warnIfSiblingHasRows(ctx.flags.path, results.length);
703
748
  return { success: true, data: results };
704
749
  }
705
750
  catch (error) {
@@ -820,7 +865,7 @@ function formatRelativeTime(isoDate) {
820
865
  const deleteCommand = {
821
866
  name: 'delete',
822
867
  aliases: ['rm'],
823
- description: 'Delete memory entry',
868
+ description: 'Delete a memory entry from the default store and its AgentDB mirror (use --path to select one store)',
824
869
  options: [
825
870
  {
826
871
  name: 'key',
@@ -870,16 +915,24 @@ const deleteCommand = {
870
915
  }
871
916
  // Use sql.js directly for consistent data access (Issue #980)
872
917
  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 };
918
+ const { deleteEntry } = await import('../memory/memory-initializer.js');
919
+ const paths = removalDbTargets(ctx.flags.path);
920
+ const stores = [];
921
+ for (const target of paths) {
922
+ const { dbPath } = target;
923
+ const entry = await deleteEntry({ key, namespace, ...target });
924
+ if (!entry.success)
925
+ throw new Error(`${dbPath}: ${entry.error || 'Failed to delete'}`);
926
+ stores.push({ dbPath, ...entry });
879
927
  }
928
+ const result = {
929
+ success: true, key, namespace, stores,
930
+ deleted: stores.some(entry => entry.deleted),
931
+ remainingEntries: stores.reduce((total, entry) => total + entry.remainingEntries, 0),
932
+ };
880
933
  if (result.deleted) {
881
934
  output.printSuccess(`Deleted "${key}" from namespace "${namespace}"`);
882
- output.printInfo(`Remaining entries: ${result.remainingEntries}`);
935
+ output.printInfo(`Remaining entries across ${paths.length} store(s): ${result.remainingEntries}`);
883
936
  }
884
937
  else {
885
938
  output.printWarning(`Key not found: "${key}" in namespace "${namespace}"`);
@@ -901,7 +954,7 @@ const deleteCommand = {
901
954
  // either interactive confirmation or --force.
902
955
  const purgeCommand = {
903
956
  name: 'purge',
904
- description: 'Permanently delete every entry in a namespace (hard delete — not the soft delete/tombstone that `memory delete` uses)',
957
+ description: 'Permanently delete a namespace from the default store and its AgentDB mirror (use --path to select one store)',
905
958
  options: [
906
959
  {
907
960
  name: 'namespace',
@@ -939,17 +992,22 @@ const purgeCommand = {
939
992
  return { success: false, exitCode: 1 };
940
993
  }
941
994
  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;
995
+ const { listEntries, purgeNamespace } = await import('../memory/memory-initializer.js');
996
+ const paths = removalDbTargets(dbPath);
997
+ let previewCount = 0;
998
+ for (const target of paths) {
999
+ const preview = await listEntries({ namespace, limit: 1, ...target });
1000
+ if (!preview.success)
1001
+ throw new Error(`${target.dbPath}: ${preview.error || 'Failed to read purge target'}`);
1002
+ previewCount += preview.total ?? preview.entries?.length ?? 0;
1003
+ }
946
1004
  if (dryRun) {
947
- output.printInfo(`Would permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}" (dry run — nothing deleted)`);
1005
+ output.printInfo(`Would permanently delete ${previewCount} entr${previewCount === 1 ? 'y' : 'ies'} from namespace "${namespace}" across ${paths.length} store(s) (dry run — nothing deleted)`);
948
1006
  return { success: true, data: { namespace, wouldDelete: previewCount } };
949
1007
  }
950
1008
  if (!force && ctx.interactive) {
951
1009
  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.`,
1010
+ 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
1011
  default: false
954
1012
  });
955
1013
  if (!confirmed) {
@@ -961,13 +1019,20 @@ const purgeCommand = {
961
1019
  output.printError(`Refusing to purge namespace "${namespace}" without --force in non-interactive mode`);
962
1020
  return { success: false, exitCode: 1 };
963
1021
  }
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 };
1022
+ const stores = [];
1023
+ for (const target of paths) {
1024
+ const purged = await purgeNamespace({ namespace, ...target });
1025
+ if (!purged.success)
1026
+ throw new Error(`${target.dbPath}: ${purged.error || 'Failed to purge'}`);
1027
+ stores.push({ dbPath: target.dbPath, ...purged });
968
1028
  }
1029
+ const result = {
1030
+ success: true, stores,
1031
+ deletedCount: stores.reduce((total, store) => total + store.deletedCount, 0),
1032
+ remainingEntries: stores.reduce((total, store) => total + store.remainingEntries, 0),
1033
+ };
969
1034
  output.printSuccess(`Purged ${result.deletedCount} entr${result.deletedCount === 1 ? 'y' : 'ies'} from namespace "${namespace}"`);
970
- output.printInfo(`Remaining entries (all namespaces): ${result.remainingEntries}`);
1035
+ output.printInfo(`Remaining entries (all namespaces, ${paths.length} store(s)): ${result.remainingEntries}`);
971
1036
  return { success: true, data: result };
972
1037
  }
973
1038
  catch (error) {
@@ -984,8 +1049,16 @@ const statsCommand = {
984
1049
  action: async (ctx) => {
985
1050
  // Call MCP memory/stats tool for real statistics
986
1051
  try {
987
- const statsResult = await callMCPTool('memory_stats', {});
1052
+ const dbPath = resolveDbPath(ctx.flags.path);
1053
+ const statsResult = await callMCPTool('memory_stats', { dbPath });
1054
+ if (statsResult.available === false) {
1055
+ output.printError(`Failed to get stats: ${statsResult.error || 'Store unavailable'}`);
1056
+ return { success: false, exitCode: 1 };
1057
+ }
1058
+ const unreadStore = await countSiblingStoreRows(dbPath);
988
1059
  const stats = {
1060
+ ...(unreadStore ? { unreadStore } : {}),
1061
+ ...(statsResult.truncated ? { truncated: true } : {}),
989
1062
  backend: statsResult.backend,
990
1063
  entries: {
991
1064
  total: statsResult.totalEntries,
@@ -993,7 +1066,7 @@ const statsCommand = {
993
1066
  text: statsResult.totalEntries
994
1067
  },
995
1068
  storage: {
996
- total: statsResult.totalSize,
1069
+ total: statsResult.totalSize ?? 'unknown',
997
1070
  location: statsResult.location
998
1071
  },
999
1072
  version: statsResult.version,
@@ -1004,6 +1077,9 @@ const statsCommand = {
1004
1077
  output.printJson(stats);
1005
1078
  return { success: true, data: stats };
1006
1079
  }
1080
+ if (unreadStore) {
1081
+ output.printWarning(`${unreadStore.rows} entries are in ${unreadStore.path} and were not read here. Read them with --path ${unreadStore.path}.`);
1082
+ }
1007
1083
  output.writeln();
1008
1084
  output.writeln(output.bold('Memory Statistics'));
1009
1085
  output.writeln();
@@ -1109,8 +1185,6 @@ const statsCommand = {
1109
1185
  output.writeln(output.bold('Embedding'));
1110
1186
  output.printInfo(`Provider info unavailable: ${e instanceof Error ? e.message : String(e)}`);
1111
1187
  }
1112
- output.writeln();
1113
- output.printInfo('V3 Performance: 150x-12,500x faster search with HNSW indexing');
1114
1188
  return { success: true, data: stats };
1115
1189
  }
1116
1190
  catch (error) {
@@ -1245,19 +1319,27 @@ const cleanupCommand = {
1245
1319
  { command: 'claude-flow memory cleanup --expired-only', description: 'Clean expired entries' }
1246
1320
  ],
1247
1321
  action: async (ctx) => {
1248
- const dryRun = ctx.flags.dryRun;
1249
- const force = ctx.flags.force;
1322
+ const dryRun = ctx.flags.dryRun === true;
1323
+ const force = ctx.flags.force === true;
1324
+ if (ctx.flags.format === 'json' && !dryRun && !force) {
1325
+ output.printJson({ success: false, error: 'Use --dry-run to preview or --force to authorize cleanup with --format json.' });
1326
+ return { success: false, exitCode: 1 };
1327
+ }
1250
1328
  if (dryRun) {
1251
1329
  output.writeln(output.warning('DRY RUN - No changes will be made'));
1252
1330
  }
1253
1331
  output.printInfo('Analyzing memory for cleanup...');
1254
1332
  try {
1255
- const result = await callMCPTool('memory_cleanup', {
1256
- dryRun,
1333
+ const selectors = {
1257
1334
  olderThan: ctx.flags.olderThan,
1258
1335
  expiredOnly: ctx.flags.expiredOnly,
1259
1336
  lowQualityThreshold: ctx.flags.lowQuality,
1260
1337
  namespace: ctx.flags.namespace,
1338
+ };
1339
+ // Preview before asking: dryRun:false executes deletion inside the tool.
1340
+ let result = await callMCPTool('memory_cleanup', {
1341
+ ...selectors,
1342
+ dryRun: dryRun || !force,
1261
1343
  });
1262
1344
  if (ctx.flags.format === 'json') {
1263
1345
  output.printJson(result);
@@ -1286,6 +1368,7 @@ const cleanupCommand = {
1286
1368
  output.printInfo('Cleanup cancelled');
1287
1369
  return { success: true, data: result };
1288
1370
  }
1371
+ result = await callMCPTool('memory_cleanup', { ...selectors, dryRun: false });
1289
1372
  }
1290
1373
  if (!dryRun) {
1291
1374
  output.writeln();
@@ -1525,7 +1608,8 @@ const importCommand = {
1525
1608
  short: 'n',
1526
1609
  description: 'Import into specific namespace',
1527
1610
  type: 'string'
1528
- }
1611
+ },
1612
+ DB_PATH_OPTION
1529
1613
  ],
1530
1614
  examples: [
1531
1615
  { command: 'claude-flow memory import -i ./backup.json', description: 'Import from file' },
@@ -1543,6 +1627,7 @@ const importCommand = {
1543
1627
  inputPath,
1544
1628
  merge: ctx.flags.merge ?? true,
1545
1629
  namespace: ctx.flags.namespace,
1630
+ dbPath: resolveDbPath(ctx.flags.path),
1546
1631
  });
1547
1632
  output.printSuccess(`Imported from ${result.inputPath}`);
1548
1633
  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.
@@ -8,6 +8,7 @@
8
8
  import { output } from '../output.js';
9
9
  import { createPluginDiscoveryService, searchPlugins, getPluginSearchSuggestions, getFeaturedPlugins, getOfficialPlugins, } from '../plugins/store/index.js';
10
10
  import { getPluginManager } from '../plugins/manager.js';
11
+ import { DEFAULT_PLUGIN_PERMISSIONS } from '../plugins/trust-policy.js';
11
12
  import { getBulkRatings } from '../services/registry-api.js';
12
13
  // List subcommand - Now uses IPFS-based registry
13
14
  const listCommand = {
@@ -185,18 +186,29 @@ const installCommand = {
185
186
  { name: 'version', short: 'v', type: 'string', description: 'Specific version to install' },
186
187
  { name: 'global', short: 'g', type: 'boolean', description: 'Install globally' },
187
188
  { name: 'dev', short: 'd', type: 'boolean', description: 'Install as dev dependency' },
188
- { name: 'verify', type: 'boolean', description: 'Verify checksum (default: true)', default: true },
189
+ {
190
+ name: 'verify',
191
+ type: 'boolean',
192
+ description: 'Verify before registering (default: true). Registry installs must match the registry sha256 checksum. ' +
193
+ 'Hooks and commands are withheld from a plugin that declares trustLevel "untrusted"/"unverified" or ' +
194
+ `permissions beyond the default set (${DEFAULT_PLUGIN_PERMISSIONS.join(', ')}) unless --trust is given. ` +
195
+ '--no-verify skips all of this.',
196
+ default: true,
197
+ },
198
+ { name: 'trust', type: 'boolean', description: 'Register hooks and commands even if the plugin is untrusted or declares elevated permissions', default: false },
189
199
  { name: 'registry', short: 'r', type: 'string', description: 'Registry to use' },
190
200
  ],
191
201
  examples: [
192
202
  { command: 'claude-flow plugins install -n community-analytics', description: 'Install plugin from IPFS' },
193
203
  { command: 'claude-flow plugins install -n ./my-plugin --dev', description: 'Install local plugin' },
204
+ { command: 'claude-flow plugins install -n ./my-plugin --trust', description: 'Install a local plugin that declares elevated permissions, registering its hooks' },
194
205
  ],
195
206
  action: async (ctx) => {
196
207
  const name = ctx.flags.name;
197
208
  const version = ctx.flags.version || 'latest';
198
209
  const registryName = ctx.flags.registry;
199
210
  const verify = ctx.flags.verify !== false;
211
+ const trust = ctx.flags.trust === true;
200
212
  if (!name) {
201
213
  output.printError('Plugin name is required');
202
214
  return { success: false, exitCode: 1 };
@@ -227,7 +239,7 @@ const installCommand = {
227
239
  if (isLocalPath) {
228
240
  // Install from local path
229
241
  spinner.setText(`Installing from ${name}...`);
230
- result = await manager.installFromLocal(name);
242
+ result = await manager.installFromLocal(name, { verify, trust });
231
243
  }
232
244
  else {
233
245
  // First, try to find in registry for metadata
@@ -242,7 +254,13 @@ const installCommand = {
242
254
  }
243
255
  // Install from npm (since IPFS is demo mode)
244
256
  spinner.setText(`Installing ${name} from npm...`);
245
- result = await manager.installFromNpm(name, version !== 'latest' ? version : undefined);
257
+ result = await manager.installFromNpm(name, version !== 'latest' ? version : undefined, {
258
+ verify,
259
+ trust,
260
+ expectedChecksum: plugin?.checksum,
261
+ registryTrustLevel: plugin?.trustLevel,
262
+ registryPermissions: plugin?.permissions,
263
+ });
246
264
  }
247
265
  if (!result.success) {
248
266
  spinner.fail(`Installation failed: ${result.error}`);
@@ -251,20 +269,39 @@ const installCommand = {
251
269
  const installed = result.plugin;
252
270
  spinner.succeed(`Installed ${installed.name}@${installed.version}`);
253
271
  output.writeln();
272
+ if (result.decision?.verificationSkipped) {
273
+ output.printWarning('Verification skipped (--no-verify): hooks and commands were registered without a trust or checksum check.');
274
+ }
275
+ for (const warning of result.warnings ?? []) {
276
+ output.printWarning(warning);
277
+ }
254
278
  const boxContent = [
255
279
  `Plugin: ${installed.name}`,
256
280
  `Version: ${installed.version}`,
257
281
  `Source: ${installed.source}`,
258
282
  `Path: ${installed.path || 'N/A'}`,
259
283
  ``,
284
+ `Trust: ${installed.trustLevel ?? 'not declared'}`,
285
+ `Permissions: ${installed.permissions?.join(', ') || 'none declared'}`,
286
+ `Verification: ${installed.verification ?? 'n/a'}`,
287
+ `Install scripts: ${installed.source === 'local' ? 'n/a (local link)' : installed.scriptsRun ? 'ran' : 'skipped (use --trust to run)'}`,
288
+ ``,
260
289
  `Hooks registered: ${installed.hooks?.length || 0}`,
261
290
  `Commands added: ${installed.commands?.length || 0}`,
262
291
  ];
263
- if (plugin) {
264
- boxContent.push(`Trust: ${plugin.trustLevel}`);
265
- boxContent.push(`Permissions: ${plugin.permissions.join(', ') || 'none'}`);
266
- }
267
292
  output.printBox(boxContent.join('\n'), 'Installation Complete');
293
+ if (installed.withheld) {
294
+ output.writeln();
295
+ output.printWarning(`Withheld ${installed.withheld.hooks.length} hook(s) and ${installed.withheld.commands.length} command(s) ` +
296
+ `from ${installed.name} because it ${installed.withheld.reasons.join('; ')}.`);
297
+ if (installed.withheld.hooks.length > 0) {
298
+ output.writeln(output.dim(` Hooks not registered: ${installed.withheld.hooks.join(', ')}`));
299
+ }
300
+ if (installed.withheld.commands.length > 0) {
301
+ output.writeln(output.dim(` Commands not added: ${installed.withheld.commands.join(', ')}`));
302
+ }
303
+ output.writeln(output.dim(` To register them, uninstall and reinstall with --trust.`));
304
+ }
268
305
  return { success: true, data: installed };
269
306
  }
270
307
  catch (error) {
@@ -86,8 +86,11 @@ export const policyCommand = {
86
86
  const state = loadPolicyState(root);
87
87
  return print({ receipts: state.receipts });
88
88
  }
89
- if (operation === 'verify')
90
- return print(await verifyPolicyLedger(root));
89
+ if (operation === 'verify') {
90
+ const ledger = await verifyPolicyLedger(root);
91
+ const result = print(ledger);
92
+ return ledger.valid ? result : { ...result, success: false, exitCode: 1 };
93
+ }
91
94
  throw new Error(`unknown policy operation: ${operation}`);
92
95
  }
93
96
  catch (error) {