@claude-flow/cli 3.25.5 → 3.26.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 (83) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/.helpers-version +1 -1
  3. package/.claude/helpers/helpers.manifest.json +5 -4
  4. package/.claude/helpers/hook-handler.cjs +129 -0
  5. package/.claude/helpers/statusline.cjs +771 -505
  6. package/.claude/proven-config.json +42 -0
  7. package/catalog-manifest.json +12 -0
  8. package/dist/src/commands/advisor.d.ts +15 -0
  9. package/dist/src/commands/advisor.js +94 -0
  10. package/dist/src/commands/doctor.js +151 -32
  11. package/dist/src/commands/funnel.d.ts +13 -0
  12. package/dist/src/commands/funnel.js +103 -0
  13. package/dist/src/commands/hooks.js +112 -46
  14. package/dist/src/commands/index.js +10 -0
  15. package/dist/src/commands/init.js +44 -0
  16. package/dist/src/commands/proxy.d.ts +21 -0
  17. package/dist/src/commands/proxy.js +310 -0
  18. package/dist/src/commands/security.js +29 -0
  19. package/dist/src/commands/settings.d.ts +19 -0
  20. package/dist/src/commands/settings.js +180 -0
  21. package/dist/src/commands/version.d.ts +42 -0
  22. package/dist/src/commands/version.js +106 -0
  23. package/dist/src/funnel/advisor-tip.d.ts +58 -0
  24. package/dist/src/funnel/advisor-tip.js +92 -0
  25. package/dist/src/funnel/attribution.d.ts +37 -0
  26. package/dist/src/funnel/attribution.js +101 -0
  27. package/dist/src/funnel/consent.d.ts +22 -0
  28. package/dist/src/funnel/consent.js +55 -0
  29. package/dist/src/funnel/credit-errors.d.ts +31 -0
  30. package/dist/src/funnel/credit-errors.js +88 -0
  31. package/dist/src/funnel/credit-notifier.d.ts +44 -0
  32. package/dist/src/funnel/credit-notifier.js +74 -0
  33. package/dist/src/funnel/disclosure.d.ts +46 -0
  34. package/dist/src/funnel/disclosure.js +97 -0
  35. package/dist/src/funnel/enrollment.d.ts +36 -0
  36. package/dist/src/funnel/enrollment.js +64 -0
  37. package/dist/src/funnel/environment.d.ts +17 -0
  38. package/dist/src/funnel/environment.js +39 -0
  39. package/dist/src/funnel/event-transport.d.ts +51 -0
  40. package/dist/src/funnel/event-transport.js +199 -0
  41. package/dist/src/funnel/events.d.ts +42 -0
  42. package/dist/src/funnel/events.js +150 -0
  43. package/dist/src/funnel/index.d.ts +20 -0
  44. package/dist/src/funnel/index.js +20 -0
  45. package/dist/src/funnel/insights.d.ts +49 -0
  46. package/dist/src/funnel/insights.js +120 -0
  47. package/dist/src/funnel/local-signals.d.ts +15 -0
  48. package/dist/src/funnel/local-signals.js +68 -0
  49. package/dist/src/funnel/message-transport.d.ts +51 -0
  50. package/dist/src/funnel/message-transport.js +149 -0
  51. package/dist/src/funnel/messages.d.ts +55 -0
  52. package/dist/src/funnel/messages.js +160 -0
  53. package/dist/src/funnel/power-saver-notifier.d.ts +44 -0
  54. package/dist/src/funnel/power-saver-notifier.js +92 -0
  55. package/dist/src/funnel/precedence.d.ts +16 -0
  56. package/dist/src/funnel/precedence.js +85 -0
  57. package/dist/src/funnel/promo.d.ts +41 -0
  58. package/dist/src/funnel/promo.js +144 -0
  59. package/dist/src/funnel/rate-limit-notifier.d.ts +55 -0
  60. package/dist/src/funnel/rate-limit-notifier.js +102 -0
  61. package/dist/src/funnel/rotation.d.ts +19 -0
  62. package/dist/src/funnel/rotation.js +70 -0
  63. package/dist/src/funnel/state.d.ts +13 -0
  64. package/dist/src/funnel/state.js +52 -0
  65. package/dist/src/funnel/toggle-cooldown.d.ts +17 -0
  66. package/dist/src/funnel/toggle-cooldown.js +32 -0
  67. package/dist/src/funnel/types.d.ts +86 -0
  68. package/dist/src/funnel/types.js +26 -0
  69. package/dist/src/init/executor.js +25 -19
  70. package/dist/src/init/helper-refresh.d.ts +33 -2
  71. package/dist/src/init/helper-refresh.js +102 -12
  72. package/dist/src/init/helpers-generator.js +52 -0
  73. package/dist/src/init/mcp-generator.js +11 -6
  74. package/dist/src/init/statusline-generator.js +282 -116
  75. package/dist/src/services/daemon-autostart.js +28 -5
  76. package/dist/src/services/fable-harness.d.ts +39 -0
  77. package/dist/src/services/fable-harness.js +41 -0
  78. package/package.json +16 -4
  79. package/plugins/ruflo-metaharness/scripts/smoke.sh +18 -5
  80. package/dist/src/ruvector/lattice-wasm.d.ts +0 -14
  81. package/dist/src/ruvector/lattice-wasm.js +0 -144
  82. package/plugins/ruflo-metaharness/.claude-flow/data/pending-insights.jsonl +0 -5
  83. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +0 -6
@@ -8,6 +8,7 @@ import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import { storeCommand } from './transfer-store.js';
9
9
  import { existsSync, readFileSync, statSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
+ import { getSecurityStatus as sharedGetSecurityStatus, getSwarmStatus as sharedGetSwarmStatus, getGitUncommittedCount as sharedGetGitUncommittedCount, } from '../funnel/local-signals.js';
11
12
  /**
12
13
  * #1686 — `?? 0` only defaults null/undefined; NaN slips through and
13
14
  * surfaces as `"NaN"` (or earlier crashed `.toFixed`) in the metrics
@@ -3412,52 +3413,11 @@ const statuslineCommand = {
3412
3413
  const dddProgress = Math.min(100, Math.floor((domainsCompleted / totalDomains) * 100));
3413
3414
  return { domainsCompleted, totalDomains, dddProgress, patternsLearned: learning.patterns, sessionsCompleted: learning.sessions };
3414
3415
  }
3415
- // Get security status
3416
- function getSecurityStatus() {
3417
- const scanResultsPath = path.join(process.cwd(), '.claude', 'security-scans');
3418
- let cvesFixed = 0;
3419
- const totalCves = 3;
3420
- if (fs.existsSync(scanResultsPath)) {
3421
- try {
3422
- const scans = fs.readdirSync(scanResultsPath).filter((f) => f.endsWith('.json'));
3423
- cvesFixed = Math.min(totalCves, scans.length);
3424
- }
3425
- catch {
3426
- // Ignore
3427
- }
3428
- }
3429
- const auditPath = path.join(process.cwd(), '.swarm', 'security');
3430
- if (fs.existsSync(auditPath)) {
3431
- try {
3432
- const audits = fs.readdirSync(auditPath).filter((f) => f.includes('audit'));
3433
- cvesFixed = Math.min(totalCves, Math.max(cvesFixed, audits.length));
3434
- }
3435
- catch {
3436
- // Ignore
3437
- }
3438
- }
3439
- const status = cvesFixed >= totalCves ? 'CLEAN' : cvesFixed > 0 ? 'IN_PROGRESS' : 'PENDING';
3440
- return { status, cvesFixed, totalCves };
3441
- }
3442
- // Get swarm status
3443
- function getSwarmStatus() {
3444
- let activeAgents = 0;
3445
- let coordinationActive = false;
3446
- const maxAgents = 15;
3447
- const isWindows = process.platform === 'win32';
3448
- try {
3449
- const psCmd = isWindows
3450
- ? 'tasklist /FI "IMAGENAME eq node.exe" /NH 2>NUL | find /c /v "" 2>NUL || echo 0'
3451
- : 'ps aux 2>/dev/null | grep -c agentic-flow || echo "0"';
3452
- const ps = execSync(psCmd, { encoding: 'utf-8', timeout: 3000 });
3453
- activeAgents = Math.max(0, parseInt(ps.trim()) - 1);
3454
- coordinationActive = activeAgents > 0;
3455
- }
3456
- catch {
3457
- // ps/tasklist unavailable or timed out — report zero
3458
- }
3459
- return { activeAgents, maxAgents, coordinationActive };
3460
- }
3416
+ // Security/swarm status — shared with the advisor-tip refresh (ADR-316)
3417
+ // via funnel/local-signals.ts, a single source of truth so the two call
3418
+ // sites can never silently drift on what these signals mean.
3419
+ const getSecurityStatus = sharedGetSecurityStatus;
3420
+ const getSwarmStatus = sharedGetSwarmStatus;
3461
3421
  // Get system metrics
3462
3422
  function getSystemMetrics() {
3463
3423
  let memoryMB = 0;
@@ -3554,12 +3514,30 @@ const statuslineCommand = {
3554
3514
  const swarm = getSwarmStatus();
3555
3515
  const system = getSystemMetrics();
3556
3516
  const user = getUserInfo();
3517
+ const getGitUncommittedCount = sharedGetGitUncommittedCount;
3518
+ // Funnel promo row (ADR-301). The statusline is spawned with piped stdio
3519
+ // by an interactive host, so interactivity is asserted here; all other
3520
+ // gates (RUFLO_FUNNEL, enterprise policy, config, CI, disclosure,
3521
+ // rotation ratio) are enforced inside getFunnelPromo. Never allowed to
3522
+ // break the statusline.
3523
+ let promo = null;
3524
+ try {
3525
+ const { getFunnelPromo } = await import('../funnel/index.js');
3526
+ promo = getFunnelPromo({
3527
+ interactive: true,
3528
+ localInsights: { security, swarm, gitUncommittedCount: getGitUncommittedCount() },
3529
+ });
3530
+ }
3531
+ catch {
3532
+ promo = null;
3533
+ }
3557
3534
  const statusData = {
3558
3535
  user,
3559
3536
  v3Progress: progress,
3560
3537
  security,
3561
3538
  swarm,
3562
3539
  system,
3540
+ promo,
3563
3541
  timestamp: new Date().toISOString()
3564
3542
  };
3565
3543
  // JSON output
@@ -4418,6 +4396,90 @@ const notifyCommand = {
4418
4396
  return { success: true, data: { timestamp, level, message } };
4419
4397
  }
4420
4398
  };
4399
+ // Refresh-funnel subcommand — the fix for "promo doesn't load right away".
4400
+ //
4401
+ // refreshRemoteMessages() (funnel/message-transport.ts) is fire-and-forget
4402
+ // by design so the STATUSLINE's own per-render invocation never blocks on
4403
+ // a network call. But the statusline is spawned as a short-lived subprocess
4404
+ // per render (execSync from statusline-generator.ts) — a fire-and-forget
4405
+ // promise kicked off there has no "later" to run in; the process exits
4406
+ // before the HTTPS fetch can complete, so the local message cache never
4407
+ // actually gets written and the promo row never appears (confirmed live:
4408
+ // two consecutive cold-cache statusline renders, 5s apart, both returned
4409
+ // promo:null and the cache file was never created).
4410
+ //
4411
+ // This command exists to be invoked from a LONGER-LIVED context — the
4412
+ // SessionStart hook (see hook-handler.cjs's 'session-restore' handler,
4413
+ // which spawns this detached so it isn't killed when the hook's own
4414
+ // process exits, and isn't awaited so it doesn't add to the hook's own
4415
+ // timeout budget). One real, properly-awaited refresh attempt here, once
4416
+ // per session, is what actually gives refreshRemoteMessages() a chance to
4417
+ // finish before anything needs the cache.
4418
+ const refreshFunnelCommand = {
4419
+ name: 'refresh-funnel',
4420
+ description: 'Best-effort background refresh of the funnel message cache (internal — see hook-handler.cjs session-restore)',
4421
+ options: [
4422
+ { name: 'quiet', description: 'Suppress output (used when spawned detached from a hook)', type: 'boolean', default: false },
4423
+ ],
4424
+ action: async (ctx) => {
4425
+ try {
4426
+ const { refreshRemoteMessages } = await import('../funnel/index.js');
4427
+ const result = await refreshRemoteMessages();
4428
+ if (!ctx.flags.quiet) {
4429
+ output.writeln(JSON.stringify(result));
4430
+ }
4431
+ return { success: true, data: result };
4432
+ }
4433
+ catch (error) {
4434
+ // Fail silent by design (matches message-transport.ts's own "fail
4435
+ // silent" discipline) — a broken refresh must never surface as a
4436
+ // hook error, only ever as "no promo this session."
4437
+ if (!ctx.flags.quiet) {
4438
+ output.printError(`refresh-funnel failed: ${error instanceof Error ? error.message : String(error)}`);
4439
+ }
4440
+ return { success: true, data: { refreshed: false, skipped: 'error' } };
4441
+ }
4442
+ }
4443
+ };
4444
+ // Refresh-advisor subcommand — ADR-316's co-pilot tip. Mirrors
4445
+ // refreshFunnelCommand exactly: a properly-awaited CLI subcommand meant to
4446
+ // be spawned DETACHED from hook-handler.cjs's session-restore handler, so a
4447
+ // real (potentially multi-second, real-money) `claude -p` call gets a
4448
+ // chance to finish without ever blocking or being awaited by the hook's own
4449
+ // process. refreshAdvisorTipIfStale() itself is the safety net that makes
4450
+ // this cheap to call on every session-restore: it checks consent + a 24h
4451
+ // TTL BEFORE spending anything, so most invocations are a no-op file read.
4452
+ const refreshAdvisorCommand = {
4453
+ name: 'refresh-advisor',
4454
+ description: 'Best-effort background refresh of the co-pilot advisor tip (internal — see hook-handler.cjs session-restore; ADR-316)',
4455
+ options: [
4456
+ { name: 'quiet', description: 'Suppress output (used when spawned detached from a hook)', type: 'boolean', default: false },
4457
+ ],
4458
+ action: async (ctx) => {
4459
+ try {
4460
+ const { refreshAdvisorTipIfStale } = await import('../funnel/advisor-tip.js');
4461
+ const { getSecurityStatus, getSwarmStatus, getGitUncommittedCount } = await import('../funnel/local-signals.js');
4462
+ const result = await refreshAdvisorTipIfStale({
4463
+ security: getSecurityStatus(),
4464
+ swarm: getSwarmStatus(),
4465
+ gitUncommittedCount: getGitUncommittedCount(),
4466
+ });
4467
+ if (!ctx.flags.quiet) {
4468
+ output.writeln(JSON.stringify(result));
4469
+ }
4470
+ return { success: true, data: result };
4471
+ }
4472
+ catch (error) {
4473
+ // Fail silent by design (matches refresh-funnel's own discipline) — a
4474
+ // broken advisor refresh must never surface as a hook error, only
4475
+ // ever as "no tip this window."
4476
+ if (!ctx.flags.quiet) {
4477
+ output.printError(`refresh-advisor failed: ${error instanceof Error ? error.message : String(error)}`);
4478
+ }
4479
+ return { success: true, data: { refreshed: false, reason: 'error' } };
4480
+ }
4481
+ }
4482
+ };
4421
4483
  // Main hooks command
4422
4484
  export const hooksCommand = {
4423
4485
  name: 'hooks',
@@ -4461,6 +4523,10 @@ export const hooksCommand = {
4461
4523
  // Agent Teams integration
4462
4524
  teammateIdleCommand,
4463
4525
  taskCompletedCommand,
4526
+ // Funnel background refresh — see refreshFunnelCommand's own doc comment
4527
+ refreshFunnelCommand,
4528
+ // Advisor co-pilot tip background refresh — ADR-316
4529
+ refreshAdvisorCommand,
4464
4530
  ],
4465
4531
  options: [],
4466
4532
  examples: [
@@ -31,6 +31,7 @@ const commandLoaders = {
31
31
  'hive-mind': () => import('./hive-mind.js'),
32
32
  process: () => import('./process.js'),
33
33
  daemon: () => import('./daemon.js'),
34
+ version: () => import('./version.js'),
34
35
  // V3 Advanced Commands (less frequently used - lazy load)
35
36
  neural: () => import('./neural.js'),
36
37
  security: () => import('./security.js'),
@@ -73,6 +74,15 @@ const commandLoaders = {
73
74
  metaharness: () => import('./metaharness.js'),
74
75
  // Eject (ADR-150 Phase 2) — lift ruflo project into a renamed standalone harness
75
76
  eject: () => import('./eject.js'),
77
+ // Cognitum lifecycle funnel controls (ADR-301/305/309)
78
+ funnel: () => import('./funnel.js'),
79
+ // User-facing preferences wrapper (ADR-311 copy discipline — no "funnel" in
80
+ // the user surface). Forwards to the funnel primitives internally.
81
+ settings: () => import('./settings.js'),
82
+ // Meta LLM Proxy — sponsored downtime capacity (ADR-304/307/313)
83
+ proxy: () => import('./proxy.js'),
84
+ // Fable co-pilot advisor tip in the statusline insight ticker (ADR-316)
85
+ advisor: () => import('./advisor.js'),
76
86
  };
77
87
  // Cache for loaded commands
78
88
  const loadedCommands = new Map();
@@ -7,6 +7,41 @@ import { confirm, select, multiSelect, input } from '../prompt.js';
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import { executeInit, executeUpgrade, executeUpgradeWithMissing, DEFAULT_INIT_OPTIONS, MINIMAL_INIT_OPTIONS, FULL_INIT_OPTIONS, } from '../init/index.js';
10
+ import { ENROLLMENT_SCREEN, recordEnrollmentOutcome, shouldOfferEnrollment, } from '../funnel/enrollment.js';
11
+ /**
12
+ * ADR-302 post-init capability enrollment. One-time, interactive-TTY-only,
13
+ * skippable with --no-signup, auto-skipped in CI/automation. Never throws
14
+ * and never affects init's exit code. Accepting records the `account`
15
+ * consent receipt only — no other capability is enabled by this prompt.
16
+ */
17
+ async function offerCapabilityEnrollment(ctx) {
18
+ try {
19
+ const noSignup = Boolean(ctx.flags['no-signup'] || ctx.flags.noSignup);
20
+ if (ctx.flags.format === 'json')
21
+ return; // scripted output stays pure
22
+ if (!shouldOfferEnrollment({ noSignup, cwd: ctx.cwd }))
23
+ return;
24
+ output.writeln();
25
+ output.writeln(ENROLLMENT_SCREEN);
26
+ output.writeln();
27
+ const accepted = await confirm({
28
+ message: 'Create a free Cognitum account now?',
29
+ default: false,
30
+ });
31
+ recordEnrollmentOutcome(Boolean(accepted));
32
+ output.writeln();
33
+ if (accepted) {
34
+ output.printInfo('Create your free account at https://cognitum.one');
35
+ output.writeln(output.dim(' CLI sign-in (`ruflo auth login`) ships with the ADR-306 auth release.'));
36
+ }
37
+ else {
38
+ output.writeln(output.dim('You can enable later at https://cognitum.one — this prompt will not repeat.'));
39
+ }
40
+ }
41
+ catch {
42
+ // Enrollment is optional; init success is never affected.
43
+ }
44
+ }
10
45
  // Codex initialization action
11
46
  async function initCodexAction(ctx, options) {
12
47
  const { force, minimal, full, dualMode } = options;
@@ -445,6 +480,7 @@ const initAction = async (ctx) => {
445
480
  if (ctx.flags.format === 'json') {
446
481
  output.printJson(result);
447
482
  }
483
+ await offerCapabilityEnrollment(ctx);
448
484
  return { success: true, data: result };
449
485
  }
450
486
  catch (error) {
@@ -686,6 +722,7 @@ const wizardCommand = {
686
722
  { setting: 'Hooks', value: `${result.summary.hooksEnabled} enabled` },
687
723
  ],
688
724
  });
725
+ await offerCapabilityEnrollment(ctx);
689
726
  return { success: true, data: result };
690
727
  }
691
728
  catch (error) {
@@ -1017,6 +1054,13 @@ export const initCommand = {
1017
1054
  type: 'boolean',
1018
1055
  default: false,
1019
1056
  },
1057
+ {
1058
+ // ADR-302 — skip the one-time post-init capability enrollment prompt.
1059
+ name: 'no-signup',
1060
+ description: 'Skip the post-init Cognitum capability enrollment prompt',
1061
+ type: 'boolean',
1062
+ default: false,
1063
+ },
1020
1064
  {
1021
1065
  name: 'only-claude',
1022
1066
  description: 'Only create .claude/ directory (skip runtime)',
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `ruflo proxy` — Meta LLM Proxy control surface (ADR-304/307/313).
3
+ *
4
+ * This command currently implements the ADR-313 sponsored-downtime
5
+ * subcommands only (`sponsor-enable` / `sponsor-disable` / `sponsor-status`).
6
+ * The full lifecycle command set ADR-307 specifies
7
+ * (`install|start|stop|status|logs|update|uninstall`) manages the proxy
8
+ * *process* itself and is out of scope here — this surface only manages
9
+ * the consent-gated sponsored-mode flag the already-running proxy reads
10
+ * from its config mirror.
11
+ *
12
+ * Consent (ADR-302/313): granting `sponsored-downtime` consent here writes
13
+ * BOTH the ruflo-side consent receipt (source of truth) AND a mirror flag
14
+ * into ~/.ruflo/proxy-config.toml (the file the Rust proxy binary reads).
15
+ * The proxy never writes this file — ruflo does, exactly once per
16
+ * enable/disable action.
17
+ */
18
+ import type { Command } from '../types.js';
19
+ export declare const proxyCommand: Command;
20
+ export default proxyCommand;
21
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1,310 @@
1
+ /**
2
+ * `ruflo proxy` — Meta LLM Proxy control surface (ADR-304/307/313).
3
+ *
4
+ * This command currently implements the ADR-313 sponsored-downtime
5
+ * subcommands only (`sponsor-enable` / `sponsor-disable` / `sponsor-status`).
6
+ * The full lifecycle command set ADR-307 specifies
7
+ * (`install|start|stop|status|logs|update|uninstall`) manages the proxy
8
+ * *process* itself and is out of scope here — this surface only manages
9
+ * the consent-gated sponsored-mode flag the already-running proxy reads
10
+ * from its config mirror.
11
+ *
12
+ * Consent (ADR-302/313): granting `sponsored-downtime` consent here writes
13
+ * BOTH the ruflo-side consent receipt (source of truth) AND a mirror flag
14
+ * into ~/.ruflo/proxy-config.toml (the file the Rust proxy binary reads).
15
+ * The proxy never writes this file — ruflo does, exactly once per
16
+ * enable/disable action.
17
+ */
18
+ import * as fs from 'fs';
19
+ import { output } from '../output.js';
20
+ import { hasConsent, recordConsent, revokeConsent, funnelStateDir } from '../funnel/index.js';
21
+ import { recordFunnelEvent } from '../funnel/events.js';
22
+ import { clearRateLimitStatus, readRateLimitStatus } from '../funnel/rate-limit-notifier.js';
23
+ import { clearQuotaLowStatus, readQuotaLowStatus } from '../funnel/power-saver-notifier.js';
24
+ import { getInstalledCliVersion } from '../init/helper-refresh.js';
25
+ import * as path from 'path';
26
+ const PROXY_CONFIG_FILE = 'proxy-config.toml';
27
+ /**
28
+ * Minimal hand-rolled TOML writer — this config has exactly one boolean
29
+ * field ruflo needs to set (`sponsored_consent_granted`); every other field
30
+ * the proxy binary defaults itself (ADR-307 "a malformed config must never
31
+ * crash the proxy — it degrades to safe defaults"). We read-modify-write
32
+ * the raw text so a user's own bind/backend customizations survive, only
33
+ * ever touching this one line. No TOML parser dependency needed for this.
34
+ */
35
+ function readProxyConfigRaw() {
36
+ try {
37
+ return fs.readFileSync(path.join(funnelStateDir(), PROXY_CONFIG_FILE), 'utf-8');
38
+ }
39
+ catch {
40
+ return '';
41
+ }
42
+ }
43
+ function writeConsentMirrorLine(field, value) {
44
+ const dir = funnelStateDir();
45
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
46
+ const target = path.join(dir, PROXY_CONFIG_FILE);
47
+ const raw = readProxyConfigRaw();
48
+ const line = `${field} = ${value}`;
49
+ const pattern = new RegExp(`^${field}\\s*=.*$`, 'm');
50
+ let next;
51
+ if (pattern.test(raw)) {
52
+ next = raw.replace(pattern, line);
53
+ }
54
+ else {
55
+ next = raw.length > 0 ? `${raw.trimEnd()}\n${line}\n` : `${line}\n`;
56
+ }
57
+ const tmp = `${target}.tmp`;
58
+ fs.writeFileSync(tmp, next, { encoding: 'utf-8', mode: 0o600 });
59
+ fs.renameSync(tmp, target);
60
+ }
61
+ function writeSponsoredConsentMirror(granted) {
62
+ writeConsentMirrorLine('sponsored_consent_granted', granted);
63
+ }
64
+ function writePowerSaverConsentMirror(granted) {
65
+ writeConsentMirrorLine('power_saver_consent_granted', granted);
66
+ }
67
+ function writeTrainingShareConsentMirror(granted) {
68
+ writeConsentMirrorLine('training_share_consent_granted', granted);
69
+ }
70
+ const SPONSOR_DISCLOSURE = [
71
+ 'Enabling sponsored downtime mode.',
72
+ '',
73
+ "While your Claude usage limit resets, requests can be routed through",
74
+ "Cognitum's own model capacity, sponsored at no cost to you. This is a",
75
+ 'separate data plane from your own cloud-routing config — Cognitum sees',
76
+ 'these prompts (server-side, same handling as any api.cognitum.one',
77
+ 'request), never your own Claude account.',
78
+ '',
79
+ 'Sponsored capacity is rate-limited and best-effort — Cognitum may',
80
+ 'throttle or decline requests under load. Disable anytime:',
81
+ ' ruflo proxy sponsor-disable',
82
+ ].join('\n');
83
+ const sponsorEnableSub = {
84
+ name: 'sponsor-enable',
85
+ description: "Opt into Cognitum-sponsored downtime capacity (ADR-313)",
86
+ options: [
87
+ { name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
88
+ ],
89
+ action: async (ctx) => {
90
+ if (hasConsent('sponsored-downtime')) {
91
+ output.writeln('Sponsored downtime mode is already enabled.');
92
+ return { success: true, data: { alreadyEnabled: true } };
93
+ }
94
+ output.writeln(SPONSOR_DISCLOSURE);
95
+ output.writeln('');
96
+ if (!ctx.flags.yes) {
97
+ output.writeln('Re-run with --yes to confirm: ruflo proxy sponsor-enable --yes');
98
+ return { success: true, data: { confirmed: false } };
99
+ }
100
+ recordConsent('sponsored-downtime', true, 'proxy-sponsor-enable');
101
+ writeSponsoredConsentMirror(true);
102
+ recordFunnelEvent('sponsor_mode_enabled', 'statusline', getInstalledCliVersion());
103
+ output.printSuccess('Sponsored downtime mode enabled.');
104
+ output.writeln('The proxy will use it automatically while ruflo settings notices');
105
+ output.writeln('rate-limited is flagged. Disable anytime: ruflo proxy sponsor-disable');
106
+ return { success: true, data: { confirmed: true } };
107
+ },
108
+ };
109
+ const sponsorDisableSub = {
110
+ name: 'sponsor-disable',
111
+ description: 'Revoke sponsored-downtime consent and stop using sponsored capacity',
112
+ action: async () => {
113
+ revokeConsent('sponsored-downtime', 'proxy-sponsor-disable');
114
+ writeSponsoredConsentMirror(false);
115
+ recordFunnelEvent('sponsor_mode_disabled', 'statusline', getInstalledCliVersion());
116
+ output.printSuccess('Sponsored downtime mode disabled.');
117
+ return { success: true };
118
+ },
119
+ };
120
+ const sponsorStatusSub = {
121
+ name: 'sponsor-status',
122
+ description: 'Show sponsored-downtime consent + rate-limit flag state',
123
+ action: async () => {
124
+ const consented = hasConsent('sponsored-downtime');
125
+ const rateLimited = readRateLimitStatus();
126
+ output.writeln(`Sponsored downtime consent: ${consented ? 'granted' : 'not granted'}`);
127
+ output.writeln(`Rate-limit flag: ${rateLimited.limited ? `set (since ${rateLimited.since})` : 'not set'}`);
128
+ if (rateLimited.limited && !consented) {
129
+ output.writeln('');
130
+ output.writeln('You are flagged as rate-limited but have not enabled sponsored');
131
+ output.writeln('capacity. Enable it with: ruflo proxy sponsor-enable --yes');
132
+ }
133
+ return { success: true, data: { consented, rateLimited } };
134
+ },
135
+ };
136
+ /** Convenience: clear the rate-limited flag once you're back to normal. */
137
+ const sponsorClearSub = {
138
+ name: 'sponsor-clear',
139
+ description: 'Clear the rate-limited flag (your Claude limit has reset)',
140
+ action: async () => {
141
+ const changed = clearRateLimitStatus();
142
+ if (!changed) {
143
+ output.printError('Rate-limit flag was just toggled — try again in a few minutes (ADR-314 anti-abuse cooldown).');
144
+ return { success: false };
145
+ }
146
+ output.printSuccess('Rate-limit flag cleared.');
147
+ return { success: true };
148
+ },
149
+ };
150
+ const POWER_SAVER_DISCLOSURE = [
151
+ 'Enabling power saver mode.',
152
+ '',
153
+ "Everyday requests will route through Cognitum's own difficulty-based",
154
+ 'router (cognitum-auto) instead of your Claude subscription directly —',
155
+ 'simple messages stay cheap, genuinely hard reasoning still escalates to',
156
+ 'a comparable frontier model. This is billed to YOUR OWN Cognitum',
157
+ 'account (cloud-routing), not sponsored/free capacity — a separate',
158
+ 'decision from sponsored downtime mode.',
159
+ '',
160
+ 'Disable anytime: ruflo proxy power-saver-disable',
161
+ ].join('\n');
162
+ const powerSaverEnableSub = {
163
+ name: 'power-saver-enable',
164
+ description: 'Opt into power saver mode — route everyday requests through your own Cognitum account (ADR-314)',
165
+ options: [
166
+ { name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
167
+ ],
168
+ action: async (ctx) => {
169
+ if (hasConsent('power-saver')) {
170
+ output.writeln('Power saver mode is already enabled.');
171
+ return { success: true, data: { alreadyEnabled: true } };
172
+ }
173
+ output.writeln(POWER_SAVER_DISCLOSURE);
174
+ output.writeln('');
175
+ if (!ctx.flags.yes) {
176
+ output.writeln('Re-run with --yes to confirm: ruflo proxy power-saver-enable --yes');
177
+ return { success: true, data: { confirmed: false } };
178
+ }
179
+ recordConsent('power-saver', true, 'proxy-power-saver-enable');
180
+ writePowerSaverConsentMirror(true);
181
+ recordFunnelEvent('power_saver_enabled', 'statusline', getInstalledCliVersion());
182
+ output.printSuccess('Power saver mode enabled.');
183
+ output.writeln('Flag it active with: ruflo settings notices quota-low');
184
+ output.writeln('Disable anytime: ruflo proxy power-saver-disable');
185
+ return { success: true, data: { confirmed: true } };
186
+ },
187
+ };
188
+ const powerSaverDisableSub = {
189
+ name: 'power-saver-disable',
190
+ description: 'Revoke power-saver consent and stop routing through Cognitum for cost savings',
191
+ action: async () => {
192
+ revokeConsent('power-saver', 'proxy-power-saver-disable');
193
+ writePowerSaverConsentMirror(false);
194
+ recordFunnelEvent('power_saver_disabled', 'statusline', getInstalledCliVersion());
195
+ output.printSuccess('Power saver mode disabled.');
196
+ return { success: true };
197
+ },
198
+ };
199
+ const powerSaverStatusSub = {
200
+ name: 'power-saver-status',
201
+ description: 'Show power-saver consent + quota-low flag state',
202
+ action: async () => {
203
+ const consented = hasConsent('power-saver');
204
+ const quotaLow = readQuotaLowStatus();
205
+ output.writeln(`Power saver consent: ${consented ? 'granted' : 'not granted'}`);
206
+ output.writeln(`Quota-low flag: ${quotaLow.low ? `set (since ${quotaLow.since})` : 'not set'}`);
207
+ if (quotaLow.low && !consented) {
208
+ output.writeln('');
209
+ output.writeln('You are flagged as running low but have not enabled power saver');
210
+ output.writeln('mode. Enable it with: ruflo proxy power-saver-enable --yes');
211
+ }
212
+ return { success: true, data: { consented, quotaLow } };
213
+ },
214
+ };
215
+ /** Convenience: clear the quota-low flag once you're back to normal. */
216
+ const powerSaverClearSub = {
217
+ name: 'power-saver-clear',
218
+ description: 'Clear the quota-low flag',
219
+ action: async () => {
220
+ const changed = clearQuotaLowStatus();
221
+ if (!changed) {
222
+ output.printError('Quota-low flag was just toggled — try again in a few minutes (ADR-314 anti-abuse cooldown).');
223
+ return { success: false };
224
+ }
225
+ output.printSuccess('Quota-low flag cleared.');
226
+ return { success: true };
227
+ },
228
+ };
229
+ const TRAINING_SHARE_DISCLOSURE = [
230
+ 'Enabling training-data sharing.',
231
+ '',
232
+ 'When a request runs on the sponsored plane, the proxy will attach a',
233
+ 'consent header telling Cognitum it may retain that interaction —',
234
+ '(post safety-scan) — as input to its MicroLoRA training pipeline',
235
+ '(meta-llm ADR-251), gated through its existing SHADOW/promotion-rule',
236
+ 'safety net; adaptations are never auto-served. This is content, not',
237
+ 'just metadata, and is entirely separate from sponsored-downtime',
238
+ 'consent — using free capacity never implicitly means this is on.',
239
+ '',
240
+ 'Declining has zero effect on sponsored-capacity access.',
241
+ '',
242
+ 'Disable anytime: ruflo proxy training-share-disable',
243
+ ].join('\n');
244
+ const trainingShareEnableSub = {
245
+ name: 'training-share-enable',
246
+ description: 'Opt into sharing sponsored-plane interaction content for meta-llm training (ADR-315)',
247
+ options: [
248
+ { name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
249
+ ],
250
+ action: async (ctx) => {
251
+ if (hasConsent('training-data-sharing')) {
252
+ output.writeln('Training-data sharing is already enabled.');
253
+ return { success: true, data: { alreadyEnabled: true } };
254
+ }
255
+ output.writeln(TRAINING_SHARE_DISCLOSURE);
256
+ output.writeln('');
257
+ if (!ctx.flags.yes) {
258
+ output.writeln('Re-run with --yes to confirm: ruflo proxy training-share-enable --yes');
259
+ return { success: true, data: { confirmed: false } };
260
+ }
261
+ recordConsent('training-data-sharing', true, 'proxy-training-share-enable');
262
+ writeTrainingShareConsentMirror(true);
263
+ recordFunnelEvent('training_share_enabled', 'statusline', getInstalledCliVersion());
264
+ output.printSuccess('Training-data sharing enabled.');
265
+ output.writeln('Only sponsored-plane requests carry the consent header. Disable anytime:');
266
+ output.writeln(' ruflo proxy training-share-disable');
267
+ return { success: true, data: { confirmed: true } };
268
+ },
269
+ };
270
+ const trainingShareDisableSub = {
271
+ name: 'training-share-disable',
272
+ description: 'Revoke training-data-sharing consent — stop sending the training consent header',
273
+ action: async () => {
274
+ revokeConsent('training-data-sharing', 'proxy-training-share-disable');
275
+ writeTrainingShareConsentMirror(false);
276
+ recordFunnelEvent('training_share_disabled', 'statusline', getInstalledCliVersion());
277
+ output.printSuccess('Training-data sharing disabled.');
278
+ return { success: true };
279
+ },
280
+ };
281
+ const trainingShareStatusSub = {
282
+ name: 'training-share-status',
283
+ description: 'Show training-data-sharing consent state',
284
+ action: async () => {
285
+ const consented = hasConsent('training-data-sharing');
286
+ output.writeln(`Training-data sharing consent: ${consented ? 'granted' : 'not granted'}`);
287
+ if (consented) {
288
+ output.writeln('Sponsored-plane requests carry X-Cognitum-Training-Consent: true.');
289
+ }
290
+ return { success: true, data: { consented } };
291
+ },
292
+ };
293
+ export const proxyCommand = {
294
+ name: 'proxy',
295
+ description: 'Meta LLM Proxy — sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
296
+ subcommands: [
297
+ sponsorEnableSub, sponsorDisableSub, sponsorStatusSub, sponsorClearSub,
298
+ powerSaverEnableSub, powerSaverDisableSub, powerSaverStatusSub, powerSaverClearSub,
299
+ trainingShareEnableSub, trainingShareDisableSub, trainingShareStatusSub,
300
+ ],
301
+ examples: [
302
+ { command: 'ruflo proxy sponsor-status', description: 'Show current sponsored-mode state' },
303
+ { command: 'ruflo proxy sponsor-enable --yes', description: 'Opt into sponsored downtime capacity' },
304
+ { command: 'ruflo proxy power-saver-enable --yes', description: 'Opt into power saver mode' },
305
+ { command: 'ruflo proxy training-share-enable --yes', description: 'Opt into training-data sharing (ADR-315)' },
306
+ ],
307
+ action: sponsorStatusSub.action,
308
+ };
309
+ export default proxyCommand;
310
+ //# sourceMappingURL=proxy.js.map
@@ -218,6 +218,35 @@ const scanCommand = {
218
218
  `Critical: ${criticalCount} High: ${highCount} Medium: ${mediumCount} Low: ${lowCount}`,
219
219
  `Total Issues: ${findings.length}`,
220
220
  ].join('\n'), 'Scan Summary');
221
+ // Persist the scan result so downstream consumers (the statusline's
222
+ // getSecurityStatus in funnel/local-signals.ts, which reads
223
+ // `.claude/security-scans/*.json`) reflect real scan state. Best-effort:
224
+ // a failed write must never fail the scan itself.
225
+ try {
226
+ const scanDirOut = path.join(path.resolve(target), '.claude', 'security-scans');
227
+ fs.mkdirSync(scanDirOut, { recursive: true });
228
+ const record = {
229
+ timestamp: new Date().toISOString(),
230
+ target,
231
+ depth,
232
+ type: scanType,
233
+ summary: {
234
+ critical: criticalCount,
235
+ high: highCount,
236
+ medium: mediumCount,
237
+ low: lowCount,
238
+ total: findings.length,
239
+ },
240
+ findings,
241
+ };
242
+ // Deterministic name keyed on scan config so repeated runs overwrite
243
+ // rather than accumulate stale reports.
244
+ const outFile = path.join(scanDirOut, `scan-${scanType}-${depth}.json`);
245
+ fs.writeFileSync(outFile, JSON.stringify(record, null, 2));
246
+ }
247
+ catch {
248
+ // Persistence is advisory only — ignore write failures.
249
+ }
221
250
  // Auto-fix if requested
222
251
  if (fix && criticalCount + highCount > 0) {
223
252
  output.writeln();