@claude-flow/cli 3.64.0 → 3.65.1

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 (93) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/catalog-manifest.json +2 -2
  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 +21 -3
  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 +8 -2
  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.js +2 -1
  38. package/dist/src/mcp-tools/http-fetch-pinned.d.ts +44 -0
  39. package/dist/src/mcp-tools/http-fetch-pinned.js +148 -0
  40. package/dist/src/mcp-tools/http-fetch-tools.d.ts +11 -2
  41. package/dist/src/mcp-tools/http-fetch-tools.js +19 -70
  42. package/dist/src/memory/verified-guards.d.ts +70 -0
  43. package/dist/src/memory/verified-guards.js +101 -0
  44. package/dist/src/memory/verified-outcomes.d.ts +68 -3
  45. package/dist/src/memory/verified-outcomes.js +88 -19
  46. package/dist/src/memory/verified-reverify.d.ts +63 -0
  47. package/dist/src/memory/verified-reverify.js +174 -0
  48. package/dist/src/missions/schemas.d.ts +4 -4
  49. package/dist/src/mods/apply.d.ts +2 -0
  50. package/dist/src/mods/apply.js +68 -8
  51. package/dist/src/mods/command-registry/schema.d.ts +36 -36
  52. package/dist/src/mods/compact-snapshots.d.ts +21 -0
  53. package/dist/src/mods/compact-snapshots.js +45 -0
  54. package/dist/src/mods/headless-check.d.ts +58 -0
  55. package/dist/src/mods/headless-check.js +137 -0
  56. package/dist/src/mods/headless-plugins.d.ts +44 -0
  57. package/dist/src/mods/headless-plugins.js +92 -0
  58. package/dist/src/mods/install-trust.d.ts +28 -0
  59. package/dist/src/mods/install-trust.js +127 -0
  60. package/dist/src/mods/install.d.ts +19 -0
  61. package/dist/src/mods/install.js +38 -1
  62. package/dist/src/mods/plugin-paths.d.ts +60 -0
  63. package/dist/src/mods/plugin-paths.js +131 -0
  64. package/dist/src/mods/plugin-repair.d.ts +8 -0
  65. package/dist/src/mods/plugin-repair.js +7 -0
  66. package/dist/src/plugins/marketplace-trust/scan.d.ts +23 -0
  67. package/dist/src/plugins/marketplace-trust/scan.js +255 -0
  68. package/dist/src/plugins/marketplace-trust/signature.d.ts +53 -0
  69. package/dist/src/plugins/marketplace-trust/signature.js +140 -0
  70. package/dist/src/plugins/marketplace-trust/verify-status.d.ts +42 -0
  71. package/dist/src/plugins/marketplace-trust/verify-status.js +110 -0
  72. package/dist/src/services/headless-worker-executor.d.ts +2 -0
  73. package/dist/src/services/headless-worker-executor.js +13 -3
  74. package/package.json +2 -1
  75. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +8 -1
  76. package/plugins/ruflo-metaharness/CHANGELOG.md +6 -0
  77. package/plugins/ruflo-metaharness/README.md +14 -1
  78. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +2 -0
  79. package/plugins/ruflo-metaharness/hooks/hooks.json +16 -2
  80. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +12 -2
  81. package/plugins/ruflo-metaharness/scripts/_secret-env.cjs +50 -0
  82. package/plugins/ruflo-metaharness/scripts/_secret.mjs +35 -0
  83. package/plugins/ruflo-metaharness/scripts/smoke.sh +7 -7
  84. package/plugins/ruflo-metaharness/skills/harness-bench/SKILL.md +2 -1
  85. package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +1 -1
  86. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +2 -1
  87. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
  88. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +2 -1
  89. package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +2 -1
  90. package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +1 -0
  91. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  92. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +2 -1
  93. package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +1 -1
@@ -0,0 +1,113 @@
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
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ export const BUDGET_PATH = '.claude-flow/cost/budget.json';
13
+ export const MAX_WINDOWS = 8;
14
+ export const MAX_USD = 1_000_000;
15
+ /** The largest budget file read (a budget is a few hundred bytes). */
16
+ export const MAX_FILE_BYTES = 64 * 1024;
17
+ /** LiteLLM's `budget_duration` grammar (`30s`, `30m`, `30h`, `30d`), with a bounded count. */
18
+ export const PERIOD = /^[1-9][0-9]{0,3}[smhd]$/;
19
+ const MODEL = /^[A-Za-z0-9._:/@-]{1,128}$/;
20
+ const SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
21
+ export const periodSeconds = (period) => Number(period.slice(0, -1)) * SECONDS[period.slice(-1)];
22
+ /** A budget value validated, or undefined when it breaks any rule (see the mod copy for the list). */
23
+ export function validateBudget(value) {
24
+ if (!value || typeof value !== 'object' || Array.isArray(value))
25
+ return undefined;
26
+ const o = value;
27
+ if (o.version !== 1)
28
+ return undefined;
29
+ if (!Array.isArray(o.windows) || o.windows.length === 0 || o.windows.length > MAX_WINDOWS)
30
+ return undefined;
31
+ const windows = [];
32
+ const seen = new Set();
33
+ for (const w of o.windows) {
34
+ if (!w || typeof w !== 'object' || Array.isArray(w))
35
+ return undefined;
36
+ const { usd, period } = w;
37
+ if (typeof usd !== 'number' || !Number.isFinite(usd) || usd <= 0 || usd > MAX_USD)
38
+ return undefined;
39
+ if (typeof period !== 'string' || !PERIOD.test(period) || seen.has(period))
40
+ return undefined;
41
+ seen.add(period);
42
+ windows.push({ usd, period });
43
+ }
44
+ if (o.hardStop !== undefined && typeof o.hardStop !== 'boolean')
45
+ return undefined;
46
+ if (o.fallbackModel !== undefined && (typeof o.fallbackModel !== 'string' || !MODEL.test(o.fallbackModel)))
47
+ return undefined;
48
+ return {
49
+ version: 1,
50
+ windows,
51
+ hardStop: o.hardStop === true,
52
+ ...(typeof o.fallbackModel === 'string' ? { fallbackModel: o.fallbackModel } : {}),
53
+ };
54
+ }
55
+ /** The smallest-`usd` window (ties: the shorter period): the one a single session nears first. */
56
+ export function ladderWindow(budget) {
57
+ return [...budget.windows].sort((a, b) => a.usd - b.usd || periodSeconds(a.period) - periodSeconds(b.period))[0];
58
+ }
59
+ /** Reads and validates the project's budget file. Never throws. */
60
+ export function readBudget(cwd) {
61
+ const file = path.join(cwd, BUDGET_PATH);
62
+ let text;
63
+ try {
64
+ const stat = fs.statSync(file);
65
+ if (!stat.isFile())
66
+ return { kind: 'invalid', reason: `${BUDGET_PATH} is not a file` };
67
+ if (stat.size > MAX_FILE_BYTES)
68
+ return { kind: 'invalid', reason: `${BUDGET_PATH} is over ${MAX_FILE_BYTES} bytes` };
69
+ text = fs.readFileSync(file, 'utf8');
70
+ }
71
+ catch (error) {
72
+ return error?.code === 'ENOENT' ? { kind: 'absent' } : { kind: 'invalid', reason: String(error?.message ?? error) };
73
+ }
74
+ let value;
75
+ try {
76
+ value = JSON.parse(text);
77
+ }
78
+ catch {
79
+ return { kind: 'invalid', reason: `${BUDGET_PATH} is not JSON` };
80
+ }
81
+ const budget = validateBudget(value);
82
+ return budget ? { kind: 'ok', budget } : { kind: 'invalid', reason: `${BUDGET_PATH} fails validation (version 1, 1-${MAX_WINDOWS} windows, usd in (0, ${MAX_USD}], period like 24h/7d/30d)` };
83
+ }
84
+ /** A budget from `set` flags (`--daily` 24h, `--weekly` 7d, `--monthly` 30d), or the reason it is not one. */
85
+ export function budgetFromFlags(flags) {
86
+ const windows = [];
87
+ for (const [flag, period] of [['daily', '24h'], ['weekly', '7d'], ['monthly', '30d']]) {
88
+ const raw = flags[flag];
89
+ if (raw === undefined || raw === false)
90
+ continue;
91
+ const usd = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN;
92
+ if (!Number.isFinite(usd) || usd <= 0 || usd > MAX_USD)
93
+ return { error: `--${flag} must be a USD amount in (0, ${MAX_USD}]` };
94
+ windows.push({ usd, period });
95
+ }
96
+ if (windows.length === 0)
97
+ return { error: 'give at least one of --daily, --weekly, --monthly' };
98
+ const value = { version: 1, windows, hardStop: flags.hardStop === true || flags.hardStop === 'true' };
99
+ if (flags.fallbackModel !== undefined && flags.fallbackModel !== false)
100
+ value.fallbackModel = String(flags.fallbackModel);
101
+ const budget = validateBudget(value);
102
+ return budget ? { budget } : { error: '--fallback-model must be a plain model name (letters, digits, . _ : / @ -; at most 128)' };
103
+ }
104
+ /** Writes the budget file atomically (temp file, rename). Holds no credential. */
105
+ export function writeBudget(cwd, budget) {
106
+ const file = path.join(cwd, BUDGET_PATH);
107
+ fs.mkdirSync(path.dirname(file), { recursive: true });
108
+ const tmp = `${file}.tmp-${process.pid}`;
109
+ fs.writeFileSync(tmp, `${JSON.stringify(budget, null, 2)}\n`, 'utf8');
110
+ fs.renameSync(tmp, file);
111
+ return file;
112
+ }
113
+ //# sourceMappingURL=budget-file.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Budget export to a gateway (ADR-508 §2.2).
3
+ *
4
+ * LiteLLM virtual-key budget fields, as docs.litellm.ai documents them
5
+ * (checked 2026-10-29: /docs/proxy/users and /docs/proxy/budget_fallbacks):
6
+ * budget_limits [{ budget_duration: "24h", max_budget: 10 }, ...] stacked windows
7
+ * model_max_budget { "<model>": { budget_limit: 0.01, time_period: "1d" } }
8
+ * budget_fallbacks { "<model>": ["<fallback>", ...] } LiteLLM >= v1.92.0
9
+ * Durations use LiteLLM's grammar: "30s", "30m", "30h", "30d".
10
+ *
11
+ * meta-llm documents no budget API, so it is refused with a pointer to
12
+ * ADR-508 rather than guessed at.
13
+ */
14
+ import { type BudgetFile } from './budget-file.js';
15
+ export declare const ADR_POINTER = "v3/docs/adr/ADR-508-cost-ladder-gateway-budgets.md";
16
+ export declare const GATEWAYS: readonly ["litellm", "meta-llm"];
17
+ export interface LiteLLMKeyBody {
18
+ budget_limits: {
19
+ budget_duration: string;
20
+ max_budget: number;
21
+ }[];
22
+ model_max_budget?: Record<string, {
23
+ budget_limit: number;
24
+ time_period: string;
25
+ }>;
26
+ budget_fallbacks?: Record<string, string[]>;
27
+ }
28
+ /** Why a gateway cannot be exported to, or undefined when it can. */
29
+ export declare function gatewayRefusal(gateway: unknown): string | undefined;
30
+ /**
31
+ * The LiteLLM `/key/generate` budget fields for a budget. Every window becomes
32
+ * one `budget_limits` entry. `model_max_budget` (the ladder's window, on the
33
+ * named model) and `budget_fallbacks` (model → [fallbackModel]) are added only
34
+ * when `model` is given: the file names no primary model, and ruflo never
35
+ * invents a gateway model name.
36
+ */
37
+ export declare function toLiteLLMKeyBody(budget: BudgetFile, model?: string): LiteLLMKeyBody;
38
+ /** The body as YAML (LiteLLM key settings) or as the `/key/generate` JSON body. */
39
+ export declare function formatBody(body: LiteLLMKeyBody, format: 'yaml' | 'json'): string;
40
+ /**
41
+ * The `/key/generate` URL for a proxy URL, or the reason it is refused:
42
+ * HTTPS only, except a loopback host (`localhost`, `127.0.0.1`, `[::1]`);
43
+ * no credentials in the URL; no query or fragment; no path starting with `//`.
44
+ * The result always keeps the given scheme and host.
45
+ */
46
+ export declare function keyGenerateUrl(proxy: unknown): {
47
+ url?: URL;
48
+ error?: string;
49
+ };
50
+ /** The master key from the environment variable `--key-env` names (never from argv), or why there is none. */
51
+ export declare function keyFromEnv(name: unknown, env?: NodeJS.ProcessEnv): {
52
+ key?: string;
53
+ error?: string;
54
+ };
55
+ /** A key shown masked: its first 4 characters and its length. */
56
+ export declare const maskKey: (key: string) => string;
57
+ export interface ApplyResult {
58
+ ok: boolean;
59
+ status: number;
60
+ generatedKey?: string;
61
+ error?: string;
62
+ }
63
+ /**
64
+ * POSTs the budget fields to the proxy's `/key/generate`. Only the budget
65
+ * fields are sent; redirects are refused (one could leave HTTPS or loopback);
66
+ * 15 s timeout.
67
+ */
68
+ export declare function applyToLiteLLM(url: URL, masterKey: string, body: LiteLLMKeyBody, fetchImpl?: typeof fetch): Promise<ApplyResult>;
69
+ //# sourceMappingURL=litellm.d.ts.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Budget export to a gateway (ADR-508 §2.2).
3
+ *
4
+ * LiteLLM virtual-key budget fields, as docs.litellm.ai documents them
5
+ * (checked 2026-10-29: /docs/proxy/users and /docs/proxy/budget_fallbacks):
6
+ * budget_limits [{ budget_duration: "24h", max_budget: 10 }, ...] stacked windows
7
+ * model_max_budget { "<model>": { budget_limit: 0.01, time_period: "1d" } }
8
+ * budget_fallbacks { "<model>": ["<fallback>", ...] } LiteLLM >= v1.92.0
9
+ * Durations use LiteLLM's grammar: "30s", "30m", "30h", "30d".
10
+ *
11
+ * meta-llm documents no budget API, so it is refused with a pointer to
12
+ * ADR-508 rather than guessed at.
13
+ */
14
+ import { stringify as stringifyYaml } from 'yaml';
15
+ import { ladderWindow } from './budget-file.js';
16
+ export const ADR_POINTER = 'v3/docs/adr/ADR-508-cost-ladder-gateway-budgets.md';
17
+ export const GATEWAYS = ['litellm', 'meta-llm'];
18
+ const MODEL = /^[A-Za-z0-9._:/@-]{1,128}$/;
19
+ const ENV_NAME = /^[A-Z_][A-Z0-9_]{0,127}$/;
20
+ /** Why a gateway cannot be exported to, or undefined when it can. */
21
+ export function gatewayRefusal(gateway) {
22
+ if (gateway === 'litellm')
23
+ return undefined;
24
+ if (gateway === 'meta-llm') {
25
+ return `--gateway meta-llm: meta-llm documents no budget API yet, so ruflo does not guess an endpoint. Its allowance tiers stay as they are; see ${ADR_POINTER} §2.2.`;
26
+ }
27
+ return `--gateway must be one of: litellm (meta-llm is reserved until it documents a budget API; see ${ADR_POINTER})`;
28
+ }
29
+ /**
30
+ * The LiteLLM `/key/generate` budget fields for a budget. Every window becomes
31
+ * one `budget_limits` entry. `model_max_budget` (the ladder's window, on the
32
+ * named model) and `budget_fallbacks` (model → [fallbackModel]) are added only
33
+ * when `model` is given: the file names no primary model, and ruflo never
34
+ * invents a gateway model name.
35
+ */
36
+ export function toLiteLLMKeyBody(budget, model) {
37
+ const body = {
38
+ budget_limits: budget.windows.map((w) => ({ budget_duration: w.period, max_budget: w.usd })),
39
+ };
40
+ if (model !== undefined) {
41
+ if (!MODEL.test(model))
42
+ throw new Error('--model must be a plain model name (letters, digits, . _ : / @ -; at most 128)');
43
+ const w = ladderWindow(budget);
44
+ body.model_max_budget = { [model]: { budget_limit: w.usd, time_period: w.period } };
45
+ if (budget.fallbackModel)
46
+ body.budget_fallbacks = { [model]: [budget.fallbackModel] };
47
+ }
48
+ return body;
49
+ }
50
+ /** The body as YAML (LiteLLM key settings) or as the `/key/generate` JSON body. */
51
+ export function formatBody(body, format) {
52
+ return format === 'json' ? `${JSON.stringify(body, null, 2)}\n` : stringifyYaml(body);
53
+ }
54
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
55
+ /**
56
+ * The `/key/generate` URL for a proxy URL, or the reason it is refused:
57
+ * HTTPS only, except a loopback host (`localhost`, `127.0.0.1`, `[::1]`);
58
+ * no credentials in the URL; no query or fragment; no path starting with `//`.
59
+ * The result always keeps the given scheme and host.
60
+ */
61
+ export function keyGenerateUrl(proxy) {
62
+ if (typeof proxy !== 'string' || proxy.trim() === '')
63
+ return { error: '--apply needs --url <proxy base URL>' };
64
+ let u;
65
+ try {
66
+ u = new URL(proxy);
67
+ }
68
+ catch {
69
+ return { error: `--url is not a URL: ${proxy.slice(0, 200)}` };
70
+ }
71
+ if (u.username || u.password)
72
+ return { error: '--url must not carry credentials; the key comes from --key-env' };
73
+ if (u.search || u.hash)
74
+ return { error: '--url must be the proxy base URL, without a query or fragment' };
75
+ const loopback = LOOPBACK.has(u.hostname);
76
+ if (u.protocol !== 'https:' && !(u.protocol === 'http:' && loopback)) {
77
+ return { error: `--apply refuses ${u.protocol}//${u.hostname}: use https, or plain http only to a loopback host (localhost, 127.0.0.1, [::1])` };
78
+ }
79
+ // A path starting with "//" ("/\" normalises to it) would read as a protocol-relative
80
+ // URL if resolved as a string, and move the request, bearer key included, to another host.
81
+ if (u.pathname.startsWith('//'))
82
+ return { error: '--url path must not start with "//" or "/\\"' };
83
+ // The path is set on a copy, never resolved from a string, so it cannot change the
84
+ // scheme or host; the check below holds that anyway.
85
+ const out = new URL(u.href);
86
+ out.pathname = `${u.pathname.endsWith('/') ? u.pathname : `${u.pathname}/`}key/generate`;
87
+ if (out.protocol !== u.protocol || out.host !== u.host)
88
+ return { error: `--url would send the key to another host (${out.host}); refused` };
89
+ return { url: out };
90
+ }
91
+ /** The master key from the environment variable `--key-env` names (never from argv), or why there is none. */
92
+ export function keyFromEnv(name, env = process.env) {
93
+ if (typeof name !== 'string' || !ENV_NAME.test(name))
94
+ return { error: '--apply needs --key-env <ENV_VAR_NAME> (upper-case letters, digits, _); the key itself is never taken from the command line' };
95
+ const key = env[name];
96
+ if (!key)
97
+ return { error: `environment variable ${name} is not set or empty` };
98
+ return { key };
99
+ }
100
+ /** A key shown masked: its first 4 characters and its length. */
101
+ export const maskKey = (key) => (key.length <= 8 ? '********' : `${key.slice(0, 4)}…(${key.length} chars)`);
102
+ /**
103
+ * POSTs the budget fields to the proxy's `/key/generate`. Only the budget
104
+ * fields are sent; redirects are refused (one could leave HTTPS or loopback);
105
+ * 15 s timeout.
106
+ */
107
+ export async function applyToLiteLLM(url, masterKey, body, fetchImpl = fetch) {
108
+ let res;
109
+ try {
110
+ res = await fetchImpl(url, {
111
+ method: 'POST',
112
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${masterKey}` },
113
+ body: JSON.stringify(body),
114
+ redirect: 'error',
115
+ signal: AbortSignal.timeout(15_000),
116
+ });
117
+ }
118
+ catch (error) {
119
+ return { ok: false, status: 0, error: String(error?.message ?? error).slice(0, 300) };
120
+ }
121
+ const text = (await res.text().catch(() => '')).slice(0, 64 * 1024);
122
+ if (!res.ok)
123
+ return { ok: false, status: res.status, error: text.replace(/\s+/g, ' ').slice(0, 300) };
124
+ let generatedKey;
125
+ try {
126
+ const parsed = JSON.parse(text);
127
+ if (typeof parsed.key === 'string')
128
+ generatedKey = parsed.key;
129
+ }
130
+ catch {
131
+ // a 2xx without a JSON body still applied
132
+ }
133
+ return { ok: true, status: res.status, generatedKey };
134
+ }
135
+ //# sourceMappingURL=litellm.js.map
@@ -3,6 +3,7 @@
3
3
  * Main execution logic for V3 initialization
4
4
  */
5
5
  import type { InitOptions, InitResult } from './types.js';
6
+ import { type RiskySettingsMode } from './settings-risk-scanner.js';
6
7
  /**
7
8
  * Execute initialization
8
9
  */
@@ -29,6 +30,10 @@ export interface UpgradeResult {
29
30
  * the upgrade — surfaced so a user can review before trusting it.
30
31
  */
31
32
  warnings?: string[];
33
+ /** ADR-509 §2.3 — auto-approve keys found in the existing settings.json. */
34
+ riskySettings?: string[];
35
+ /** ADR-509 §2.3 — the upgrade stopped before writing anything because of riskySettings. */
36
+ stoppedOnRiskySettings?: boolean;
32
37
  }
33
38
  /**
34
39
  * Merge new settings into existing settings.json
@@ -45,13 +50,13 @@ export declare function mergeSettingsForUpgrade(existing: Record<string, unknown
45
50
  * @param targetDir - Target directory
46
51
  * @param upgradeSettings - If true, merge new settings into existing settings.json
47
52
  */
48
- export declare function executeUpgrade(targetDir: string, upgradeSettings?: boolean): Promise<UpgradeResult>;
53
+ export declare function executeUpgrade(targetDir: string, upgradeSettings?: boolean, riskySettings?: RiskySettingsMode): Promise<UpgradeResult>;
49
54
  /**
50
55
  * Execute upgrade with --add-missing flag
51
56
  * Adds any new skills, agents, and commands that don't exist yet
52
57
  * @param targetDir - Target directory
53
58
  * @param upgradeSettings - If true, merge new settings into existing settings.json
54
59
  */
55
- export declare function executeUpgradeWithMissing(targetDir: string, upgradeSettings?: boolean): Promise<UpgradeResult>;
60
+ export declare function executeUpgradeWithMissing(targetDir: string, upgradeSettings?: boolean, riskySettings?: RiskySettingsMode): Promise<UpgradeResult>;
56
61
  export default executeInit;
57
62
  //# sourceMappingURL=executor.d.ts.map
@@ -19,8 +19,9 @@ import { getInstalledCliVersion, HELPERS_STAMP_FILE } from './helper-refresh.js'
19
19
  import { generateClaudeMd } from './claudemd-generator.js';
20
20
  import { recordMemoryPackagePath } from './memory-package-resolver.js';
21
21
  import { COMMONJS_COMPANION_HELPERS, ensureCommonJsCompanions } from './helper-companions.js';
22
- import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
23
- import { AUTO_MEMORY_HOOK_TIMEOUT_S, helperHookCommand, helperScopeFor, helperStatusLineCommand, pinToUserHelpers } from './helper-commands.js';
22
+ import { scanSettingsForRisk, formatRiskFindingsAsWarnings, RISKY_SETTINGS_HINT } from './settings-risk-scanner.js';
23
+ import { applyRiskyMode, checkInitTrust, checkRiskySettings } from './risky-settings-gate.js';
24
+ import { AUTO_MEMORY_HOOK_TIMEOUT_S, GIT_ROOT_STATUSLINE_CMD, helperHookCommand, helperScopeFor, helperStatusLineCommand, pinToUserHelpers } from './helper-commands.js';
24
25
  /**
25
26
  * Skills to copy based on configuration
26
27
  */
@@ -175,6 +176,26 @@ export async function executeInit(options) {
175
176
  },
176
177
  };
177
178
  const targetDir = options.targetDir;
179
+ // ADR-509 §2.3 — stop before anything is written when the settings.json this
180
+ // init would merge carries project auto-approve keys (--force overwrites it,
181
+ // so nothing is carried forward then).
182
+ // A project-shipped .claude/helpers/<critical helper> that is not ruflo's
183
+ // signed copy is part of the same gate: init keeps it, and ruflo's hooks run it.
184
+ if (options.components.settings && !options.force) {
185
+ const sourceHelpers = findSourceHelpersDir(options.sourceBaseDir);
186
+ const gate = checkInitTrust(targetDir, options.riskySettings ?? 'stop', sourceHelpers);
187
+ if (gate) {
188
+ result.riskySettings = gate.lines;
189
+ if (gate.stop) {
190
+ result.success = false;
191
+ result.stoppedOnRiskySettings = true;
192
+ result.errors.push(`.claude/settings.json or .claude/helpers carries ${gate.findings.length} item(s) init would carry forward. ${RISKY_SETTINGS_HINT}`);
193
+ return result;
194
+ }
195
+ if (gate.mode === 'drop')
196
+ replaceUntrustedHelpers(targetDir, gate, sourceHelpers, result);
197
+ }
198
+ }
178
199
  try {
179
200
  // Create directory structure
180
201
  await createDirectories(targetDir, options, result);
@@ -369,8 +390,7 @@ export function mergeSettingsForUpgrade(existing, targetDir) {
369
390
  // / jetsam / kernel panic. Preserving the user's existing command (the
370
391
  // old behavior here) means anyone who installed pre-#2337 and upgraded
371
392
  // never gets the fix. Now we detect the broken form and overwrite.
372
- const NEW_STATUSLINE_CMD = scope === 'user' ? helperStatusLineCommand('user') :
373
- `node -e "var c=require('child_process'),p=require('path'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}var s=p.join(r,'.claude/helpers/statusline.cjs');process.argv.splice(1,0,s);require(s)"`;
393
+ const NEW_STATUSLINE_CMD = scope === 'user' ? helperStatusLineCommand('user') : GIT_ROOT_STATUSLINE_CMD;
374
394
  // Matches any invocation of `claude-flow hooks statusline` — either via npx
375
395
  // (`npx [--prefer-offline] [@]claude-flow[/cli][@<tag>] hooks statusline …`)
376
396
  // or as a bare command (`claude-flow hooks statusline …`). Both forms cold-
@@ -479,7 +499,7 @@ export function mergeSettingsForUpgrade(existing, targetDir) {
479
499
  * @param targetDir - Target directory
480
500
  * @param upgradeSettings - If true, merge new settings into existing settings.json
481
501
  */
482
- export async function executeUpgrade(targetDir, upgradeSettings = false) {
502
+ export async function executeUpgrade(targetDir, upgradeSettings = false, riskySettings = 'stop') {
483
503
  const result = {
484
504
  success: true,
485
505
  updated: [],
@@ -488,6 +508,20 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
488
508
  errors: [],
489
509
  settingsUpdated: [],
490
510
  };
511
+ // ADR-509 §2.3 — `--settings` carries the existing settings.json forward:
512
+ // stop before anything is written when it holds project auto-approve keys.
513
+ if (upgradeSettings) {
514
+ const gate = checkRiskySettings(path.join(targetDir, '.claude', 'settings.json'), riskySettings);
515
+ if (gate) {
516
+ result.riskySettings = gate.lines;
517
+ if (gate.stop) {
518
+ result.success = false;
519
+ result.stoppedOnRiskySettings = true;
520
+ result.errors.push(`.claude/settings.json carries ${gate.findings.length} auto-approve key(s) the upgrade would carry forward. ${RISKY_SETTINGS_HINT}`);
521
+ return result;
522
+ }
523
+ }
524
+ }
491
525
  try {
492
526
  // Ensure required directories exist
493
527
  const dirs = [
@@ -664,7 +698,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
664
698
  const settingsPath = path.join(targetDir, '.claude', 'settings.json');
665
699
  if (fs.existsSync(settingsPath)) {
666
700
  try {
667
- const existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
701
+ const existingSettings = applyRiskyMode(JSON.parse(fs.readFileSync(settingsPath, 'utf-8')), riskySettings).settings;
668
702
  const { merged: mergedSettings, warnings: settingsRiskWarnings } = mergeSettingsForUpgrade(existingSettings, targetDir);
669
703
  fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf-8');
670
704
  result.updated.push('.claude/settings.json');
@@ -706,9 +740,9 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
706
740
  * @param targetDir - Target directory
707
741
  * @param upgradeSettings - If true, merge new settings into existing settings.json
708
742
  */
709
- export async function executeUpgradeWithMissing(targetDir, upgradeSettings = false) {
743
+ export async function executeUpgradeWithMissing(targetDir, upgradeSettings = false, riskySettings = 'stop') {
710
744
  // First do the normal upgrade (pass through upgradeSettings)
711
- const result = await executeUpgrade(targetDir, upgradeSettings);
745
+ const result = await executeUpgrade(targetDir, upgradeSettings, riskySettings);
712
746
  if (!result.success) {
713
747
  return result;
714
748
  }
@@ -821,7 +855,8 @@ async function writeSettings(targetDir, options, result) {
821
855
  if (fs.existsSync(settingsPath) && !options.force) {
822
856
  // Merge hooks/env/permissions into existing settings instead of skipping
823
857
  try {
824
- const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
858
+ const dropped = applyRiskyMode(JSON.parse(fs.readFileSync(settingsPath, 'utf-8')), options.riskySettings);
859
+ const existing = dropped.settings;
825
860
  // Scan the pre-existing hooks/permissions BEFORE any merge decision
826
861
  // below — same untrusted, disk-sourced content mergeSettingsForUpgrade()
827
862
  // scans; see settings-risk-scanner.ts for why.
@@ -829,7 +864,8 @@ async function writeSettings(targetDir, options, result) {
829
864
  if (riskWarnings.length > 0) {
830
865
  result.warnings = [...(result.warnings ?? []), ...riskWarnings];
831
866
  }
832
- let merged = false;
867
+ // ADR-509: --drop-risky-settings removed keys; the file must be rewritten even if nothing else merges.
868
+ let merged = dropped.dropped > 0;
833
869
  // Merge hooks (the critical missing piece — #1484)
834
870
  if (generated.hooks && !existing.hooks) {
835
871
  existing.hooks = generated.hooks;
@@ -1205,6 +1241,24 @@ async function copyAgents(targetDir, options, result) {
1205
1241
  }
1206
1242
  }
1207
1243
  }
1244
+ /**
1245
+ * ADR-509 --drop-risky-settings: put ruflo's shipped copy in place of each
1246
+ * project helper the gate flagged (or remove it when this ruflo ships none, so
1247
+ * the hook falls back to the user's own helper).
1248
+ */
1249
+ function replaceUntrustedHelpers(targetDir, gate, sourceHelpers, result) {
1250
+ for (const f of gate.findings) {
1251
+ if (f.target.kind !== 'helper')
1252
+ continue;
1253
+ const dest = path.join(targetDir, '.claude', 'helpers', f.target.name);
1254
+ const src = sourceHelpers ? path.join(sourceHelpers, f.target.name) : null;
1255
+ fs.rmSync(dest, { force: true, recursive: true });
1256
+ if (src && fs.existsSync(src)) {
1257
+ fs.copyFileSync(src, dest);
1258
+ result.created.files.push(`.claude/helpers/${f.target.name}`);
1259
+ }
1260
+ }
1261
+ }
1208
1262
  /**
1209
1263
  * Find source helpers directory.
1210
1264
  * Validates that the directory contains hook-handler.cjs to avoid
@@ -17,4 +17,20 @@ export declare function helperStatusLineCommand(scope: HelperScope, windows?: bo
17
17
  * commands (other scripts, redirects, chains) are left alone.
18
18
  */
19
19
  export declare function pinToUserHelpers(command: string, windows?: boolean): string | null;
20
+ /**
21
+ * ADR-059 statusLine command `init upgrade` writes for a project-level
22
+ * settings file (the git-root `node -e` form). Exported so the upgrade and the
23
+ * ADR-509 classifier use one string.
24
+ */
25
+ export declare const GIT_ROOT_STATUSLINE_CMD = "node -e \"var c=require('child_process'),p=require('path'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}var s=p.join(r,'.claude/helpers/statusline.cjs');process.argv.splice(1,0,s);require(s)\"";
26
+ /**
27
+ * ADR-509: whether a settings hook (or statusLine) command is one of ruflo's own
28
+ * helper launches. Exact string equality against the closed set of commands
29
+ * ruflo generates or has generated, for each helper, each subcommand it is
30
+ * launched with, each scope and each platform. No substring or pattern test on
31
+ * the helper path: a command that merely mentions a helper (in a comment, after
32
+ * `node -r`, `--import` or a NODE_OPTIONS= prefix, or with code chained on) is
33
+ * a command ruflo did not write.
34
+ */
35
+ export declare function isRufloHelperCommand(command: string): boolean;
20
36
  //# sourceMappingURL=helper-commands.d.ts.map
@@ -170,4 +170,77 @@ export function pinToUserHelpers(command, windows = IS_WINDOWS) {
170
170
  return helperStatusLineCommand('user', windows);
171
171
  return helperHookCommand(script, args.trim(), 'user', windows);
172
172
  }
173
+ /**
174
+ * ADR-059 statusLine command `init upgrade` writes for a project-level
175
+ * settings file (the git-root `node -e` form). Exported so the upgrade and the
176
+ * ADR-509 classifier use one string.
177
+ */
178
+ export const GIT_ROOT_STATUSLINE_CMD = `node -e "var c=require('child_process'),p=require('path'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}var s=p.join(r,'.claude/helpers/statusline.cjs');process.argv.splice(1,0,s);require(s)"`;
179
+ /** Subcommands ruflo has written for each helper it launches from a hook. */
180
+ const HELPER_SUBCOMMANDS = {
181
+ 'hook-handler.cjs': [
182
+ 'pre-bash', 'post-bash', 'pre-edit', 'post-edit', 'pre-task', 'post-task', 'pre-command', 'post-command',
183
+ 'route', 'session-restore', 'session-end', 'session-start', 'notify', 'stats', 'status', 'compact-manual', 'compact-auto',
184
+ ],
185
+ 'auto-memory-hook.mjs': ['import', 'sync', 'status'],
186
+ };
187
+ const GIT_ROOT = "var c=require('child_process'),p=require('path'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}";
188
+ const GIT_ROOT_ESM = "var c=require('child_process'),p=require('path'),u=require('url'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}";
189
+ /** Every exact command string ruflo has generated (or generates) to launch `script` with `sub`. */
190
+ function generatedForms(script, sub) {
191
+ const arg = sub ? ` ${sub}` : '';
192
+ const win = script.replace(/\//g, '\\');
193
+ const forms = [
194
+ ...['project', 'user'].flatMap((scope) => [helperHookCommand(script, sub, scope, false), helperHookCommand(script, sub, scope, true)]),
195
+ `node ${script}${arg}`,
196
+ `node "$CLAUDE_PROJECT_DIR/${script}"${arg}`,
197
+ `sh -c 'exec node "\${CLAUDE_PROJECT_DIR:-.}/${script}"${arg}'`,
198
+ `cmd /c node %CLAUDE_PROJECT_DIR%/${script}${arg}`,
199
+ `cmd /c "IF EXIST \"%CLAUDE_PROJECT_DIR%\\${win}\" (node \"%CLAUDE_PROJECT_DIR%\\${win}\"${arg}) ELSE (node \"%USERPROFILE%\\${win}\"${arg})"`,
200
+ ];
201
+ if (script.endsWith('.mjs')) {
202
+ forms.push(`node -e "${GIT_ROOT_ESM}var f=p.join(r,'${script}');process.argv.splice(1,0,f);import(u.pathToFileURL(f).href)"${arg}`, `node -e "${GIT_ROOT_ESM}var f=p.join(r,'${script}');import(u.pathToFileURL(f).href)"${arg}`);
203
+ }
204
+ else {
205
+ forms.push(`node -e "${GIT_ROOT}var s=p.join(r,'${script}');process.argv.splice(1,0,s);require(s)"${arg}`);
206
+ }
207
+ return forms;
208
+ }
209
+ let knownCommands;
210
+ /** The closed set of hook and statusLine commands ruflo writes, built once. */
211
+ function rufloCommands() {
212
+ if (knownCommands)
213
+ return knownCommands;
214
+ const set = new Set();
215
+ for (const [helper, subs] of Object.entries(HELPER_SUBCOMMANDS)) {
216
+ for (const sub of subs)
217
+ for (const form of generatedForms(`.claude/helpers/${helper}`, sub))
218
+ set.add(form);
219
+ }
220
+ for (const scope of ['project', 'user'])
221
+ for (const win of [false, true])
222
+ set.add(helperStatusLineCommand(scope, win));
223
+ for (const form of generatedForms(STATUSLINE_SCRIPT, ''))
224
+ set.add(form);
225
+ set.add(GIT_ROOT_STATUSLINE_CMD);
226
+ knownCommands = set;
227
+ return set;
228
+ }
229
+ // Older ruflo-generated CLI hooks (`npx @claude-flow/cli@latest hooks pre-bash`), which `init upgrade` migrates.
230
+ // Exact shape only: a registry spec with a version or tag (never a bare name a project's node_modules/.bin
231
+ // could answer), one of ruflo's own hook/memory subcommands, and at most ten plain words with no shell
232
+ // syntax (bounded, one alternative: no backtracking blow-up on a crafted settings file).
233
+ const LEGACY_NPX_HOOK = /^npx (?:-y |--yes )?(?:@claude-flow\/cli|claude-flow|ruflo)@(?:latest|alpha|v3alpha|\d+\.\d+\.\d+(?:-[\w.]+)?) (?:hooks (?:pre-bash|post-bash|pre-edit|post-edit|pre-task|post-task|pre-command|post-command|route|session-start|session-end|session-restore|notify|statusline)|memory (?:import|sync|store))(?: [\w.:=-]+){0,10}$/;
234
+ /**
235
+ * ADR-509: whether a settings hook (or statusLine) command is one of ruflo's own
236
+ * helper launches. Exact string equality against the closed set of commands
237
+ * ruflo generates or has generated, for each helper, each subcommand it is
238
+ * launched with, each scope and each platform. No substring or pattern test on
239
+ * the helper path: a command that merely mentions a helper (in a comment, after
240
+ * `node -r`, `--import` or a NODE_OPTIONS= prefix, or with code chained on) is
241
+ * a command ruflo did not write.
242
+ */
243
+ export function isRufloHelperCommand(command) {
244
+ return rufloCommands().has(command) || LEGACY_NPX_HOOK.test(command);
245
+ }
173
246
  //# sourceMappingURL=helper-commands.js.map
@@ -0,0 +1,39 @@
1
+ import { type AutoApproveFinding, type RiskySettingsMode } from './settings-risk-scanner.js';
2
+ export interface RiskySettingsGate {
3
+ mode: RiskySettingsMode;
4
+ findings: AutoApproveFinding[];
5
+ /** Display lines: location, reason, sanitized snippet. */
6
+ lines: string[];
7
+ /** True when init must stop (mode `stop` and at least one finding). */
8
+ stop: boolean;
9
+ }
10
+ export declare function scanAutoApprove(settings: Record<string, unknown>): AutoApproveFinding[];
11
+ /** Reads a settings file and judges it; null when the file is absent, unreadable or not a JSON object. */
12
+ export declare function checkRiskySettings(settingsPath: string, mode?: RiskySettingsMode): RiskySettingsGate | null;
13
+ /**
14
+ * ADR-509 §2.3: the helpers ruflo's hooks launch, as the PROJECT ships them.
15
+ * `init` keeps an existing `.claude/helpers/<name>` unless --force, and the
16
+ * project-scope hook commands prefer the project's copy, so a repository that
17
+ * ships its own hook-handler.cjs runs it under an exact ruflo hook command. A
18
+ * helper is trusted when its bytes are the copy this ruflo ships, or its
19
+ * SHA-256 is in a helpers manifest signed with ruflo's key (this ruflo's, or
20
+ * the project's own, which an older signed release left). Anything else is a
21
+ * finding. A `.LOCKED` helpers dir (deliberate local edits) is skipped, and with
22
+ * no ground truth at all this fails open. `pubkeyPem` is injectable for tests.
23
+ */
24
+ export declare function untrustedProjectHelpers(targetDir: string, sourceHelpersDir: string | null, pubkeyPem?: string): AutoApproveFinding[];
25
+ /**
26
+ * The init gate: the settings file's auto-approve keys plus the project's
27
+ * untrusted helpers. Null when there is nothing to report.
28
+ */
29
+ export declare function checkInitTrust(targetDir: string, mode: RiskySettingsMode, sourceHelpersDir: string | null): RiskySettingsGate | null;
30
+ /** `drop`: the settings without the auto-approve keys; otherwise the settings unchanged. */
31
+ export declare function applyRiskyMode(settings: Record<string, unknown>, mode: RiskySettingsMode | undefined): {
32
+ settings: Record<string, unknown>;
33
+ dropped: number;
34
+ };
35
+ /** Reads `--keep-risky-settings` / `--drop-risky-settings` (camel or kebab); both at once is an error. */
36
+ export declare function riskyModeFromFlags(flags: Record<string, unknown>): RiskySettingsMode | {
37
+ error: string;
38
+ };
39
+ //# sourceMappingURL=risky-settings-gate.d.ts.map