@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.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/catalog-manifest.json +3 -3
- package/dist/src/commands/cost.d.ts +18 -0
- package/dist/src/commands/cost.js +195 -0
- package/dist/src/commands/doctor-auto-approve.d.ts +9 -0
- package/dist/src/commands/doctor-auto-approve.js +32 -0
- package/dist/src/commands/doctor.js +28 -2
- package/dist/src/commands/hive-mind-worker.d.ts +2 -0
- package/dist/src/commands/hive-mind-worker.js +9 -3
- package/dist/src/commands/hive-mind.js +15 -1
- package/dist/src/commands/hooks.js +45 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init.js +76 -2
- package/dist/src/commands/mods.js +13 -0
- package/dist/src/commands/plugins-paths.d.ts +13 -0
- package/dist/src/commands/plugins-paths.js +55 -0
- package/dist/src/commands/plugins-verify.d.ts +9 -0
- package/dist/src/commands/plugins-verify.js +92 -0
- package/dist/src/commands/plugins.js +13 -1
- package/dist/src/cost/attribution.d.ts +35 -0
- package/dist/src/cost/attribution.js +101 -0
- package/dist/src/cost/budget-file.d.ts +57 -0
- package/dist/src/cost/budget-file.js +113 -0
- package/dist/src/cost/litellm.d.ts +69 -0
- package/dist/src/cost/litellm.js +135 -0
- package/dist/src/init/executor.d.ts +7 -2
- package/dist/src/init/executor.js +64 -10
- package/dist/src/init/helper-commands.d.ts +16 -0
- package/dist/src/init/helper-commands.js +73 -0
- package/dist/src/init/risky-settings-gate.d.ts +39 -0
- package/dist/src/init/risky-settings-gate.js +130 -0
- package/dist/src/init/settings-risk-scanner.d.ts +35 -0
- package/dist/src/init/settings-risk-scanner.js +85 -0
- package/dist/src/init/types.d.ts +12 -0
- package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
- package/dist/src/mcp-tools/capability-brain.js +1 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts +1 -0
- package/dist/src/mcp-tools/hooks-tools.js +38 -0
- package/dist/src/memory/verified-guards.d.ts +70 -0
- package/dist/src/memory/verified-guards.js +101 -0
- package/dist/src/memory/verified-outcomes.d.ts +228 -0
- package/dist/src/memory/verified-outcomes.js +372 -0
- package/dist/src/memory/verified-reverify.d.ts +63 -0
- package/dist/src/memory/verified-reverify.js +174 -0
- package/dist/src/missions/schemas.d.ts +36 -36
- package/dist/src/mods/apply.d.ts +2 -0
- package/dist/src/mods/apply.js +68 -8
- package/dist/src/mods/command-registry/schema.d.ts +222 -222
- package/dist/src/mods/compact-snapshots.d.ts +21 -0
- package/dist/src/mods/compact-snapshots.js +45 -0
- package/dist/src/mods/headless-check.d.ts +58 -0
- package/dist/src/mods/headless-check.js +137 -0
- package/dist/src/mods/headless-plugins.d.ts +44 -0
- package/dist/src/mods/headless-plugins.js +92 -0
- package/dist/src/mods/install-trust.d.ts +28 -0
- package/dist/src/mods/install-trust.js +127 -0
- package/dist/src/mods/install.d.ts +19 -0
- package/dist/src/mods/install.js +38 -1
- package/dist/src/mods/plugin-paths.d.ts +60 -0
- package/dist/src/mods/plugin-paths.js +131 -0
- package/dist/src/mods/plugin-repair.d.ts +8 -0
- package/dist/src/mods/plugin-repair.js +7 -0
- package/dist/src/plugins/marketplace-trust/scan.d.ts +23 -0
- package/dist/src/plugins/marketplace-trust/scan.js +255 -0
- package/dist/src/plugins/marketplace-trust/signature.d.ts +53 -0
- package/dist/src/plugins/marketplace-trust/signature.js +140 -0
- package/dist/src/plugins/marketplace-trust/verify-status.d.ts +42 -0
- package/dist/src/plugins/marketplace-trust/verify-status.js +110 -0
- package/dist/src/services/headless-worker-executor.d.ts +2 -0
- package/dist/src/services/headless-worker-executor.js +13 -3
- package/package.json +2 -1
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +8 -1
- package/plugins/ruflo-metaharness/CHANGELOG.md +6 -0
- package/plugins/ruflo-metaharness/README.md +14 -1
- package/plugins/ruflo-metaharness/agents/metaharness-architect.md +2 -0
- package/plugins/ruflo-metaharness/hooks/hooks.json +16 -2
- package/plugins/ruflo-metaharness/scripts/_redblue.mjs +12 -2
- package/plugins/ruflo-metaharness/scripts/_secret-env.cjs +50 -0
- package/plugins/ruflo-metaharness/scripts/_secret.mjs +35 -0
- package/plugins/ruflo-metaharness/scripts/smoke.sh +7 -7
- package/plugins/ruflo-metaharness/skills/harness-bench/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +1 -0
- package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +2 -1
- 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
|