@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.
- package/.claude/.proven-config-version +1 -0
- package/.claude/helpers/.helpers-version +1 -1
- package/.claude/helpers/helpers.manifest.json +5 -4
- package/.claude/helpers/hook-handler.cjs +129 -0
- package/.claude/helpers/statusline.cjs +771 -505
- package/.claude/proven-config.json +42 -0
- package/catalog-manifest.json +12 -0
- package/dist/src/commands/advisor.d.ts +15 -0
- package/dist/src/commands/advisor.js +94 -0
- package/dist/src/commands/doctor.js +151 -32
- package/dist/src/commands/funnel.d.ts +13 -0
- package/dist/src/commands/funnel.js +103 -0
- package/dist/src/commands/hooks.js +112 -46
- package/dist/src/commands/index.js +10 -0
- package/dist/src/commands/init.js +44 -0
- package/dist/src/commands/proxy.d.ts +21 -0
- package/dist/src/commands/proxy.js +310 -0
- package/dist/src/commands/security.js +29 -0
- package/dist/src/commands/settings.d.ts +19 -0
- package/dist/src/commands/settings.js +180 -0
- package/dist/src/commands/version.d.ts +42 -0
- package/dist/src/commands/version.js +106 -0
- package/dist/src/funnel/advisor-tip.d.ts +58 -0
- package/dist/src/funnel/advisor-tip.js +92 -0
- package/dist/src/funnel/attribution.d.ts +37 -0
- package/dist/src/funnel/attribution.js +101 -0
- package/dist/src/funnel/consent.d.ts +22 -0
- package/dist/src/funnel/consent.js +55 -0
- package/dist/src/funnel/credit-errors.d.ts +31 -0
- package/dist/src/funnel/credit-errors.js +88 -0
- package/dist/src/funnel/credit-notifier.d.ts +44 -0
- package/dist/src/funnel/credit-notifier.js +74 -0
- package/dist/src/funnel/disclosure.d.ts +46 -0
- package/dist/src/funnel/disclosure.js +97 -0
- package/dist/src/funnel/enrollment.d.ts +36 -0
- package/dist/src/funnel/enrollment.js +64 -0
- package/dist/src/funnel/environment.d.ts +17 -0
- package/dist/src/funnel/environment.js +39 -0
- package/dist/src/funnel/event-transport.d.ts +51 -0
- package/dist/src/funnel/event-transport.js +199 -0
- package/dist/src/funnel/events.d.ts +42 -0
- package/dist/src/funnel/events.js +150 -0
- package/dist/src/funnel/index.d.ts +20 -0
- package/dist/src/funnel/index.js +20 -0
- package/dist/src/funnel/insights.d.ts +49 -0
- package/dist/src/funnel/insights.js +120 -0
- package/dist/src/funnel/local-signals.d.ts +15 -0
- package/dist/src/funnel/local-signals.js +68 -0
- package/dist/src/funnel/message-transport.d.ts +51 -0
- package/dist/src/funnel/message-transport.js +149 -0
- package/dist/src/funnel/messages.d.ts +55 -0
- package/dist/src/funnel/messages.js +160 -0
- package/dist/src/funnel/power-saver-notifier.d.ts +44 -0
- package/dist/src/funnel/power-saver-notifier.js +92 -0
- package/dist/src/funnel/precedence.d.ts +16 -0
- package/dist/src/funnel/precedence.js +85 -0
- package/dist/src/funnel/promo.d.ts +41 -0
- package/dist/src/funnel/promo.js +144 -0
- package/dist/src/funnel/rate-limit-notifier.d.ts +55 -0
- package/dist/src/funnel/rate-limit-notifier.js +102 -0
- package/dist/src/funnel/rotation.d.ts +19 -0
- package/dist/src/funnel/rotation.js +70 -0
- package/dist/src/funnel/state.d.ts +13 -0
- package/dist/src/funnel/state.js +52 -0
- package/dist/src/funnel/toggle-cooldown.d.ts +17 -0
- package/dist/src/funnel/toggle-cooldown.js +32 -0
- package/dist/src/funnel/types.d.ts +86 -0
- package/dist/src/funnel/types.js +26 -0
- package/dist/src/init/executor.js +25 -19
- package/dist/src/init/helper-refresh.d.ts +33 -2
- package/dist/src/init/helper-refresh.js +102 -12
- package/dist/src/init/helpers-generator.js +52 -0
- package/dist/src/init/mcp-generator.js +11 -6
- package/dist/src/init/statusline-generator.js +282 -116
- package/dist/src/services/daemon-autostart.js +28 -5
- package/dist/src/services/fable-harness.d.ts +39 -0
- package/dist/src/services/fable-harness.js +41 -0
- package/package.json +16 -4
- package/plugins/ruflo-metaharness/scripts/smoke.sh +18 -5
- package/dist/src/ruvector/lattice-wasm.d.ts +0 -14
- package/dist/src/ruvector/lattice-wasm.js +0 -144
- package/plugins/ruflo-metaharness/.claude-flow/data/pending-insights.jsonl +0 -5
- 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
|
-
//
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
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();
|