@claude-flow/cli 3.63.9 → 3.65.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 (90) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/catalog-manifest.json +3 -3
  3. package/dist/src/commands/cost.d.ts +18 -0
  4. package/dist/src/commands/cost.js +195 -0
  5. package/dist/src/commands/doctor-auto-approve.d.ts +9 -0
  6. package/dist/src/commands/doctor-auto-approve.js +32 -0
  7. package/dist/src/commands/doctor.js +28 -2
  8. package/dist/src/commands/hive-mind-worker.d.ts +2 -0
  9. package/dist/src/commands/hive-mind-worker.js +9 -3
  10. package/dist/src/commands/hive-mind.js +15 -1
  11. package/dist/src/commands/hooks.js +45 -0
  12. package/dist/src/commands/index.js +2 -0
  13. package/dist/src/commands/init.js +76 -2
  14. package/dist/src/commands/mods.js +13 -0
  15. package/dist/src/commands/plugins-paths.d.ts +13 -0
  16. package/dist/src/commands/plugins-paths.js +55 -0
  17. package/dist/src/commands/plugins-verify.d.ts +9 -0
  18. package/dist/src/commands/plugins-verify.js +92 -0
  19. package/dist/src/commands/plugins.js +13 -1
  20. package/dist/src/cost/attribution.d.ts +35 -0
  21. package/dist/src/cost/attribution.js +101 -0
  22. package/dist/src/cost/budget-file.d.ts +57 -0
  23. package/dist/src/cost/budget-file.js +113 -0
  24. package/dist/src/cost/litellm.d.ts +69 -0
  25. package/dist/src/cost/litellm.js +135 -0
  26. package/dist/src/init/executor.d.ts +7 -2
  27. package/dist/src/init/executor.js +64 -10
  28. package/dist/src/init/helper-commands.d.ts +16 -0
  29. package/dist/src/init/helper-commands.js +73 -0
  30. package/dist/src/init/risky-settings-gate.d.ts +39 -0
  31. package/dist/src/init/risky-settings-gate.js +130 -0
  32. package/dist/src/init/settings-risk-scanner.d.ts +35 -0
  33. package/dist/src/init/settings-risk-scanner.js +85 -0
  34. package/dist/src/init/types.d.ts +12 -0
  35. package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
  36. package/dist/src/mcp-tools/capability-brain.js +1 -1
  37. package/dist/src/mcp-tools/hooks-tools.d.ts +1 -0
  38. package/dist/src/mcp-tools/hooks-tools.js +38 -0
  39. package/dist/src/memory/verified-guards.d.ts +70 -0
  40. package/dist/src/memory/verified-guards.js +101 -0
  41. package/dist/src/memory/verified-outcomes.d.ts +228 -0
  42. package/dist/src/memory/verified-outcomes.js +372 -0
  43. package/dist/src/memory/verified-reverify.d.ts +63 -0
  44. package/dist/src/memory/verified-reverify.js +174 -0
  45. package/dist/src/missions/schemas.d.ts +36 -36
  46. package/dist/src/mods/apply.d.ts +2 -0
  47. package/dist/src/mods/apply.js +68 -8
  48. package/dist/src/mods/command-registry/schema.d.ts +222 -222
  49. package/dist/src/mods/compact-snapshots.d.ts +21 -0
  50. package/dist/src/mods/compact-snapshots.js +45 -0
  51. package/dist/src/mods/headless-check.d.ts +58 -0
  52. package/dist/src/mods/headless-check.js +137 -0
  53. package/dist/src/mods/headless-plugins.d.ts +44 -0
  54. package/dist/src/mods/headless-plugins.js +92 -0
  55. package/dist/src/mods/install-trust.d.ts +28 -0
  56. package/dist/src/mods/install-trust.js +127 -0
  57. package/dist/src/mods/install.d.ts +19 -0
  58. package/dist/src/mods/install.js +38 -1
  59. package/dist/src/mods/plugin-paths.d.ts +60 -0
  60. package/dist/src/mods/plugin-paths.js +131 -0
  61. package/dist/src/mods/plugin-repair.d.ts +8 -0
  62. package/dist/src/mods/plugin-repair.js +7 -0
  63. package/dist/src/plugins/marketplace-trust/scan.d.ts +23 -0
  64. package/dist/src/plugins/marketplace-trust/scan.js +255 -0
  65. package/dist/src/plugins/marketplace-trust/signature.d.ts +53 -0
  66. package/dist/src/plugins/marketplace-trust/signature.js +140 -0
  67. package/dist/src/plugins/marketplace-trust/verify-status.d.ts +42 -0
  68. package/dist/src/plugins/marketplace-trust/verify-status.js +110 -0
  69. package/dist/src/services/headless-worker-executor.d.ts +2 -0
  70. package/dist/src/services/headless-worker-executor.js +13 -3
  71. package/package.json +2 -1
  72. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +8 -1
  73. package/plugins/ruflo-metaharness/CHANGELOG.md +6 -0
  74. package/plugins/ruflo-metaharness/README.md +14 -1
  75. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +2 -0
  76. package/plugins/ruflo-metaharness/hooks/hooks.json +16 -2
  77. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +12 -2
  78. package/plugins/ruflo-metaharness/scripts/_secret-env.cjs +50 -0
  79. package/plugins/ruflo-metaharness/scripts/_secret.mjs +35 -0
  80. package/plugins/ruflo-metaharness/scripts/smoke.sh +7 -7
  81. package/plugins/ruflo-metaharness/skills/harness-bench/SKILL.md +2 -1
  82. package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +1 -1
  83. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +2 -1
  84. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
  85. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +2 -1
  86. package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +2 -1
  87. package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +1 -0
  88. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  89. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +2 -1
  90. package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +1 -1
@@ -11,6 +11,39 @@ import { executeInit, executeUpgrade, executeUpgradeWithMissing, DEFAULT_INIT_OP
11
11
  import { executeGrokInit } from '../init/grok-generator.js';
12
12
  import { ENROLLMENT_SCREEN, recordEnrollmentOutcome, shouldOfferEnrollment, } from '../funnel/enrollment.js';
13
13
  import { commandExists } from '../services/harness-hosts.js';
14
+ import { riskyModeFromFlags } from '../init/risky-settings-gate.js';
15
+ const RISKY_FLAG_OPTIONS = [
16
+ {
17
+ // ADR-509 §2.3
18
+ name: 'keep-risky-settings',
19
+ description: 'Carry forward project auto-approve keys found in .claude/settings.json (enableAllProjectMcpServers, enabledMcpjsonServers, bypassPermissions, blanket Bash allows, non-ruflo hooks) instead of stopping',
20
+ type: 'boolean',
21
+ default: false,
22
+ },
23
+ {
24
+ name: 'drop-risky-settings',
25
+ description: 'Write the merged .claude/settings.json without those auto-approve keys instead of stopping',
26
+ type: 'boolean',
27
+ default: false,
28
+ },
29
+ ];
30
+ /** ADR-509 §2.3 — print the keys init stopped on (or kept/dropped). Returns the exit result when it stopped. */
31
+ function reportRiskySettings(result, mode) {
32
+ if (!result.riskySettings?.length)
33
+ return null;
34
+ const head = result.stoppedOnRiskySettings
35
+ ? 'Stopped: .claude/settings.json carries project auto-approve keys that would be carried forward:'
36
+ : mode === 'drop'
37
+ ? 'Dropped these project auto-approve keys from .claude/settings.json:'
38
+ : 'Kept these project auto-approve keys in .claude/settings.json (--keep-risky-settings):';
39
+ (result.stoppedOnRiskySettings ? output.printError : output.printWarning).call(output, head);
40
+ for (const line of result.riskySettings)
41
+ output.printInfo(` • ${line}`);
42
+ if (!result.stoppedOnRiskySettings)
43
+ return null;
44
+ output.printInfo('Nothing was written. Re-run with --drop-risky-settings to write the merged settings without these keys, or --keep-risky-settings to keep them.');
45
+ return { success: false, exitCode: 1, message: 'Stopped on risky project settings', data: { riskySettings: result.riskySettings } };
46
+ }
14
47
  /**
15
48
  * ADR-302 post-init capability enrollment. One-time, interactive-TTY-only,
16
49
  * skippable with --no-signup, auto-skipped in CI/automation. Never throws
@@ -694,12 +727,22 @@ const initClaudeAction = async (ctx) => {
694
727
  if (noGlobal) {
695
728
  options.skipGlobalClaudeMd = true;
696
729
  }
730
+ const riskyMode = riskyModeFromFlags(ctx.flags);
731
+ if (typeof riskyMode === 'object') {
732
+ output.printError(riskyMode.error);
733
+ return { success: false, exitCode: 1 };
734
+ }
735
+ options.riskySettings = riskyMode;
697
736
  // Create spinner
698
737
  const spinner = output.createSpinner({ text: 'Initializing...' });
699
738
  spinner.start();
700
739
  try {
701
740
  // Execute initialization
702
741
  const result = await executeInit(options);
742
+ if (result.stoppedOnRiskySettings) {
743
+ spinner.fail('Initialization stopped');
744
+ return reportRiskySettings(result, riskyMode);
745
+ }
703
746
  if (!result.success) {
704
747
  spinner.fail('Initialization failed');
705
748
  for (const error of result.errors) {
@@ -708,6 +751,7 @@ const initClaudeAction = async (ctx) => {
708
751
  return { success: false, exitCode: 1 };
709
752
  }
710
753
  spinner.succeed('RuFlo V3 initialized successfully!');
754
+ reportRiskySettings(result, riskyMode);
711
755
  output.writeln();
712
756
  // Display summary
713
757
  const summary = [];
@@ -1157,7 +1201,13 @@ const wizardCommand = {
1157
1201
  output.writeln();
1158
1202
  const spinner = output.createSpinner({ text: 'Initializing...' });
1159
1203
  spinner.start();
1204
+ const wizardRisky = riskyModeFromFlags(ctx.flags);
1205
+ options.riskySettings = typeof wizardRisky === 'object' ? 'stop' : wizardRisky;
1160
1206
  const result = await executeInit(options);
1207
+ if (result.stoppedOnRiskySettings) {
1208
+ spinner.fail('Setup stopped');
1209
+ return reportRiskySettings(result, options.riskySettings);
1210
+ }
1161
1211
  if (!result.success) {
1162
1212
  spinner.fail('Initialization failed');
1163
1213
  for (const error of result.errors) {
@@ -1362,6 +1412,7 @@ const hooksCommand = {
1362
1412
  description: 'Initialize only hooks configuration',
1363
1413
  options: [
1364
1414
  { name: 'all', description: 'Enable all hooks', type: 'boolean', default: true },
1415
+ ...RISKY_FLAG_OPTIONS,
1365
1416
  { name: 'minimal', description: 'Enable only essential hooks', type: 'boolean', default: false },
1366
1417
  ],
1367
1418
  action: async (ctx) => {
@@ -1403,9 +1454,19 @@ const hooksCommand = {
1403
1454
  }
1404
1455
  : DEFAULT_INIT_OPTIONS.hooks,
1405
1456
  };
1457
+ const hooksRisky = riskyModeFromFlags(ctx.flags);
1458
+ if (typeof hooksRisky === 'object') {
1459
+ output.printError(hooksRisky.error);
1460
+ return { success: false, exitCode: 1 };
1461
+ }
1462
+ options.riskySettings = hooksRisky;
1406
1463
  const spinner = output.createSpinner({ text: 'Creating hooks configuration...' });
1407
1464
  spinner.start();
1408
1465
  const result = await executeInit(options);
1466
+ if (result.stoppedOnRiskySettings) {
1467
+ spinner.fail('Hooks configuration stopped');
1468
+ return reportRiskySettings(result, hooksRisky);
1469
+ }
1409
1470
  if (result.success) {
1410
1471
  spinner.succeed(`Created settings.json with ${result.summary.hooksEnabled} hooks enabled`);
1411
1472
  if (result.warnings && result.warnings.length > 0) {
@@ -1465,8 +1526,14 @@ const upgradeCommand = {
1465
1526
  type: 'boolean',
1466
1527
  default: true,
1467
1528
  },
1529
+ ...RISKY_FLAG_OPTIONS,
1468
1530
  ],
1469
1531
  action: async (ctx) => {
1532
+ const upgradeRisky = riskyModeFromFlags(ctx.flags);
1533
+ if (typeof upgradeRisky === 'object') {
1534
+ output.printError(upgradeRisky.error);
1535
+ return { success: false, exitCode: 1 };
1536
+ }
1470
1537
  const addMissing = (ctx.flags['add-missing'] || ctx.flags.addMissing);
1471
1538
  const upgradeSettings = (ctx.flags.settings);
1472
1539
  const upgradeMods = ctx.flags.mods === true || (ctx.flags.mods !== false && (addMissing || upgradeSettings));
@@ -1492,8 +1559,13 @@ const upgradeCommand = {
1492
1559
  spinner.start();
1493
1560
  try {
1494
1561
  const result = addMissing
1495
- ? await executeUpgradeWithMissing(ctx.cwd, upgradeSettings)
1496
- : await executeUpgrade(ctx.cwd, upgradeSettings);
1562
+ ? await executeUpgradeWithMissing(ctx.cwd, upgradeSettings, upgradeRisky)
1563
+ : await executeUpgrade(ctx.cwd, upgradeSettings, upgradeRisky);
1564
+ if (result.stoppedOnRiskySettings) {
1565
+ spinner.fail('Upgrade stopped');
1566
+ return reportRiskySettings(result, upgradeRisky);
1567
+ }
1568
+ reportRiskySettings(result, upgradeRisky);
1497
1569
  if (!result.success) {
1498
1570
  spinner.fail('Upgrade failed');
1499
1571
  for (const error of result.errors) {
@@ -1594,6 +1666,7 @@ export const initCommand = {
1594
1666
  description: 'Initialize RuFlo in the current directory',
1595
1667
  subcommands: [wizardCommand, checkCommand, skillsCommand, hooksCommand, upgradeCommand],
1596
1668
  options: [
1669
+ ...RISKY_FLAG_OPTIONS,
1597
1670
  {
1598
1671
  name: 'force',
1599
1672
  short: 'f',
@@ -1748,6 +1821,7 @@ export const initCommand = {
1748
1821
  { command: 'claude-flow init --minimal', description: 'Initialize with minimal configuration' },
1749
1822
  { command: 'claude-flow init --full', description: 'Initialize with all components' },
1750
1823
  { command: 'claude-flow init --force', description: 'Reinitialize and overwrite existing config' },
1824
+ { command: 'claude-flow init --drop-risky-settings', description: 'Merge into an existing settings.json without its auto-approve keys (init stops on them otherwise)' },
1751
1825
  { command: 'claude-flow init --only-claude', description: 'Only create Claude Code integration' },
1752
1826
  { command: 'claude-flow init --skip-claude', description: 'Only create V3 runtime' },
1753
1827
  { command: 'claude-flow init wizard', description: 'Interactive setup wizard' },
@@ -13,6 +13,11 @@ import { join, resolve } from 'node:path';
13
13
  import { directoryMarketplace, MARKETPLACE_NAME } from '../mods/install.js';
14
14
  import { probeMods } from '../mods/probe.js';
15
15
  import { applyMods, removeMods } from '../mods/apply.js';
16
+ import { signatureFindings } from '../mods/install-trust.js';
17
+ import { acceptRiskFrom } from '../plugins/marketplace-trust/scan.js';
18
+ import { claudeConfigDir, marketplaceState } from '../mods/plugin-resolve.js';
19
+ import { MOD_PLUGINS } from '../mods/install.js';
20
+ import { homedir } from 'node:os';
16
21
  /**
17
22
  * `--source local` / `--marketplace-path <dir>`: a ruflo checkout as a
18
23
  * directory marketplace (dogfooding). It must be a ruflo marketplace.
@@ -53,6 +58,7 @@ const installSub = {
53
58
  { name: 'strict', description: 'Exit 1 when a required plugin could not be made loadable', type: 'boolean', default: false },
54
59
  { name: 'source', description: 'github (ruvnet/ruflo, default) | local (a ruflo checkout as a directory marketplace: plugins load live from it)', type: 'string', default: 'github' },
55
60
  { name: 'marketplace-path', description: 'With --source local: the ruflo checkout (default: the project root)', type: 'string' },
61
+ { name: 'accept-risk', description: 'Install this plugin (name, comma list) even though the pre-install scan found a high-severity issue; recorded in .claude-flow/mods/install.json (ADR-509)', type: 'string' },
56
62
  ],
57
63
  action: async (ctx) => {
58
64
  const scope = ctx.flags.scope ?? 'local';
@@ -80,6 +86,7 @@ const installSub = {
80
86
  marketplace,
81
87
  dryRun: ctx.flags.dryRun === true || ctx.flags['dry-run'] === true,
82
88
  pluginInstall: ctx.flags.pluginInstall !== false && ctx.flags['plugin-install'] !== false,
89
+ acceptRisk: acceptRiskFrom(ctx.flags),
83
90
  });
84
91
  if (result.install.dryRun)
85
92
  return { success: true, data: result };
@@ -120,6 +127,11 @@ function findingsCommand(name, description) {
120
127
  ],
121
128
  action: async (ctx) => {
122
129
  const findings = probeMods({ projectRoot: projectRoot(ctx) });
130
+ if (name === 'doctor') {
131
+ // ADR-509 §2.1: check the clone (and install cache) against the release signatures; writes the status the mod's trust gate reads.
132
+ const configDir = claudeConfigDir(process.env, homedir());
133
+ findings.push(...signatureFindings(configDir, marketplaceState(configDir).location, MOD_PLUGINS.map((p) => p.id.split('@')[0])));
134
+ }
123
135
  if (ctx.flags.json)
124
136
  output.printJson(findings);
125
137
  else
@@ -175,6 +187,7 @@ export const modsCommand = {
175
187
  { command: 'ruflo mods install --no-plugin-install', description: 'Only write settings; run no claude command' },
176
188
  { command: 'ruflo mods install --source local', description: 'Dogfood: load the mods live from this ruflo checkout (directory marketplace)' },
177
189
  { command: 'ruflo mods doctor', description: 'Marketplace fresh? Plugins loadable? Function hooks on? Refused by policy? Each problem prints a one-line fix' },
190
+ { command: 'ruflo mods install --accept-risk ruflo-console', description: 'Install past a high pre-install scan finding for that one plugin (recorded)' },
178
191
  { command: 'ruflo mods doctor --strict', description: 'Same, and exit 1 when anything is not fully working (for CI)' },
179
192
  { command: 'ruflo mods uninstall', description: 'Remove only what install added' },
180
193
  ],
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ADR-510 §2.3 — `ruflo plugins paths [--json] [--names a,b]`.
3
+ *
4
+ * Prints the folder each ruflo plugin resolves to (the same resolver headless
5
+ * workers use for `--plugin-dir`), so an Agent SDK caller can build
6
+ * `plugins: [{ type: "local", path }]`, and lists plugins that could not be
7
+ * found. Read-only.
8
+ */
9
+ import type { Command } from '../types.js';
10
+ /** Default list: every plugin `ruflo mods install` enables. */
11
+ export declare const DEFAULT_PATH_PLUGINS: readonly string[];
12
+ export declare const pathsCommand: Command;
13
+ //# sourceMappingURL=plugins-paths.d.ts.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ADR-510 §2.3 — `ruflo plugins paths [--json] [--names a,b]`.
3
+ *
4
+ * Prints the folder each ruflo plugin resolves to (the same resolver headless
5
+ * workers use for `--plugin-dir`), so an Agent SDK caller can build
6
+ * `plugins: [{ type: "local", path }]`, and lists plugins that could not be
7
+ * found. Read-only.
8
+ */
9
+ import { output } from '../output.js';
10
+ import { MOD_PLUGIN_IDS } from '../mods/install.js';
11
+ import { resolvePluginPaths } from '../mods/plugin-paths.js';
12
+ /** Default list: every plugin `ruflo mods install` enables. */
13
+ export const DEFAULT_PATH_PLUGINS = MOD_PLUGIN_IDS.map((id) => id.split('@')[0]);
14
+ const PLUGIN_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
15
+ export const pathsCommand = {
16
+ name: 'paths',
17
+ description: 'Print the local folder of each ruflo plugin (for claude -p --plugin-dir or Agent SDK plugins)',
18
+ options: [
19
+ { name: 'names', short: 'n', type: 'string', description: `Comma list of plugin names (default: ${DEFAULT_PATH_PLUGINS.join(',')})` },
20
+ { name: 'json', type: 'boolean', description: 'Print { resolved, missing } as JSON', default: false },
21
+ ],
22
+ examples: [
23
+ { command: 'ruflo plugins paths', description: 'Where ruflo-mods, ruflo-swarm and ruflo-console live on this machine' },
24
+ { command: 'ruflo plugins paths --names ruflo-mods --json', description: 'JSON for an Agent SDK plugins array' },
25
+ ],
26
+ action: async (ctx) => {
27
+ const raw = ctx.flags.names;
28
+ const names = raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : [...DEFAULT_PATH_PLUGINS];
29
+ const bad = names.filter((n) => !PLUGIN_NAME.test(n.split('@')[0]));
30
+ if (bad.length > 0) {
31
+ const message = `invalid plugin name: ${bad.map((b) => JSON.stringify(b)).join(', ')}`;
32
+ if (ctx.flags.json)
33
+ output.writeln(JSON.stringify({ resolved: [], missing: [], error: message }));
34
+ else
35
+ output.printError(message);
36
+ return { success: false, exitCode: 1, message };
37
+ }
38
+ const result = resolvePluginPaths(names, { projectRoot: ctx.cwd || process.cwd() });
39
+ if (ctx.flags.json) {
40
+ output.writeln(JSON.stringify(result, null, 2));
41
+ }
42
+ else {
43
+ for (const p of result.resolved)
44
+ output.writeln(`${p.name}\t${p.path}\t(${p.source})`);
45
+ for (const m of result.missing)
46
+ output.writeln(output.dim(`${m.name}\tmissing: ${m.reason}`));
47
+ if (result.resolved.length > 0) {
48
+ output.writeln(output.dim('Agent SDK: plugins: [{ type: "local", path }] — check the init message\'s plugin_errors'));
49
+ }
50
+ }
51
+ // Missing plugins are reported, not an error: a worker still runs without them.
52
+ return { success: true, exitCode: 0, data: result };
53
+ },
54
+ };
55
+ //# sourceMappingURL=plugins-paths.js.map
@@ -0,0 +1,9 @@
1
+ import type { Command } from '../types.js';
2
+ export declare const verifyCommand: Command;
3
+ /**
4
+ * ADR-509 §2.2 for `ruflo plugins install -n <local path>`: a high finding
5
+ * stops the install unless --accept-risk names the plugin (its directory name).
6
+ * Returns an error message when the install must stop.
7
+ */
8
+ export declare function scanLocalPluginBeforeInstall(dir: string, acceptRisk: readonly string[]): string | null;
9
+ //# sourceMappingURL=plugins-verify.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ADR-509 — `ruflo plugins verify [name]` and the scan `ruflo plugins install`
3
+ * runs on a local plugin before registering it.
4
+ */
5
+ import { homedir } from 'node:os';
6
+ import { basename, resolve } from 'node:path';
7
+ import { output } from '../output.js';
8
+ import { claudeConfigDir, marketplaceState } from '../mods/plugin-resolve.js';
9
+ import { formatScanFinding, gateScan, scanPluginDir } from '../plugins/marketplace-trust/scan.js';
10
+ import { describeVerification } from '../plugins/marketplace-trust/signature.js';
11
+ import { isUntrusted, verifyClone, verifyInstallCache, writeVerifyStatus } from '../plugins/marketplace-trust/verify-status.js';
12
+ export const verifyCommand = {
13
+ name: 'verify',
14
+ description: 'Check ruflo marketplace plugins against their release signatures (verified / modified / unsigned / bad-signature)',
15
+ options: [
16
+ { name: 'clone', description: 'A ruflo marketplace checkout to verify instead of the clone Claude Code loads from', type: 'string' },
17
+ { name: 'no-cache', description: 'Skip the install-cache copies', type: 'boolean', default: false },
18
+ { name: 'strict', description: 'Exit 1 on unsigned too (default: only modified / bad-signature fail)', type: 'boolean', default: false },
19
+ { name: 'json', description: 'Output as JSON', type: 'boolean', default: false },
20
+ ],
21
+ examples: [
22
+ { command: 'ruflo plugins verify', description: 'Every plugin in the ruflo marketplace clone, plus install-cache copies' },
23
+ { command: 'ruflo plugins verify ruflo-mods', description: 'One plugin' },
24
+ { command: 'ruflo plugins verify --clone . --strict', description: 'Release check: a checkout must verify completely' },
25
+ ],
26
+ action: async (ctx) => {
27
+ const names = ctx.args.filter((a) => /^[A-Za-z0-9._-]{1,100}$/.test(a));
28
+ const explicit = ctx.flags.clone ?? undefined;
29
+ const configDir = claudeConfigDir(process.env, homedir());
30
+ const clone = explicit ? resolve(explicit) : marketplaceState(configDir).location;
31
+ if (!clone) {
32
+ output.printError('No ruflo marketplace clone (run: claude plugin marketplace add ruvnet/ruflo, or pass --clone <dir>)');
33
+ return { success: false, exitCode: 1 };
34
+ }
35
+ let results;
36
+ try {
37
+ results = verifyClone(clone, names);
38
+ }
39
+ catch (error) {
40
+ output.printError(`Cannot read ${clone}/.claude-plugin/marketplace.json: ${error.message}`);
41
+ return { success: false, exitCode: 1 };
42
+ }
43
+ if (names.length && results.length === 0) {
44
+ output.printError(`${names.join(', ')}: not in the marketplace at ${clone}`);
45
+ return { success: false, exitCode: 1 };
46
+ }
47
+ const noCache = ctx.flags.noCache === true || ctx.flags['no-cache'] === true || ctx.flags.cache === false;
48
+ // The install cache is what Claude Code loads an installed plugin from, so the status the mod's
49
+ // trust gate reads always includes it (--no-cache only hides it here); an explicit --clone writes nothing.
50
+ const loadedCache = explicit ? [] : verifyInstallCache(configDir, 'ruflo', names);
51
+ const cache = noCache ? [] : loadedCache;
52
+ const statusPath = explicit ? undefined : writeVerifyStatus(configDir, clone, results, undefined, loadedCache);
53
+ const all = [...results, ...cache];
54
+ const strict = ctx.flags.strict === true;
55
+ const failed = all.filter((r) => isUntrusted(r.result) || (strict && r.result === 'unsigned'));
56
+ if (ctx.flags.json) {
57
+ output.printJson({ clone, statusPath, clonePlugins: results, installCache: cache });
58
+ }
59
+ else {
60
+ output.writeln(output.dim(`clone: ${clone}`));
61
+ for (const r of results)
62
+ printRow(r);
63
+ if (cache.length)
64
+ output.writeln(output.dim('install cache:'));
65
+ for (const r of cache)
66
+ printRow(r);
67
+ if (statusPath)
68
+ output.writeln(output.dim(`status written: ${statusPath}`));
69
+ }
70
+ return failed.length ? { success: false, exitCode: 1, data: all } : { success: true, data: all };
71
+ },
72
+ };
73
+ function printRow(r) {
74
+ const mark = r.result === 'verified' ? output.success('✓') : r.result === 'unsigned' ? output.warning('!') : output.error('✗');
75
+ output.writeln(`${mark} ${describeVerification(r)}${r.version ? output.dim(` (v${r.version})`) : ''}`);
76
+ }
77
+ /**
78
+ * ADR-509 §2.2 for `ruflo plugins install -n <local path>`: a high finding
79
+ * stops the install unless --accept-risk names the plugin (its directory name).
80
+ * Returns an error message when the install must stop.
81
+ */
82
+ export function scanLocalPluginBeforeInstall(dir, acceptRisk) {
83
+ const name = basename(resolve(dir));
84
+ const findings = scanPluginDir(name, resolve(dir));
85
+ for (const f of findings)
86
+ output.writeln(f.severity === 'high' ? `${output.error('✗')} scan ${formatScanFinding(f)}` : output.dim(` scan ${formatScanFinding(f)}`));
87
+ const gate = gateScan(findings, new Set(acceptRisk));
88
+ if (gate.accepted.length)
89
+ output.printWarning(`--accept-risk ${name}: installing past its high scan finding(s)`);
90
+ return gate.blocked.length ? `${name}: high scan finding(s); review them, then re-run with --accept-risk ${name}` : null;
91
+ }
92
+ //# sourceMappingURL=plugins-verify.js.map
@@ -10,6 +10,9 @@ import { createPluginDiscoveryService, searchPlugins, getPluginSearchSuggestions
10
10
  import { getPluginManager } from '../plugins/manager.js';
11
11
  import { DEFAULT_PLUGIN_PERMISSIONS, isStrictPluginMode } from '../plugins/trust-policy.js';
12
12
  import { getBulkRatings } from '../services/registry-api.js';
13
+ import { scanLocalPluginBeforeInstall, verifyCommand } from './plugins-verify.js';
14
+ import { acceptRiskFrom } from '../plugins/marketplace-trust/scan.js';
15
+ import { pathsCommand } from './plugins-paths.js';
13
16
  // List subcommand - Now uses IPFS-based registry
14
17
  const listCommand = {
15
18
  name: 'list',
@@ -218,6 +221,7 @@ const installCommand = {
218
221
  default: true,
219
222
  },
220
223
  { name: 'trust', type: 'boolean', description: 'Register hooks and commands even if the plugin is untrusted or declares elevated permissions', default: false },
224
+ { name: 'accept-risk', type: 'string', description: 'Install a local plugin (its directory name) even though the pre-install scan found a high-severity issue in its .mcp.json, hooks/hooks.json or hook modules (ADR-509)' },
221
225
  {
222
226
  name: 'allow-unverified',
223
227
  type: 'boolean',
@@ -270,6 +274,12 @@ const installCommand = {
270
274
  let plugin;
271
275
  const registryWarnings = [];
272
276
  if (isLocalPath) {
277
+ // ADR-509 §2.2: static scan before anything is registered; a high finding stops the install.
278
+ const blocked = scanLocalPluginBeforeInstall(name, acceptRiskFrom(ctx.flags));
279
+ if (blocked) {
280
+ spinner.fail(`Refusing to install ${blocked}`);
281
+ return { success: false, exitCode: 1 };
282
+ }
273
283
  // Install from local path
274
284
  spinner.setText(`Installing from ${name}...`);
275
285
  result = await manager.installFromLocal(name, { verify, trust });
@@ -896,7 +906,7 @@ const rateCommand = {
896
906
  export const pluginsCommand = {
897
907
  name: 'plugins',
898
908
  description: 'Plugin management with IPFS-based decentralized registry',
899
- subcommands: [listCommand, searchCommand, installCommand, uninstallCommand, upgradeCommand, toggleCommand, infoCommand, createCommand, rateCommand],
909
+ subcommands: [listCommand, searchCommand, installCommand, uninstallCommand, upgradeCommand, toggleCommand, infoCommand, createCommand, rateCommand, verifyCommand, pathsCommand],
900
910
  examples: [
901
911
  { command: 'claude-flow plugins list', description: 'List plugins from IPFS registry' },
902
912
  { command: 'claude-flow plugins search -q neural', description: 'Search for plugins' },
@@ -917,6 +927,8 @@ export const pluginsCommand = {
917
927
  `${output.highlight('upgrade')} - Upgrade an installed plugin`,
918
928
  `${output.highlight('toggle')} - Enable or disable a plugin`,
919
929
  `${output.highlight('info')} - Show detailed plugin information`,
930
+ `${output.highlight('verify')} - Check ruflo marketplace plugins against their release signatures`,
931
+ `${output.highlight('paths')} - Local folders of ruflo plugins (claude -p --plugin-dir, Agent SDK)`,
920
932
  `${output.highlight('create')} - Scaffold a new plugin project`,
921
933
  ]);
922
934
  output.writeln();
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `ruflo cost report` (ADR-508 §2.3): which subagent types and tools spent the
3
+ * money, from the lines the ruflo-mods cost ledger writes to
4
+ * `.claude-flow/cost/attribution.jsonl` (one per session; see
5
+ * plugins/ruflo-mods/hooks/cost/attribution.ts `attributionLine`).
6
+ *
7
+ * The file is project-writable, so every line is validated to a fixed shape;
8
+ * a bad line is skipped and counted, never trusted.
9
+ */
10
+ export declare const ATTRIBUTION_PATH = ".claude-flow/cost/attribution.jsonl";
11
+ export interface AttributionLine {
12
+ endedAt: number;
13
+ totalUsd: number;
14
+ byAgent: Record<string, number>;
15
+ byTool: Record<string, number>;
16
+ refusals: number;
17
+ }
18
+ /** One line validated, or undefined. */
19
+ export declare function parseAttributionLine(text: string): AttributionLine | undefined;
20
+ export interface AttributionReport {
21
+ sessions: number;
22
+ skipped: number;
23
+ totalUsd: number;
24
+ refusals: number;
25
+ byAgent: [string, number][];
26
+ byTool: [string, number][];
27
+ }
28
+ /** The report over the file's lines, optionally only sessions that ended at or after `since` (ms). */
29
+ export declare function summarize(text: string, since?: number): AttributionReport;
30
+ /** Reads the project's attribution file; an absent file is an empty report. */
31
+ export declare function readAttribution(cwd: string, since?: number): {
32
+ report?: AttributionReport;
33
+ error?: string;
34
+ };
35
+ //# sourceMappingURL=attribution.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * `ruflo cost report` (ADR-508 §2.3): which subagent types and tools spent the
3
+ * money, from the lines the ruflo-mods cost ledger writes to
4
+ * `.claude-flow/cost/attribution.jsonl` (one per session; see
5
+ * plugins/ruflo-mods/hooks/cost/attribution.ts `attributionLine`).
6
+ *
7
+ * The file is project-writable, so every line is validated to a fixed shape;
8
+ * a bad line is skipped and counted, never trusted.
9
+ */
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ export const ATTRIBUTION_PATH = '.claude-flow/cost/attribution.jsonl';
13
+ const MAX_FILE_BYTES = 4 * 1024 * 1024;
14
+ const MAX_KEYS = 64;
15
+ const NAME = /^[A-Za-z0-9_.:@/()-]{1,80}$/;
16
+ const MAX_SESSION_USD = 1_000_000;
17
+ function amounts(value) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value))
19
+ return undefined;
20
+ const entries = Object.entries(value);
21
+ if (entries.length > MAX_KEYS + 1)
22
+ return undefined;
23
+ const out = {};
24
+ for (const [k, v] of entries) {
25
+ if (!NAME.test(k) || typeof v !== 'number' || !Number.isFinite(v) || v < 0 || v > MAX_SESSION_USD)
26
+ return undefined;
27
+ out[k] = v;
28
+ }
29
+ return out;
30
+ }
31
+ /** One line validated, or undefined. */
32
+ export function parseAttributionLine(text) {
33
+ let o;
34
+ try {
35
+ o = JSON.parse(text);
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ if (!o || typeof o !== 'object' || o.v !== 1)
41
+ return undefined;
42
+ const { endedAt, totalUsd, refusals } = o;
43
+ if (typeof endedAt !== 'number' || !Number.isFinite(endedAt))
44
+ return undefined;
45
+ if (typeof totalUsd !== 'number' || !Number.isFinite(totalUsd) || totalUsd < 0 || totalUsd > MAX_SESSION_USD)
46
+ return undefined;
47
+ if (typeof refusals !== 'number' || !Number.isInteger(refusals) || refusals < 0)
48
+ return undefined;
49
+ const byAgent = amounts(o.byAgent);
50
+ const byTool = amounts(o.byTool);
51
+ if (!byAgent || !byTool)
52
+ return undefined;
53
+ return { endedAt, totalUsd, byAgent, byTool, refusals };
54
+ }
55
+ const add = (map, record) => {
56
+ for (const [k, v] of Object.entries(record))
57
+ map.set(k, (map.get(k) ?? 0) + v);
58
+ };
59
+ const ranked = (map) => [...map].sort((a, b) => b[1] - a[1]);
60
+ /** The report over the file's lines, optionally only sessions that ended at or after `since` (ms). */
61
+ export function summarize(text, since = 0) {
62
+ const byAgent = new Map();
63
+ const byTool = new Map();
64
+ let sessions = 0;
65
+ let skipped = 0;
66
+ let totalUsd = 0;
67
+ let refusals = 0;
68
+ for (const raw of text.split('\n')) {
69
+ if (!raw.trim())
70
+ continue;
71
+ const line = parseAttributionLine(raw);
72
+ if (!line) {
73
+ skipped++;
74
+ continue;
75
+ }
76
+ if (line.endedAt < since)
77
+ continue;
78
+ sessions++;
79
+ totalUsd += line.totalUsd;
80
+ refusals += line.refusals;
81
+ add(byAgent, line.byAgent);
82
+ add(byTool, line.byTool);
83
+ }
84
+ return { sessions, skipped, totalUsd, refusals, byAgent: ranked(byAgent), byTool: ranked(byTool) };
85
+ }
86
+ /** Reads the project's attribution file; an absent file is an empty report. */
87
+ export function readAttribution(cwd, since = 0) {
88
+ const file = path.join(cwd, ATTRIBUTION_PATH);
89
+ try {
90
+ const stat = fs.statSync(file);
91
+ if (stat.size > MAX_FILE_BYTES)
92
+ return { error: `${ATTRIBUTION_PATH} is over 4 MiB` };
93
+ return { report: summarize(fs.readFileSync(file, 'utf8'), since) };
94
+ }
95
+ catch (error) {
96
+ if (error?.code === 'ENOENT')
97
+ return { report: summarize('', since) };
98
+ return { error: String(error?.message ?? error) };
99
+ }
100
+ }
101
+ //# sourceMappingURL=attribution.js.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The shared budget file (ADR-508 §2.1): `.claude-flow/cost/budget.json`,
3
+ * version 1. `ruflo cost budget set` writes it; the ruflo-mods cost ladder and
4
+ * `ruflo cost budget export` read it.
5
+ *
6
+ * Second copy of plugins/ruflo-mods/hooks/cost/file.ts (the mod may not import
7
+ * outside its plugin folder). __tests__/cost-budget-file.test.ts runs both over
8
+ * the same hostile corpus and requires identical answers.
9
+ */
10
+ export declare const BUDGET_PATH = ".claude-flow/cost/budget.json";
11
+ export declare const MAX_WINDOWS = 8;
12
+ export declare const MAX_USD = 1000000;
13
+ /** The largest budget file read (a budget is a few hundred bytes). */
14
+ export declare const MAX_FILE_BYTES: number;
15
+ /** LiteLLM's `budget_duration` grammar (`30s`, `30m`, `30h`, `30d`), with a bounded count. */
16
+ export declare const PERIOD: RegExp;
17
+ export interface BudgetWindow {
18
+ readonly usd: number;
19
+ readonly period: string;
20
+ }
21
+ export interface BudgetFile {
22
+ readonly version: 1;
23
+ readonly windows: readonly BudgetWindow[];
24
+ readonly hardStop: boolean;
25
+ readonly fallbackModel?: string;
26
+ }
27
+ export declare const periodSeconds: (period: string) => number;
28
+ /** A budget value validated, or undefined when it breaks any rule (see the mod copy for the list). */
29
+ export declare function validateBudget(value: unknown): BudgetFile | undefined;
30
+ /** The smallest-`usd` window (ties: the shorter period): the one a single session nears first. */
31
+ export declare function ladderWindow(budget: BudgetFile): BudgetWindow;
32
+ export type ReadBudget = {
33
+ kind: 'absent';
34
+ } | {
35
+ kind: 'ok';
36
+ budget: BudgetFile;
37
+ } | {
38
+ kind: 'invalid';
39
+ reason: string;
40
+ };
41
+ /** Reads and validates the project's budget file. Never throws. */
42
+ export declare function readBudget(cwd: string): ReadBudget;
43
+ export interface BudgetFlags {
44
+ daily?: unknown;
45
+ weekly?: unknown;
46
+ monthly?: unknown;
47
+ hardStop?: unknown;
48
+ fallbackModel?: unknown;
49
+ }
50
+ /** A budget from `set` flags (`--daily` 24h, `--weekly` 7d, `--monthly` 30d), or the reason it is not one. */
51
+ export declare function budgetFromFlags(flags: BudgetFlags): {
52
+ budget?: BudgetFile;
53
+ error?: string;
54
+ };
55
+ /** Writes the budget file atomically (temp file, rename). Holds no credential. */
56
+ export declare function writeBudget(cwd: string, budget: BudgetFile): string;
57
+ //# sourceMappingURL=budget-file.d.ts.map