@claude-flow/cli 3.56.9 → 3.57.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 (101) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +3 -3
  3. package/.claude/helpers/intelligence.cjs +51 -9
  4. package/.claude/helpers/router.cjs +0 -0
  5. package/.claude/skills/verification-quality/SKILL.md +14 -11
  6. package/README.md +1 -1
  7. package/catalog-manifest.json +2 -2
  8. package/dist/src/autopilot-state.d.ts +2 -0
  9. package/dist/src/autopilot-state.js +36 -8
  10. package/dist/src/command-resolution.d.ts +15 -0
  11. package/dist/src/command-resolution.js +25 -0
  12. package/dist/src/commands/agent.js +17 -17
  13. package/dist/src/commands/claims.js +6 -8
  14. package/dist/src/commands/daemon.js +94 -33
  15. package/dist/src/commands/doctor.js +95 -18
  16. package/dist/src/commands/memory.js +14 -2
  17. package/dist/src/commands/policy.js +6 -1
  18. package/dist/src/commands/security-code-patterns.d.ts +2 -2
  19. package/dist/src/commands/security-code-patterns.js +61 -3
  20. package/dist/src/commands/security.d.ts +24 -0
  21. package/dist/src/commands/security.js +81 -35
  22. package/dist/src/commands/tui.d.ts +9 -0
  23. package/dist/src/commands/tui.js +29 -3
  24. package/dist/src/config/proven-config-refresh.d.ts +9 -0
  25. package/dist/src/config/proven-config-refresh.js +24 -3
  26. package/dist/src/funnel/local-signals.js +2 -1
  27. package/dist/src/index.js +11 -47
  28. package/dist/src/init/settings-generator.js +4 -58
  29. package/dist/src/init/types.js +1 -1
  30. package/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
  31. package/dist/src/mcp-tools/agent-execute-core.js +15 -0
  32. package/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
  33. package/dist/src/mcp-tools/agent-health-metrics.js +33 -0
  34. package/dist/src/mcp-tools/agent-tools.js +45 -22
  35. package/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
  36. package/dist/src/mcp-tools/agentbbs-federation.js +7 -2
  37. package/dist/src/mcp-tools/agentdb-tools.js +25 -2
  38. package/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
  39. package/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
  40. package/dist/src/mcp-tools/analyze-tools.js +10 -11
  41. package/dist/src/mcp-tools/autopilot-tools.js +2 -2
  42. package/dist/src/mcp-tools/browser-intent-tools.js +6 -2
  43. package/dist/src/mcp-tools/embeddings-tools.js +29 -7
  44. package/dist/src/mcp-tools/hooks-tools.js +4 -0
  45. package/dist/src/mcp-tools/memory-tools.d.ts +1 -1
  46. package/dist/src/mcp-tools/memory-tools.js +100 -25
  47. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  48. package/dist/src/mcp-tools/neural-tools.js +48 -54
  49. package/dist/src/mcp-tools/policy-tools.js +5 -3
  50. package/dist/src/mcp-tools/ruvllm-tools.js +6 -0
  51. package/dist/src/mcp-tools/security-tools.js +55 -17
  52. package/dist/src/mcp-tools/workflow-tools.js +66 -17
  53. package/dist/src/memory/intelligence.d.ts +5 -1
  54. package/dist/src/memory/intelligence.js +158 -35
  55. package/dist/src/memory/memory-bridge.js +10 -3
  56. package/dist/src/memory/memory-initializer.js +2 -1
  57. package/dist/src/memory/search-defaults.d.ts +54 -0
  58. package/dist/src/memory/search-defaults.js +37 -0
  59. package/dist/src/memory/sona-optimizer.d.ts +28 -0
  60. package/dist/src/memory/sona-optimizer.js +59 -2
  61. package/dist/src/memory/stats-file-lock.d.ts +7 -0
  62. package/dist/src/memory/stats-file-lock.js +53 -0
  63. package/dist/src/mods/command-registry/schema.d.ts +24 -24
  64. package/dist/src/ruvector/agent-wasm.d.ts +10 -0
  65. package/dist/src/ruvector/agent-wasm.js +27 -1
  66. package/dist/src/ruvector/diff-classifier.d.ts +2 -4
  67. package/dist/src/ruvector/diff-classifier.js +202 -69
  68. package/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
  69. package/dist/src/ruvector/ruvllm-wasm.js +12 -1
  70. package/dist/src/services/distill-oracle.d.ts +1 -1
  71. package/dist/src/services/distill-oracle.js +2 -2
  72. package/dist/src/services/policy-anchor-log.d.ts +13 -0
  73. package/dist/src/services/policy-anchor-log.js +242 -0
  74. package/dist/src/services/policy-archive-scan.d.ts +7 -0
  75. package/dist/src/services/policy-archive-scan.js +50 -0
  76. package/dist/src/services/policy-ledger-anchor.d.ts +14 -8
  77. package/dist/src/services/policy-ledger-anchor.js +356 -40
  78. package/dist/src/services/policy-ledger-audit.d.ts +22 -0
  79. package/dist/src/services/policy-ledger-audit.js +260 -0
  80. package/dist/src/services/policy-ledger-record.d.ts +3 -0
  81. package/dist/src/services/policy-ledger-record.js +22 -0
  82. package/dist/src/services/policy-runtime.d.ts +2 -1
  83. package/dist/src/services/policy-runtime.js +267 -40
  84. package/dist/src/services/worker-daemon.d.ts +11 -0
  85. package/dist/src/services/worker-daemon.js +37 -0
  86. package/dist/src/transfer/store/discovery.js +16 -17
  87. package/dist/src/transfer/store/types.d.ts +4 -0
  88. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  89. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  90. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
  91. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  92. package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
  93. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  94. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
  95. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  96. package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
  97. package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
  98. package/package.json +5 -5
  99. package/plugins/ruflo-console/dist/tui.mjs +162 -161
  100. package/plugins/ruflo-metaharness/hooks/screen.ts +7 -1
  101. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
@@ -2515,6 +2515,12 @@ export const doctorCommand = {
2515
2515
  name: 'doctor',
2516
2516
  description: 'System diagnostics and health checks',
2517
2517
  options: [
2518
+ {
2519
+ name: 'json',
2520
+ description: 'Emit versioned JSON health checks (cannot be combined with repair flags)',
2521
+ type: 'boolean',
2522
+ default: false
2523
+ },
2518
2524
  {
2519
2525
  name: 'fix',
2520
2526
  short: 'f',
@@ -2561,6 +2567,7 @@ export const doctorCommand = {
2561
2567
  }
2562
2568
  ],
2563
2569
  examples: [
2570
+ { command: 'claude-flow doctor --json -c node', description: 'Emit machine-readable health checks' },
2564
2571
  { command: 'claude-flow doctor', description: 'Run full health check' },
2565
2572
  { command: 'claude-flow doctor --fix', description: 'Print suggested fix commands (does not auto-apply)' },
2566
2573
  { command: 'claude-flow doctor --install', description: 'Auto-install missing dependencies' },
@@ -2575,6 +2582,21 @@ export const doctorCommand = {
2575
2582
  const verbose = ctx.flags.verbose;
2576
2583
  // Parser camelCases kebab-case flag names — read via `fixHandles`, not `['fix-handles']`.
2577
2584
  const fixHandles = ctx.flags.fixHandles;
2585
+ const json = ctx.flags.json;
2586
+ const reportError = (message) => {
2587
+ if (json)
2588
+ output.writeln(JSON.stringify({ schemaVersion: 1, checks: [], error: message }));
2589
+ else
2590
+ output.printError(message);
2591
+ return { success: false, exitCode: 1, message };
2592
+ };
2593
+ if (json && (showFix || autoInstall || fixHandles)) {
2594
+ return reportError('--json cannot be combined with --fix, --install, or --fix-handles');
2595
+ }
2596
+ // Reuse the same callable for anonymous checks so their IDs do not depend
2597
+ // on display names or on whether the full or component suite was selected.
2598
+ const helperCheck = () => checkHelperIntegrity();
2599
+ const nativeAddonCheck = () => checkNativeAddonHeaders();
2578
2600
  // #2677: `--component memory` now runs the whole memory-health suite,
2579
2601
  // not just the existence check. Values can be a single check or an
2580
2602
  // array — expanded at execution time. Stuinfla's report showed the
@@ -2592,7 +2614,7 @@ export const doctorCommand = {
2592
2614
  'browser': checkAgentBrowserVersion,
2593
2615
  'config': checkConfigFile,
2594
2616
  'stale-settings': checkStaleSettingsNpx, // #2448
2595
- 'helpers': () => checkHelperIntegrity(), // #3565
2617
+ 'helpers': helperCheck, // #3565
2596
2618
  'daemon': checkDaemonStatus,
2597
2619
  get memory() {
2598
2620
  return [
@@ -2608,7 +2630,7 @@ export const doctorCommand = {
2608
2630
  ];
2609
2631
  },
2610
2632
  'memory-package': checkMemoryPackageVersion, // #3392
2611
- 'native-addon': () => checkNativeAddonHeaders(), // #4055
2633
+ 'native-addon': nativeAddonCheck, // #4055
2612
2634
  'learning': checkLearningBridge, // #2545
2613
2635
  'learning-bridge': checkLearningBridge, // #2545
2614
2636
  'api': checkApiKeys,
@@ -2634,8 +2656,7 @@ export const doctorCommand = {
2634
2656
  };
2635
2657
  if (component !== undefined && !Object.hasOwn(componentMap, component)) {
2636
2658
  const message = `Unknown doctor component: ${JSON.stringify(component)}. Valid components: ${Object.keys(componentMap).join(', ')}`;
2637
- output.printError(message);
2638
- return { success: false, exitCode: 1, message };
2659
+ return reportError(message);
2639
2660
  }
2640
2661
  // Early-return short-circuit: `--fix-handles` is a targeted mitigation, not
2641
2662
  // part of the health-check flow. Runs, reports, exits.
@@ -2699,12 +2720,15 @@ export const doctorCommand = {
2699
2720
  output.writeln(output.dim('Upstream tracking: https://github.com/anthropics/claude-code/issues/67888'));
2700
2721
  return { success: true };
2701
2722
  }
2702
- output.writeln();
2703
- output.writeln(output.bold('RuFlo Doctor'));
2704
- output.writeln(output.dim('System diagnostics and health check'));
2705
- output.writeln(output.dim('─'.repeat(50)));
2706
- output.writeln();
2707
- const nativeAgentDbChecks = (await existingNativeAgentDbPaths())
2723
+ if (!json) {
2724
+ output.writeln();
2725
+ output.writeln(output.bold('RuFlo Doctor'));
2726
+ output.writeln(output.dim('System diagnostics and health check'));
2727
+ output.writeln(output.dim('─'.repeat(50)));
2728
+ output.writeln();
2729
+ }
2730
+ const nativeAgentDbPaths = await existingNativeAgentDbPaths();
2731
+ const nativeAgentDbChecks = nativeAgentDbPaths
2708
2732
  .map((dbPath) => () => checkNativeAgentDbStructuralIntegrity(dbPath));
2709
2733
  const allChecks = [
2710
2734
  checkVersionFreshness,
@@ -2716,7 +2740,7 @@ export const doctorCommand = {
2716
2740
  checkGitRepo,
2717
2741
  checkConfigFile,
2718
2742
  checkStaleSettingsNpx, // #2448/#2677 — runaway `npx @latest` in settings
2719
- () => checkHelperIntegrity(), // #3565 — installed signed helpers vs manifest
2743
+ helperCheck, // #3565 — installed signed helpers vs manifest
2720
2744
  checkDaemonStatus,
2721
2745
  checkMemoryDatabase,
2722
2746
  checkMemoryStructuralIntegrity, // #2737 — bounded, native quick_check on every default run
@@ -2724,7 +2748,7 @@ export const doctorCommand = {
2724
2748
  checkMemoryPersistenceDriver, // #2968/#3321 — read-only native capability probe
2725
2749
  checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
2726
2750
  checkMemoryPackageVersion, // #3392 — loaded @claude-flow/memory must satisfy the CLI's declared range
2727
- () => checkNativeAddonHeaders(), // #4055 — better-sqlite3 built against Node 24.19+ headers on 24.19-24.21
2751
+ nativeAddonCheck, // #4055 — better-sqlite3 built against Node 24.19+ headers on 24.19-24.21
2728
2752
  checkApiKeys,
2729
2753
  checkMcpServers,
2730
2754
  checkMcpSchemaOverhead, // #2726 — fixed tools/list prompt cost
@@ -2747,21 +2771,59 @@ export const doctorCommand = {
2747
2771
  const entry = componentMap[component];
2748
2772
  checksToRun = Array.isArray(entry) ? entry : [entry];
2749
2773
  }
2774
+ // These are API identifiers: keep them stable even when user-facing names
2775
+ // change. Grouped components have a distinct ID for each individual check.
2776
+ const checkIds = new Map([
2777
+ [checkVersionFreshness, 'version'], [checkNodeVersion, 'node'],
2778
+ [checkNpmVersion, 'npm'], [checkClaudeCode, 'claude'],
2779
+ [checkAgentBrowserVersion, 'browser'], [checkGit, 'git'],
2780
+ [checkGitRepo, 'git-repo'], [checkConfigFile, 'config'],
2781
+ [checkStaleSettingsNpx, 'stale-settings'], [helperCheck, 'helpers'],
2782
+ [checkDaemonStatus, 'daemon'], [checkMemoryDatabase, 'memory-presence'],
2783
+ [checkMemoryStructuralIntegrity, 'memory-structural'],
2784
+ [checkMemoryIntegrity, 'memory-integrity'],
2785
+ [checkMemoryPersistenceDriver, 'memory-driver'],
2786
+ [checkMemoryPackageVersion, 'memory-package'],
2787
+ [checkMemoryContent, 'memory-content'],
2788
+ [checkMemoryEmbeddingCoverage, 'memory-embeddings'],
2789
+ [checkMemoryReflexionCoverage, 'memory-reflexion'],
2790
+ [checkMemoryCritiqueCoverage, 'memory-critique'],
2791
+ [checkLearningBridge, 'learning'], [nativeAddonCheck, 'native-addon'],
2792
+ [checkApiKeys, 'api'], [checkMcpServers, 'mcp'],
2793
+ [checkMcpSchemaOverhead, 'mcp-overhead'], [checkAIDefence, 'aidefence'],
2794
+ [checkDiskSpace, 'disk'], [checkBuildTools, 'typescript'],
2795
+ [checkAgenticFlow, 'agentic-flow'], [checkEncryptionAtRest, 'encryption'],
2796
+ [checkFederationBreaker, 'federation'], [checkMetaharness, 'metaharness'],
2797
+ [checkMetaharnessDeclaredPackages, 'metaharness-packages'],
2798
+ [checkMetaharnessIntegration, 'metaharness-integration'],
2799
+ [checkFunnel, 'funnel'], [checkProxySponsoredConsent, 'proxy-consent'],
2800
+ [checkProxyBinary, 'proxy-binary'], [checkProxyProcess, 'proxy-process'],
2801
+ [checkProxyBindAddress, 'proxy-bind'], [checkAuth, 'auth'],
2802
+ [checkTypesafeRouter, 'typesafe'], [checkMods, 'mods'],
2803
+ ]);
2804
+ nativeAgentDbChecks.forEach((check, index) => {
2805
+ // The path distinguishes simultaneous authority files; normalize separators.
2806
+ checkIds.set(check, `memory-native:${nativeAgentDbPaths[index].replace(/\\/g, '/')}`);
2807
+ });
2808
+ if (json && checksToRun.some(check => !checkIds.has(check))) {
2809
+ return reportError('Doctor check is missing a stable JSON ID');
2810
+ }
2750
2811
  const results = [];
2751
2812
  const fixes = [];
2752
2813
  // OPTIMIZATION: Run all checks in parallel for 3-5x faster execution
2753
- const spinner = output.createSpinner({ text: 'Running health checks in parallel...', spinner: 'dots' });
2754
- spinner.start();
2814
+ const spinner = json ? undefined : output.createSpinner({ text: 'Running health checks in parallel...', spinner: 'dots' });
2815
+ spinner?.start();
2755
2816
  try {
2756
2817
  // Execute all checks concurrently
2757
2818
  const checkResults = await Promise.allSettled(checksToRun.map(check => check()));
2758
- spinner.stop();
2819
+ spinner?.stop();
2759
2820
  // Process results in order
2760
2821
  for (const settledResult of checkResults) {
2761
2822
  if (settledResult.status === 'fulfilled') {
2762
2823
  const result = settledResult.value;
2763
2824
  results.push(result);
2764
- output.writeln(formatCheck(result));
2825
+ if (!json)
2826
+ output.writeln(formatCheck(result));
2765
2827
  if (result.fix && (result.status === 'fail' || result.status === 'warn')) {
2766
2828
  fixes.push(`${result.name}: ${result.fix}`);
2767
2829
  }
@@ -2773,12 +2835,15 @@ export const doctorCommand = {
2773
2835
  message: settledResult.reason?.message || 'Unknown error'
2774
2836
  };
2775
2837
  results.push(errorResult);
2776
- output.writeln(formatCheck(errorResult));
2838
+ if (!json)
2839
+ output.writeln(formatCheck(errorResult));
2777
2840
  }
2778
2841
  }
2779
2842
  }
2780
2843
  catch (error) {
2781
- spinner.stop();
2844
+ spinner?.stop();
2845
+ if (json)
2846
+ return reportError(`Failed to run health checks: ${error instanceof Error ? error.message : String(error)}`);
2782
2847
  output.writeln(output.error('Failed to run health checks'));
2783
2848
  }
2784
2849
  // #2545: --fix / --install can actually repair the Learning Bridge by
@@ -2827,6 +2892,18 @@ export const doctorCommand = {
2827
2892
  const passed = results.filter(r => r.status === 'pass').length;
2828
2893
  const warnings = results.filter(r => r.status === 'warn').length;
2829
2894
  const failed = results.filter(r => r.status === 'fail').length;
2895
+ if (json) {
2896
+ const checks = results.map((result, index) => ({
2897
+ id: checkIds.get(checksToRun[index]),
2898
+ status: result.status,
2899
+ name: result.name,
2900
+ detail: result.message,
2901
+ ...(result.fix ? { fix: result.fix } : {}),
2902
+ }));
2903
+ const data = { schemaVersion: 1, checks };
2904
+ output.writeln(JSON.stringify(data));
2905
+ return { success: failed === 0, exitCode: failed > 0 ? 1 : 0, data };
2906
+ }
2830
2907
  output.writeln();
2831
2908
  output.writeln(output.dim('─'.repeat(50)));
2832
2909
  output.writeln();
@@ -3,6 +3,7 @@
3
3
  * Memory operations for AgentDB integration
4
4
  */
5
5
  import { validateAppendConditions } from '../memory/append-conditions.js';
6
+ import { DEFAULT_SEARCH_THRESHOLD, nearMisses } from '../memory/search-defaults.js';
6
7
  import { output } from '../output.js';
7
8
  import { select, confirm, input } from '../prompt.js';
8
9
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
@@ -430,7 +431,7 @@ const searchCommand = {
430
431
  // default to 0.7, which silently re-broke #2558: `memory search` matched
431
432
  // content word-for-word and still returned nothing. Regression guard:
432
433
  // __tests__/memory-search-recall-2558.test.ts.
433
- default: 0.3
434
+ default: 0.2
434
435
  },
435
436
  {
436
437
  name: 'type',
@@ -499,7 +500,9 @@ const searchCommand = {
499
500
  // coalescing preserves an explicit zero. Fallback aligned with the
500
501
  // option's declared `default: 0.7` (was `0.3` — the two disagreed
501
502
  // and --help advertised a default the code did not honor).
502
- const threshold = ctx.flags.threshold ?? 0.3;
503
+ const threshold = ctx.flags.threshold ?? DEFAULT_SEARCH_THRESHOLD;
504
+ // The parser fills the option's declared default, so an untouched flag arrives as the default value.
505
+ const thresholdWasDefault = ctx.flags.threshold === undefined || ctx.flags.threshold === DEFAULT_SEARCH_THRESHOLD;
503
506
  const searchType = ctx.flags.type || 'semantic';
504
507
  const buildHnsw = (ctx.flags['build-hnsw'] || ctx.flags.buildHnsw);
505
508
  const requestedIntent = ctx.flags.intent || 'mixed';
@@ -738,6 +741,15 @@ const searchCommand = {
738
741
  output.writeln();
739
742
  if (results.length === 0) {
740
743
  output.printWarning('No results found');
744
+ if (thresholdWasDefault) {
745
+ const near = await nearMisses(searchEntries, { query, namespace, provenanceFilter, dbPath: dbPathSearch });
746
+ if (near) {
747
+ output.writeln(output.dim(` ${near.hint.replace(' in nearMisses', ' below').replace('Pass threshold: 0', 'Pass --threshold 0')}`));
748
+ for (const n of near.nearMisses) {
749
+ output.writeln(output.dim(` ${n.similarity.toFixed(2)} ${n.namespace}/${n.key}`));
750
+ }
751
+ }
752
+ }
741
753
  await warnIfSiblingHasRows(ctx.flags.path);
742
754
  output.writeln(output.dim('Try: claude-flow memory store -k "key" --value "data"'));
743
755
  return { success: true, data: [] };
@@ -30,6 +30,7 @@ export const policyCommand = {
30
30
  { name: 'mode', type: 'string', description: 'Policy mode: legacy | observe | enforce' },
31
31
  { name: 'project-root', type: 'string', description: 'Project root containing .claude-flow/policy' },
32
32
  { name: 'establish-anchor', type: 'boolean', description: 'verify: explicitly anchor a ledger that has receipts but no anchor (interactive, logged with user and time)' },
33
+ { name: 'full', type: 'boolean', description: 'verify: additionally replay the archived receipts (receipts.ledger.jsonl) to verify the complete historical chain, not just the retained hot tail' },
33
34
  ],
34
35
  async action(context) {
35
36
  const args = context.args ?? [];
@@ -93,9 +94,13 @@ export const policyCommand = {
93
94
  }
94
95
  if (operation === 'verify') {
95
96
  const establishAnchor = flags['establish-anchor'] === true || flags.establishAnchor === true;
97
+ const full = flags.full === true;
96
98
  if (establishAnchor)
97
99
  requireInteractiveAdministrator();
98
- const ledger = await verifyPolicyLedger(root, { establishAnchor });
100
+ const ledger = await verifyPolicyLedger(root, { establishAnchor, full });
101
+ if (ledger.error === 'receipt-key-id-mismatch') {
102
+ output.printError('Restore the original CLAUDE_FLOW_POLICY_KEY_ID for this ledger; do not rewrite or re-sign historical receipts.');
103
+ }
99
104
  const result = print({ ...ledger, anchors: anchorPosture(root), scope: LEDGER_TRUST_SCOPE });
100
105
  return ledger.valid ? result : { ...result, success: false, exitCode: 1 };
101
106
  }
@@ -6,8 +6,8 @@
6
6
  * - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
7
7
  * not a finding;
8
8
  * - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
9
- * - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and no longer skips
10
- * execSync as `exec[^S]` did;
9
+ * - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and also catches
10
+ * dynamic exec/execSync command strings when the import was on another line;
11
11
  * - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
12
12
  * when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
13
13
  * stands where SQL takes a value, and it is not inside a quoted SQL string.
@@ -6,14 +6,72 @@
6
6
  * - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
7
7
  * not a finding;
8
8
  * - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
9
- * - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and no longer skips
10
- * execSync as `exec[^S]` did;
9
+ * - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and also catches
10
+ * dynamic exec/execSync command strings when the import was on another line;
11
11
  * - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
12
12
  * when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
13
13
  * stands where SQL takes a value, and it is not inside a quoted SQL string.
14
14
  */
15
15
  /** No `g` flag, so `test` keeps no lastIndex between lines. */
16
16
  const re = (pattern) => (line) => pattern.test(line);
17
+ /** Detect a dynamic shell command at an exec/execSync call site, independent of its import. */
18
+ const interpolatedExec = (line) => {
19
+ let quote = null;
20
+ for (let i = 0; i < line.length; i++) {
21
+ const ch = line[i];
22
+ if (quote !== null) {
23
+ if (ch === '\\') {
24
+ i++;
25
+ continue;
26
+ }
27
+ if (ch === quote)
28
+ quote = null;
29
+ continue;
30
+ }
31
+ if (ch === "'" || ch === '"' || ch === '`') {
32
+ quote = ch;
33
+ continue;
34
+ }
35
+ if (i > 0 && /[\w$]/.test(line[i - 1]))
36
+ continue;
37
+ const name = line.startsWith('execSync', i) ? 'execSync' : line.startsWith('exec', i) ? 'exec' : '';
38
+ if (!name)
39
+ continue;
40
+ let arg = i + name.length;
41
+ while (/\s/.test(line[arg] ?? ''))
42
+ arg++;
43
+ if (line[arg++] !== '(')
44
+ continue;
45
+ while (/\s/.test(line[arg] ?? ''))
46
+ arg++;
47
+ const delimiter = line[arg];
48
+ if (delimiter !== '`' && delimiter !== "'" && delimiter !== '"')
49
+ continue;
50
+ for (let end = arg + 1; end < line.length; end++) {
51
+ if (line[end] === '\\') {
52
+ end++;
53
+ continue;
54
+ }
55
+ if (delimiter === '`' && line[end] === '$' && line[end + 1] === '{')
56
+ return true;
57
+ if (line[end] !== delimiter)
58
+ continue;
59
+ if (delimiter !== '`') {
60
+ let next = end + 1;
61
+ while (/\s/.test(line[next] ?? ''))
62
+ next++;
63
+ if (line[next++] === '+') {
64
+ while (/\s/.test(line[next] ?? ''))
65
+ next++;
66
+ if (/[A-Za-z_$]/.test(line[next] ?? ''))
67
+ return true;
68
+ }
69
+ }
70
+ break;
71
+ }
72
+ }
73
+ return false;
74
+ };
17
75
  // A `.length` counts only on a param-, value- or arg-named array: `req.body.length` is whatever the client sent.
18
76
  const COUNTER = /^\s*(?:\d+|[ijkn]|(?:[A-Za-z_$][\w$]*(?:Params?|Placeholders?|Values?|Args?)|params?|placeholders?|values?|args?)\.length|[A-Za-z_$]*(?:[Pp]aram|[Pp]laceholder)[\w$]*)\s*(?:[+-]\s*\d+\s*)?$/;
19
77
  // Where a pg parameter stands in SQL: after a comparison, a list or call opener, or a clause keyword.
@@ -70,7 +128,7 @@ export const CODE_PATTERNS = [
70
128
  { match: re(/(?<![\w$])eval\s*\(/), type: 'Eval Usage', severity: 'medium', desc: 'eval() can execute arbitrary code' },
71
129
  { match: re(/innerHTML\s*=/), type: 'innerHTML', severity: 'medium', desc: 'XSS risk with innerHTML' },
72
130
  { match: re(/dangerouslySetInnerHTML/), type: 'React XSS', severity: 'medium', desc: 'React XSS risk' },
73
- { match: re(/child_process.*exec/), type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
131
+ { match: line => /child_process.*exec/.test(line) || interpolatedExec(line), type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
74
132
  { match: line => sqlInterpolation(line) && sqlInterpolation(withoutPlaceholders(line)), type: 'SQL Injection', severity: 'high', desc: 'Possible SQL injection' },
75
133
  ];
76
134
  /**
@@ -5,6 +5,30 @@
5
5
  * Created with ❤️ by ruv.io
6
6
  */
7
7
  import type { Command } from '../types.js';
8
+ /**
9
+ * #4124: `npm audit` can fail without exiting through the vulnerability path.
10
+ * In a pnpm/yarn/bun project there is no package-lock.json, so it prints
11
+ * `{"error":{"code":"ENOLOCK",...}}`. That has no `vulnerabilities` key, and
12
+ * both `scan` and `cve` used to read it as a clean tree. Only a report with a
13
+ * `vulnerabilities` object counts as a scan; anything else is "could not scan".
14
+ */
15
+ type NpmAuditVuln = {
16
+ severity: string;
17
+ via: Array<{
18
+ title?: string;
19
+ url?: string;
20
+ source?: number;
21
+ name?: string;
22
+ } | string>;
23
+ };
24
+ export type NpmAuditResult = {
25
+ ok: true;
26
+ vulnerabilities: Record<string, NpmAuditVuln>;
27
+ } | {
28
+ ok: false;
29
+ reason: string;
30
+ };
31
+ export declare function interpretNpmAudit(raw: string, dir?: string): NpmAuditResult;
8
32
  export declare const securityCommand: Command;
9
33
  export default securityCommand;
10
34
  //# sourceMappingURL=security.d.ts.map
@@ -155,6 +155,7 @@ const scanCommand = {
155
155
  spinner.start();
156
156
  const findings = [];
157
157
  let criticalCount = 0, highCount = 0, mediumCount = 0, lowCount = 0;
158
+ let depsSkipped;
158
159
  try {
159
160
  const fs = await import('fs');
160
161
  const path = await import('path');
@@ -178,35 +179,41 @@ const scanCommand = {
178
179
  // npm audit exits non-zero when vulnerabilities found — stdout still has JSON
179
180
  auditResult = (auditErr instanceof Error && 'stdout' in auditErr ? auditErr.stdout : undefined) || '{}';
180
181
  }
181
- try {
182
- const audit = JSON.parse(auditResult);
183
- if (audit.vulnerabilities) {
184
- for (const [pkg, vuln] of Object.entries(audit.vulnerabilities)) {
185
- const sev = vuln.severity || 'low';
186
- const title = Array.isArray(vuln.via) && vuln.via[0]?.title ? vuln.via[0].title : 'Vulnerability';
187
- if (sev === 'critical')
188
- criticalCount++;
189
- else if (sev === 'high')
190
- highCount++;
191
- else if (sev === 'moderate' || sev === 'medium')
192
- mediumCount++;
193
- else
194
- lowCount++;
195
- findings.push({
196
- severity: sev === 'critical' ? 'critical' :
197
- sev === 'high' ? 'high' :
198
- sev === 'moderate' || sev === 'medium' ? 'medium' : 'low',
199
- type: 'Dependency CVE',
200
- location: `package.json:${pkg}`,
201
- description: title.substring(0, 35),
202
- });
203
- }
182
+ const audit = interpretNpmAudit(auditResult, path.resolve(target));
183
+ if (!audit.ok) {
184
+ depsSkipped = audit.reason;
185
+ }
186
+ else {
187
+ for (const [pkg, vuln] of Object.entries(audit.vulnerabilities)) {
188
+ const sev = vuln.severity || 'low';
189
+ const first = Array.isArray(vuln.via) ? vuln.via[0] : undefined;
190
+ const title = typeof first === 'object' && first?.title ? first.title : 'Vulnerability';
191
+ if (sev === 'critical')
192
+ criticalCount++;
193
+ else if (sev === 'high')
194
+ highCount++;
195
+ else if (sev === 'moderate' || sev === 'medium')
196
+ mediumCount++;
197
+ else
198
+ lowCount++;
199
+ findings.push({
200
+ severity: sev === 'critical' ? 'critical' :
201
+ sev === 'high' ? 'high' :
202
+ sev === 'moderate' || sev === 'medium' ? 'medium' : 'low',
203
+ type: 'Dependency CVE',
204
+ location: `package.json:${pkg}`,
205
+ description: title.substring(0, 35),
206
+ });
204
207
  }
205
208
  }
206
- catch { /* JSON parse failed, no vulns */ }
207
209
  }
210
+ else {
211
+ depsSkipped = 'No package.json found in the scan target; dependency scan could not run.';
212
+ }
213
+ }
214
+ catch (err) {
215
+ depsSkipped = `npm audit could not run: ${err instanceof Error ? err.message : String(err)}`;
208
216
  }
209
- catch { /* npm audit failed */ }
210
217
  }
211
218
  // Phase 2: Scan for hardcoded secrets
212
219
  if (scanType === 'all' || scanType === 'code') {
@@ -342,9 +349,14 @@ const scanCommand = {
342
349
  output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
343
350
  }
344
351
  }
352
+ else if (depsSkipped) {
353
+ output.writeln(output.warning('No issues found by the checks that ran, but the dependency scan did not run.'));
354
+ }
345
355
  else {
346
356
  output.writeln(output.success('No security issues found!'));
347
357
  }
358
+ if (depsSkipped)
359
+ output.writeln(output.warning(`Dependency scan skipped: ${depsSkipped}`));
348
360
  output.writeln();
349
361
  output.printBox([
350
362
  `Target: ${target}`,
@@ -367,6 +379,7 @@ const scanCommand = {
367
379
  low: lowCount,
368
380
  total: findings.length,
369
381
  },
382
+ ...(depsSkipped ? { incomplete: true, skipped: { deps: depsSkipped } } : {}),
370
383
  findings,
371
384
  };
372
385
  // Persist the scan result so downstream consumers (the statusline's
@@ -408,6 +421,11 @@ const scanCommand = {
408
421
  output.printJson(record);
409
422
  else if (outputFormat === 'sarif')
410
423
  output.printJson(scanSarif(findings));
424
+ if (depsSkipped) {
425
+ if (outputFormat !== 'text')
426
+ output.printError(`Dependency scan skipped: ${depsSkipped}`);
427
+ return { success: false, exitCode: 2 };
428
+ }
411
429
  return { success: findings.length === 0 || (criticalCount === 0 && highCount === 0) };
412
430
  }
413
431
  catch (error) {
@@ -418,6 +436,36 @@ const scanCommand = {
418
436
  }
419
437
  },
420
438
  };
439
+ export function interpretNpmAudit(raw, dir) {
440
+ let audit;
441
+ try {
442
+ audit = JSON.parse(raw);
443
+ }
444
+ catch {
445
+ return { ok: false, reason: 'npm audit did not return a JSON report.' };
446
+ }
447
+ if (audit && typeof audit === 'object' && audit.error) {
448
+ const code = typeof audit.error.code === 'string' ? audit.error.code : 'error';
449
+ const summary = typeof audit.error.summary === 'string' ? audit.error.summary : 'npm audit failed.';
450
+ return { ok: false, reason: `npm audit failed (${code}): ${summary}${code === 'ENOLOCK' ? auditHint(dir) : ''}` };
451
+ }
452
+ if (!audit || typeof audit !== 'object' || !audit.vulnerabilities || typeof audit.vulnerabilities !== 'object' || Array.isArray(audit.vulnerabilities)) {
453
+ return { ok: false, reason: 'npm audit returned no vulnerability report.' };
454
+ }
455
+ return { ok: true, vulnerabilities: audit.vulnerabilities };
456
+ }
457
+ function auditHint(dir) {
458
+ if (!dir)
459
+ return '';
460
+ const has = (f) => existsSync(resolvePath(dir, f));
461
+ if (has('pnpm-lock.yaml'))
462
+ return ' This is a pnpm project: run `pnpm audit` instead.';
463
+ if (has('yarn.lock'))
464
+ return ' This is a yarn project: run `yarn npm audit` (Berry) or `yarn audit` (v1) instead.';
465
+ if (has('bun.lock') || has('bun.lockb'))
466
+ return ' This is a bun project: run `bun audit` instead.';
467
+ return ' Create one with `npm i --package-lock-only`, or use your package manager\'s audit.';
468
+ }
421
469
  // CVE subcommand
422
470
  const cveCommand = {
423
471
  name: 'cve',
@@ -451,22 +499,20 @@ const cveCommand = {
451
499
  // npm audit exits non-zero when vulnerabilities found — stdout still has JSON
452
500
  auditJson = (e instanceof Error && 'stdout' in e ? e.stdout : '') || '{}';
453
501
  }
454
- let audit;
455
- try {
456
- audit = JSON.parse(auditJson);
457
- }
458
- catch {
459
- output.writeln(output.warning('⚠ Could not parse `npm audit --json` output.'));
460
- output.writeln(output.dim('Make sure you are inside a project with a package.json.'));
502
+ const audit = interpretNpmAudit(auditJson, process.cwd());
503
+ if (!audit.ok) {
504
+ output.writeln(output.warning(`⚠ Could not check dependencies: ${audit.reason}`));
505
+ output.writeln(output.dim('No result is reported rather than a false all-clear.'));
461
506
  return { success: false, exitCode: 2 };
462
507
  }
463
- const vulns = audit.vulnerabilities || {};
508
+ const vulns = audit.vulnerabilities;
464
509
  const cveRows = [];
465
510
  const CVE_RE = /CVE-\d{4}-\d{4,7}/g;
466
511
  for (const [pkg, v] of Object.entries(vulns)) {
467
512
  const sev = v.severity || 'low';
468
- const titles = (v.via || []).filter((x) => typeof x === 'object' && x?.title).map((x) => x.title);
469
- const urls = (v.via || []).filter((x) => typeof x === 'object' && x?.url).map((x) => x.url);
513
+ const via = (v.via || []).filter((x) => typeof x === 'object' && x !== null);
514
+ const titles = via.filter((x) => x.title).map((x) => x.title);
515
+ const urls = via.filter((x) => x.url).map((x) => x.url);
470
516
  const allText = `${titles.join(' ')} ${urls.join(' ')}`;
471
517
  const cveIds = Array.from(new Set(allText.match(CVE_RE) || []));
472
518
  cveRows.push({ pkg, severity: sev, cveIds, title: titles[0] || 'Vulnerability', url: urls[0] });
@@ -1,6 +1,15 @@
1
1
  import type { Command } from '../types.js';
2
2
  /** `<own install>/plugins/ruflo-console/dist/tui.mjs`, the first that exists. */
3
3
  export declare function locateTuiBundle(startDir?: string): string | null;
4
+ /** The look and motion flags: `--look bbs|orbital|plain` (default bbs, as in the console), `--fps N` (0-30, default 8; 0 draws pictures still), `--no-anim`, `--no-pictures`. A bad value is ignored, never passed on. */
5
+ export declare function lookFlags(flags: Record<string, unknown> | undefined): {
6
+ look?: string;
7
+ fps?: number;
8
+ anim?: boolean;
9
+ pictures?: boolean;
10
+ boot?: boolean;
11
+ mouse?: boolean;
12
+ };
4
13
  /** The AI page's settings from the flags (and one environment variable for the endpoint list). Absent flags leave the defaults; a bad number is ignored, never passed on. */
5
14
  export declare function aiPage(flags: Record<string, unknown> | undefined): {
6
15
  model?: string;