claude-flow 3.32.2 → 3.32.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/statusline.cjs +38 -38
- package/.claude-plugin/scripts/ruflo-hook.cjs +2 -2
- package/.claude-plugin/scripts/ruflo-hook.sh +17 -17
- package/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/auth/client.d.ts +89 -0
- package/v3/@claude-flow/cli/dist/src/auth/client.js +242 -0
- package/v3/@claude-flow/cli/dist/src/auth/constants.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/auth/constants.js +7 -0
- package/v3/@claude-flow/cli/dist/src/auth/scopes.d.ts +14 -0
- package/v3/@claude-flow/cli/dist/src/auth/scopes.js +21 -0
- package/v3/@claude-flow/cli/dist/src/auth/security-bridge.d.ts +36 -0
- package/v3/@claude-flow/cli/dist/src/auth/security-bridge.js +42 -0
- package/v3/@claude-flow/cli/dist/src/auth/session.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/auth/session.js +32 -0
- package/v3/@claude-flow/cli/dist/src/auth/state.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/auth/state.js +53 -0
- package/v3/@claude-flow/cli/dist/src/auth/types.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/auth/types.js +11 -0
- package/v3/@claude-flow/cli/dist/src/commands/auth.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/commands/auth.js +244 -0
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +211 -4
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.js +277 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy.js +97 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +11 -3
- package/v3/@claude-flow/cli/dist/src/proxy/install.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/proxy/install.js +135 -0
- package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.js +249 -0
- package/v3/@claude-flow/cli/dist/src/proxy/paths.d.ts +34 -0
- package/v3/@claude-flow/cli/dist/src/proxy/paths.js +70 -0
- package/v3/@claude-flow/cli/dist/src/proxy/release.d.ts +47 -0
- package/v3/@claude-flow/cli/dist/src/proxy/release.js +138 -0
- package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.d.ts +5 -0
- package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.js +61 -0
- package/v3/@claude-flow/cli/dist/src/proxy/verify.d.ts +44 -0
- package/v3/@claude-flow/cli/dist/src/proxy/verify.js +68 -0
- package/v3/@claude-flow/cli/package.json +3 -3
- package/.claude/.proven-config-version +0 -1
- package/.claude/proven-config.json +0 -42
|
@@ -978,7 +978,7 @@ async function checkFunnel() {
|
|
|
978
978
|
}
|
|
979
979
|
}
|
|
980
980
|
/** Meta LLM Proxy — sponsored-downtime health (ADR-313). */
|
|
981
|
-
async function
|
|
981
|
+
async function checkProxySponsoredConsent() {
|
|
982
982
|
try {
|
|
983
983
|
const { funnelStateDir, hasConsent, readRateLimitStatus, lastRecordedEvent } = await import('../funnel/index.js');
|
|
984
984
|
const dir = funnelStateDir();
|
|
@@ -1019,6 +1019,208 @@ async function checkProxy() {
|
|
|
1019
1019
|
};
|
|
1020
1020
|
}
|
|
1021
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Binary presence + tamper check (ADR-307). Deliberately NEVER spawns the
|
|
1024
|
+
* binary to probe a version — confirmed empirically (2026-07-16) that
|
|
1025
|
+
* `meta-proxy` has no `--version`/`--help` flag and starts the live server
|
|
1026
|
+
* as a side effect of ANY invocation, which a doctor health check must never
|
|
1027
|
+
* do. Version info instead comes from install-manifest.json (written at
|
|
1028
|
+
* install time) and, once running, the proxy's own `/status` endpoint via
|
|
1029
|
+
* checkProxyProcess below.
|
|
1030
|
+
*/
|
|
1031
|
+
async function checkProxyBinary() {
|
|
1032
|
+
const NAME = 'Meta LLM Proxy binary (ADR-307)';
|
|
1033
|
+
try {
|
|
1034
|
+
const { proxyBinaryPath, proxyInstallManifestPath } = await import('../proxy/paths.js');
|
|
1035
|
+
const binPath = proxyBinaryPath();
|
|
1036
|
+
if (!existsSync(binPath)) {
|
|
1037
|
+
return { name: NAME, status: 'warn', message: 'not installed', fix: 'ruflo proxy install' };
|
|
1038
|
+
}
|
|
1039
|
+
const manifestPath = proxyInstallManifestPath();
|
|
1040
|
+
if (!existsSync(manifestPath)) {
|
|
1041
|
+
return {
|
|
1042
|
+
name: NAME,
|
|
1043
|
+
status: 'warn',
|
|
1044
|
+
message: 'binary present but no install-manifest.json — provenance unknown (installed outside `ruflo proxy install`?)',
|
|
1045
|
+
fix: 'ruflo proxy update --release <x.y.z>',
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
1049
|
+
const liveSha = createHash('sha256').update(readFileSync(binPath)).digest('hex');
|
|
1050
|
+
if (liveSha !== manifest.sha256) {
|
|
1051
|
+
return {
|
|
1052
|
+
name: NAME,
|
|
1053
|
+
status: 'fail',
|
|
1054
|
+
message: `binary sha256 does not match the recorded install manifest — possible tampering or a manual overwrite (expected ${manifest.sha256.slice(0, 12)}…, got ${liveSha.slice(0, 12)}…)`,
|
|
1055
|
+
fix: 'ruflo proxy update --release <x.y.z>',
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return { name: NAME, status: 'pass', message: `v${manifest.version}, signature-verified at install (${manifest.verifiedAt})` };
|
|
1059
|
+
}
|
|
1060
|
+
catch (err) {
|
|
1061
|
+
return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
/** PID-file liveness (ADR-307) — mirrors daemon.ts's signal-0 probe pattern. */
|
|
1065
|
+
async function checkProxyProcess() {
|
|
1066
|
+
const NAME = 'Meta LLM Proxy process (ADR-307)';
|
|
1067
|
+
try {
|
|
1068
|
+
const { proxyPidFilePath } = await import('../proxy/paths.js');
|
|
1069
|
+
const pidPath = proxyPidFilePath();
|
|
1070
|
+
if (!existsSync(pidPath)) {
|
|
1071
|
+
return { name: NAME, status: 'warn', message: 'not running (no PID file)', fix: 'ruflo proxy start' };
|
|
1072
|
+
}
|
|
1073
|
+
const pidRaw = readFileSync(pidPath, 'utf-8').trim();
|
|
1074
|
+
const pid = parseInt(pidRaw, 10);
|
|
1075
|
+
if (!Number.isFinite(pid)) {
|
|
1076
|
+
return { name: NAME, status: 'warn', message: `PID file is malformed: ${JSON.stringify(pidRaw)}`, fix: 'ruflo proxy stop && ruflo proxy start' };
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
process.kill(pid, 0); // signal-0 liveness probe — throws if the process is dead
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
return { name: NAME, status: 'warn', message: `PID file points at ${pid}, which is not running — stale PID file`, fix: 'ruflo proxy start' };
|
|
1083
|
+
}
|
|
1084
|
+
// Live version-compat + data-plane info, ONLY once PID liveness already
|
|
1085
|
+
// confirmed a process is running (never spawns anything — see
|
|
1086
|
+
// checkProxyBinary's comment on why probing via process launch is unsafe).
|
|
1087
|
+
// GET /status shape confirmed against the real v0.1.0 binary:
|
|
1088
|
+
// {"version","data_plane","bind","sponsored_available","proxy_token_valid"}.
|
|
1089
|
+
const { proxyConfigPath, proxyTokenPath, proxyInstallManifestPath } = await import('../proxy/paths.js');
|
|
1090
|
+
const bindMatch = existsSync(proxyConfigPath())
|
|
1091
|
+
? readFileSync(proxyConfigPath(), 'utf-8').match(/^bind\s*=\s*"([^"]*)"/m)
|
|
1092
|
+
: null;
|
|
1093
|
+
const bind = bindMatch ? bindMatch[1] : '127.0.0.1:11435';
|
|
1094
|
+
let token;
|
|
1095
|
+
try {
|
|
1096
|
+
token = readFileSync(proxyTokenPath(), 'utf-8').trim();
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
return { name: NAME, status: 'pass', message: `running (pid ${pid}); no proxy-token to query /status` };
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const controller = new AbortController();
|
|
1103
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
1104
|
+
const resp = await fetch(`http://${bind}/status`, {
|
|
1105
|
+
headers: { authorization: `Bearer ${token}` },
|
|
1106
|
+
signal: controller.signal,
|
|
1107
|
+
});
|
|
1108
|
+
clearTimeout(timer);
|
|
1109
|
+
if (!resp.ok) {
|
|
1110
|
+
return { name: NAME, status: 'warn', message: `running (pid ${pid}); /status returned HTTP ${resp.status}` };
|
|
1111
|
+
}
|
|
1112
|
+
const body = (await resp.json());
|
|
1113
|
+
let versionNote = '';
|
|
1114
|
+
if (existsSync(proxyInstallManifestPath())) {
|
|
1115
|
+
const manifest = JSON.parse(readFileSync(proxyInstallManifestPath(), 'utf-8'));
|
|
1116
|
+
if (manifest.version && body.version && manifest.version !== body.version) {
|
|
1117
|
+
return {
|
|
1118
|
+
name: NAME,
|
|
1119
|
+
status: 'warn',
|
|
1120
|
+
message: `running (pid ${pid}) reports v${body.version}, but the installed binary is v${manifest.version} — a stale process from a previous version?`,
|
|
1121
|
+
fix: 'ruflo proxy stop && ruflo proxy start',
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
versionNote = body.version ? ` v${body.version}` : '';
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
name: NAME,
|
|
1128
|
+
status: 'pass',
|
|
1129
|
+
message: `running (pid ${pid})${versionNote}; data plane: ${body.data_plane ?? 'unknown'}`,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
// Live process but /status unreachable (still starting up, or a
|
|
1134
|
+
// network hiccup) — not a failure, PID liveness already passed.
|
|
1135
|
+
return {
|
|
1136
|
+
name: NAME,
|
|
1137
|
+
status: 'pass',
|
|
1138
|
+
message: `running (pid ${pid}); /status unreachable (${err instanceof Error ? err.message : String(err)})`,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
catch (err) {
|
|
1143
|
+
return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
/** Non-loopback bind exposure warning (ADR-307's mandated startup warning, surfaced in doctor too). */
|
|
1147
|
+
async function checkProxyBindAddress() {
|
|
1148
|
+
const NAME = 'Meta LLM Proxy bind address (ADR-307)';
|
|
1149
|
+
try {
|
|
1150
|
+
const { proxyConfigPath, isLoopbackBind } = await import('../proxy/paths.js');
|
|
1151
|
+
const cfgPath = proxyConfigPath();
|
|
1152
|
+
if (!existsSync(cfgPath)) {
|
|
1153
|
+
return { name: NAME, status: 'pass', message: 'no config file yet — defaults to loopback-only (127.0.0.1:11435)' };
|
|
1154
|
+
}
|
|
1155
|
+
const raw = readFileSync(cfgPath, 'utf-8');
|
|
1156
|
+
const match = raw.match(/^bind\s*=\s*"([^"]*)"/m);
|
|
1157
|
+
const bind = match ? match[1] : '127.0.0.1:11435';
|
|
1158
|
+
if (!isLoopbackBind(bind)) {
|
|
1159
|
+
return {
|
|
1160
|
+
name: NAME,
|
|
1161
|
+
status: 'warn',
|
|
1162
|
+
message: `bound to non-loopback address ${bind} — this exposes the proxy to your network`,
|
|
1163
|
+
fix: 'Set bind back to 127.0.0.1:<port> in proxy-config.toml unless external exposure is intended',
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
return { name: NAME, status: 'pass', message: `loopback-only (${bind})` };
|
|
1167
|
+
}
|
|
1168
|
+
catch (err) {
|
|
1169
|
+
return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/** `ruflo auth` health (ADR-306). Warn (never fail) on absence — auth is never required for core functionality. */
|
|
1173
|
+
async function checkAuth() {
|
|
1174
|
+
const NAME = 'Cognitum identity (ADR-306)';
|
|
1175
|
+
try {
|
|
1176
|
+
const { listProfiles } = await import('../auth/state.js');
|
|
1177
|
+
const { domainForScope } = await import('../auth/scopes.js');
|
|
1178
|
+
const { hasConsent } = await import('../funnel/index.js');
|
|
1179
|
+
const { profiles } = listProfiles();
|
|
1180
|
+
if (profiles.length === 0) {
|
|
1181
|
+
return { name: NAME, status: 'warn', message: 'not logged in', fix: 'ruflo auth login' };
|
|
1182
|
+
}
|
|
1183
|
+
let keychainAvailable = 'unknown';
|
|
1184
|
+
try {
|
|
1185
|
+
const sec = await import('@claude-flow/security');
|
|
1186
|
+
keychainAvailable = await (await sec.createKeychainAdapter()).isAvailable();
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
keychainAvailable = 'unknown'; // security package unavailable — surfaced by checkProxyBinary's sibling concerns, not duplicated here
|
|
1190
|
+
}
|
|
1191
|
+
// Scope-vs-receipt consistency check (ADR-306: "fail-closed... reports").
|
|
1192
|
+
// Unlike every other check in this file, a violation here is a FAIL, not
|
|
1193
|
+
// a warn — a scope present without a matching consent receipt is exactly
|
|
1194
|
+
// the condition ADR-306 says must never silently pass.
|
|
1195
|
+
const violations = [];
|
|
1196
|
+
for (const p of profiles) {
|
|
1197
|
+
for (const scope of p.scopes) {
|
|
1198
|
+
const domain = domainForScope(scope);
|
|
1199
|
+
if (domain && !hasConsent(domain))
|
|
1200
|
+
violations.push(`${p.profile}: ${scope}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (violations.length > 0) {
|
|
1204
|
+
return {
|
|
1205
|
+
name: NAME,
|
|
1206
|
+
status: 'fail',
|
|
1207
|
+
message: `scope granted without a matching consent receipt: ${violations.join(', ')}`,
|
|
1208
|
+
fix: 'ruflo auth logout && ruflo auth login',
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
const names = profiles.map((p) => p.profile).join(', ');
|
|
1212
|
+
const sessionOnly = profiles.filter((p) => !p.keychainRef).map((p) => p.profile);
|
|
1213
|
+
const parts = [`profiles: ${names}`];
|
|
1214
|
+
if (keychainAvailable === false)
|
|
1215
|
+
parts.push('keychain backend unreachable — falling back to session-only tokens');
|
|
1216
|
+
if (sessionOnly.length > 0)
|
|
1217
|
+
parts.push(`session-only (no persisted refresh token): ${sessionOnly.join(', ')}`);
|
|
1218
|
+
return { name: NAME, status: 'pass', message: parts.join('; ') };
|
|
1219
|
+
}
|
|
1220
|
+
catch (err) {
|
|
1221
|
+
return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1022
1224
|
async function checkMetaharnessIntegration() {
|
|
1023
1225
|
// Locate plugins dir.
|
|
1024
1226
|
//
|
|
@@ -1395,7 +1597,7 @@ export const doctorCommand = {
|
|
|
1395
1597
|
{
|
|
1396
1598
|
name: 'component',
|
|
1397
1599
|
short: 'c',
|
|
1398
|
-
description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, metaharness)',
|
|
1600
|
+
description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, metaharness)',
|
|
1399
1601
|
type: 'string'
|
|
1400
1602
|
},
|
|
1401
1603
|
{
|
|
@@ -1525,7 +1727,8 @@ export const doctorCommand = {
|
|
|
1525
1727
|
checkMetaharness, // ADR-150 — MetaHarness upstream package
|
|
1526
1728
|
checkMetaharnessIntegration, // iter 45 — ruflo-side integration layer
|
|
1527
1729
|
checkFunnel, // ADR-305 — effective funnel state + deciding precedence source
|
|
1528
|
-
|
|
1730
|
+
checkProxySponsoredConsent, // ADR-313 — Meta LLM Proxy sponsored-downtime health
|
|
1731
|
+
checkAuth, // ADR-306 — Cognitum identity (warn-only; never fails bare `ruflo doctor`)
|
|
1529
1732
|
];
|
|
1530
1733
|
// #2677: `--component memory` now runs the whole memory-health suite,
|
|
1531
1734
|
// not just the existence check. Values can be a single check or an
|
|
@@ -1563,7 +1766,11 @@ export const doctorCommand = {
|
|
|
1563
1766
|
'metaharness': checkMetaharness, // ADR-150 — upstream package
|
|
1564
1767
|
'metaharness-integration': checkMetaharnessIntegration, // iter 45 — ruflo-side
|
|
1565
1768
|
'funnel': checkFunnel, // ADR-305
|
|
1566
|
-
'
|
|
1769
|
+
// ADR-307 — deep-dive array, same pattern as 'memory' above: the cheap
|
|
1770
|
+
// sponsored-consent check first, then binary/process/bind in the order
|
|
1771
|
+
// a user would actually debug them (is it installed? running? exposed?).
|
|
1772
|
+
'proxy': [checkProxySponsoredConsent, checkProxyBinary, checkProxyProcess, checkProxyBindAddress],
|
|
1773
|
+
'auth': checkAuth, // ADR-306
|
|
1567
1774
|
};
|
|
1568
1775
|
let checksToRun = allChecks;
|
|
1569
1776
|
if (component && componentMap[component]) {
|
|
@@ -79,6 +79,8 @@ const commandLoaders = {
|
|
|
79
79
|
// User-facing preferences wrapper (ADR-311 copy discipline — no "funnel" in
|
|
80
80
|
// the user surface). Forwards to the funnel primitives internally.
|
|
81
81
|
settings: () => import('./settings.js'),
|
|
82
|
+
// Cognitum identity — login/logout/status (ADR-306)
|
|
83
|
+
auth: () => import('./auth.js'),
|
|
82
84
|
// Meta LLM Proxy — sponsored downtime capacity (ADR-304/307/313)
|
|
83
85
|
proxy: () => import('./proxy.js'),
|
|
84
86
|
// Fable co-pilot advisor tip in the statusline insight ticker (ADR-316)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo proxy install|start|stop|status|logs|update|uninstall` (ADR-307) —
|
|
3
|
+
* the full lifecycle command set, kept in its own file so
|
|
4
|
+
* src/commands/proxy.ts (the ADR-313/314/315 consent subcommands) stays
|
|
5
|
+
* under the repo's 500-line-per-file convention. Merged into one
|
|
6
|
+
* `proxyCommand` in proxy.ts.
|
|
7
|
+
*/
|
|
8
|
+
import type { Command } from '../types.js';
|
|
9
|
+
import { type ProxyStatus } from '../proxy/lifecycle.js';
|
|
10
|
+
/** Pinned and reviewed; later upgrades remain explicit commands. */
|
|
11
|
+
export declare const DEFAULT_PROXY_RELEASE = "0.4.0";
|
|
12
|
+
/**
|
|
13
|
+
* Human-oriented next steps for `ruflo proxy` and `ruflo proxy status`.
|
|
14
|
+
* Keep this independent of the command framework so the state-specific
|
|
15
|
+
* guidance has a small, direct regression-test surface.
|
|
16
|
+
*/
|
|
17
|
+
export declare function proxyConsoleGuidance(status: ProxyStatus): string[];
|
|
18
|
+
export declare function printProxyConsoleGuidance(status: ProxyStatus): void;
|
|
19
|
+
export declare const proxyLifecycleSubcommands: Command[];
|
|
20
|
+
//# sourceMappingURL=proxy-lifecycle.d.ts.map
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo proxy install|start|stop|status|logs|update|uninstall` (ADR-307) —
|
|
3
|
+
* the full lifecycle command set, kept in its own file so
|
|
4
|
+
* src/commands/proxy.ts (the ADR-313/314/315 consent subcommands) stays
|
|
5
|
+
* under the repo's 500-line-per-file convention. Merged into one
|
|
6
|
+
* `proxyCommand` in proxy.ts.
|
|
7
|
+
*/
|
|
8
|
+
import { output } from '../output.js';
|
|
9
|
+
import { hasConsent, recordConsent, revokeConsent } from '../funnel/index.js';
|
|
10
|
+
import { installProxy, uninstallProxy } from '../proxy/install.js';
|
|
11
|
+
import { startForeground, startBackground, stopProxy, getProxyStatus, readProxyLogTail, watchProxyLog, ProxyNotInstalledError, ProxyAlreadyRunningError, } from '../proxy/lifecycle.js';
|
|
12
|
+
import { proxyTokenPath } from '../proxy/paths.js';
|
|
13
|
+
import { removeInjectedToken, startTokenRefreshPump } from '../proxy/token-bridge.js';
|
|
14
|
+
/** Pinned and reviewed; later upgrades remain explicit commands. */
|
|
15
|
+
export const DEFAULT_PROXY_RELEASE = '0.4.0';
|
|
16
|
+
const PROXY_COMMAND = 'npx ruflo@latest proxy';
|
|
17
|
+
const AUTH_COMMAND = 'npx ruflo@latest auth';
|
|
18
|
+
/**
|
|
19
|
+
* Human-oriented next steps for `ruflo proxy` and `ruflo proxy status`.
|
|
20
|
+
* Keep this independent of the command framework so the state-specific
|
|
21
|
+
* guidance has a small, direct regression-test surface.
|
|
22
|
+
*/
|
|
23
|
+
export function proxyConsoleGuidance(status) {
|
|
24
|
+
if (!status.installed) {
|
|
25
|
+
return [
|
|
26
|
+
'',
|
|
27
|
+
'Next step',
|
|
28
|
+
` ${PROXY_COMMAND} install --yes Install signed Meta-Proxy v${DEFAULT_PROXY_RELEASE}`,
|
|
29
|
+
'',
|
|
30
|
+
'After installation, start it in the background:',
|
|
31
|
+
` ${PROXY_COMMAND} start --service`,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
if (!status.running) {
|
|
35
|
+
return [
|
|
36
|
+
'',
|
|
37
|
+
'Next step',
|
|
38
|
+
` ${PROXY_COMMAND} start --service Start the proxy in the background`,
|
|
39
|
+
'',
|
|
40
|
+
'Foreground mode (shows live logs; Ctrl+C stops it):',
|
|
41
|
+
` ${PROXY_COMMAND} start`,
|
|
42
|
+
'',
|
|
43
|
+
'Optional: enable Cognitum cloud routing (local-only is the default):',
|
|
44
|
+
` ${AUTH_COMMAND} login`,
|
|
45
|
+
` ${PROXY_COMMAND} config --cloud --yes`,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
'',
|
|
50
|
+
'Meta Proxy is ready.',
|
|
51
|
+
` Logs: ${PROXY_COMMAND} logs`,
|
|
52
|
+
` Stop: ${PROXY_COMMAND} stop`,
|
|
53
|
+
'',
|
|
54
|
+
'Optional: enable Cognitum cloud routing (local-only is the default):',
|
|
55
|
+
` ${AUTH_COMMAND} login`,
|
|
56
|
+
` ${PROXY_COMMAND} config --cloud --yes`,
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
export function printProxyConsoleGuidance(status) {
|
|
60
|
+
for (const line of proxyConsoleGuidance(status))
|
|
61
|
+
output.writeln(line);
|
|
62
|
+
}
|
|
63
|
+
const INSTALL_DISCLOSURE = [
|
|
64
|
+
'Installing the Meta LLM Proxy (ADR-304/307).',
|
|
65
|
+
'',
|
|
66
|
+
'This downloads a separately-released Rust binary (Ed25519-signature and',
|
|
67
|
+
'checksum verified before anything is written to disk) and runs it as a',
|
|
68
|
+
'local process bound to 127.0.0.1 only. The proxy routes to LOCAL',
|
|
69
|
+
'backends by default — no prompt leaves this machine. Cloud routing is a',
|
|
70
|
+
'separate, explicit opt-in (`ruflo proxy config --cloud`), never enabled',
|
|
71
|
+
'by install alone.',
|
|
72
|
+
'',
|
|
73
|
+
'Uninstall anytime: ruflo proxy uninstall',
|
|
74
|
+
].join('\n');
|
|
75
|
+
const installSub = {
|
|
76
|
+
name: 'install',
|
|
77
|
+
description: 'Download, verify, and install the meta-proxy binary (ADR-307)',
|
|
78
|
+
options: [
|
|
79
|
+
// NOT named 'version' — index.ts:107 globally intercepts any --version/-v
|
|
80
|
+
// anywhere in argv (`if (flags.version || flags.V) { showVersion(); return; }`)
|
|
81
|
+
// BEFORE subcommand dispatch, regardless of which command defines it. A
|
|
82
|
+
// subcommand-local --version is silently swallowed by the CLI's own
|
|
83
|
+
// `ruflo --version` handling — confirmed the hard way in E2E testing.
|
|
84
|
+
{ name: 'release', description: `Release version to install (default: ${DEFAULT_PROXY_RELEASE})`, type: 'string' },
|
|
85
|
+
{ name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
|
|
86
|
+
],
|
|
87
|
+
action: async (ctx) => {
|
|
88
|
+
// Resolve the reviewed default before the consent gate. An explicit empty
|
|
89
|
+
// override still fails below without recording a consent receipt.
|
|
90
|
+
const version = typeof ctx.flags.release === 'string' ? ctx.flags.release : DEFAULT_PROXY_RELEASE;
|
|
91
|
+
if (!version) {
|
|
92
|
+
output.printError('ruflo proxy install requires --release <x.y.z> — there is no version-discovery ' +
|
|
93
|
+
'endpoint yet (see the plan doc for the tracked follow-up). Find the latest at ' +
|
|
94
|
+
'the release channel and pass it explicitly.');
|
|
95
|
+
return { success: false, exitCode: 1 };
|
|
96
|
+
}
|
|
97
|
+
if (!hasConsent('proxy-install')) {
|
|
98
|
+
output.writeln(INSTALL_DISCLOSURE);
|
|
99
|
+
output.writeln('');
|
|
100
|
+
const confirmed = Boolean(ctx.flags.yes);
|
|
101
|
+
if (!confirmed) {
|
|
102
|
+
output.writeln(`Re-run with --yes to confirm: ruflo proxy install --yes (installs ${version})`);
|
|
103
|
+
return { success: true, data: { confirmed: false } };
|
|
104
|
+
}
|
|
105
|
+
recordConsent('proxy-install', true, 'proxy-install');
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const spinner = output.createSpinner({ text: `Installing meta-proxy ${version}...`, spinner: 'dots' });
|
|
109
|
+
spinner.start();
|
|
110
|
+
const result = await installProxy({ version, log: (line) => spinner.setText(line) });
|
|
111
|
+
spinner.succeed(`meta-proxy ${version} installed`);
|
|
112
|
+
output.writeln(` binary: ${result.binaryPath}`);
|
|
113
|
+
output.writeln(` sha256: ${result.sha256}`);
|
|
114
|
+
return { success: true, data: result };
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
118
|
+
output.printError('Install failed', message);
|
|
119
|
+
return { success: false, message, exitCode: 1 };
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const updateSub = {
|
|
124
|
+
name: 'update',
|
|
125
|
+
description: 'Re-verify and replace the installed binary with a specific version (never automatic)',
|
|
126
|
+
options: [{ name: 'release', description: 'Release version to install', type: 'string', required: true }],
|
|
127
|
+
action: async (ctx) => {
|
|
128
|
+
const version = typeof ctx.flags.release === 'string' ? ctx.flags.release : undefined;
|
|
129
|
+
if (!version) {
|
|
130
|
+
output.printError('ruflo proxy update requires --release <x.y.z>');
|
|
131
|
+
return { success: false, exitCode: 1 };
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const spinner = output.createSpinner({ text: `Updating meta-proxy to ${version}...`, spinner: 'dots' });
|
|
135
|
+
spinner.start();
|
|
136
|
+
const result = await installProxy({ version, log: (line) => spinner.setText(line) });
|
|
137
|
+
spinner.succeed(`meta-proxy updated to ${version}`);
|
|
138
|
+
output.writeln(` binary: ${result.binaryPath}`);
|
|
139
|
+
return { success: true, data: result };
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
143
|
+
output.printError('Update failed', message);
|
|
144
|
+
return { success: false, message, exitCode: 1 };
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const startSub = {
|
|
149
|
+
name: 'start',
|
|
150
|
+
description: 'Start meta-proxy (foreground by default; --service to detach)',
|
|
151
|
+
options: [
|
|
152
|
+
{ name: 'service', description: 'Run detached (background), survives terminal close but not a reboot — full OS-service registration is not yet implemented', type: 'boolean', default: false },
|
|
153
|
+
],
|
|
154
|
+
action: async (ctx) => {
|
|
155
|
+
const service = Boolean(ctx.flags.service);
|
|
156
|
+
try {
|
|
157
|
+
if (service) {
|
|
158
|
+
const { pid } = await startBackground();
|
|
159
|
+
output.printSuccess(`meta-proxy started in the background (pid ${pid})`);
|
|
160
|
+
output.writeln(' Note: survives this terminal closing, but not a reboot — OS-service registration is not yet implemented.');
|
|
161
|
+
output.writeln(' Logs: ruflo proxy logs');
|
|
162
|
+
return { success: true, data: { pid } };
|
|
163
|
+
}
|
|
164
|
+
output.writeln('Starting meta-proxy in the foreground — press Ctrl+C to stop.');
|
|
165
|
+
await startTokenRefreshPump();
|
|
166
|
+
await startForeground(); // never returns normally
|
|
167
|
+
return { success: true };
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
if (e instanceof ProxyNotInstalledError || e instanceof ProxyAlreadyRunningError) {
|
|
171
|
+
output.printError(e.message);
|
|
172
|
+
return { success: false, message: e.message, exitCode: 1 };
|
|
173
|
+
}
|
|
174
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
175
|
+
output.printError('Failed to start meta-proxy', message);
|
|
176
|
+
return { success: false, message, exitCode: 1 };
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const superviseSub = {
|
|
181
|
+
name: 'supervise',
|
|
182
|
+
description: 'Internal detached supervisor for token refresh and meta-proxy lifecycle',
|
|
183
|
+
action: async () => {
|
|
184
|
+
await startTokenRefreshPump();
|
|
185
|
+
await startForeground(true);
|
|
186
|
+
return { success: true };
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
const stopSub = {
|
|
190
|
+
name: 'stop',
|
|
191
|
+
description: 'Stop a running meta-proxy process',
|
|
192
|
+
action: async () => {
|
|
193
|
+
const result = await stopProxy();
|
|
194
|
+
removeInjectedToken();
|
|
195
|
+
if (!result.wasRunning) {
|
|
196
|
+
output.writeln('meta-proxy was not running.');
|
|
197
|
+
return { success: true, data: result };
|
|
198
|
+
}
|
|
199
|
+
output.printSuccess(`meta-proxy stopped (was pid ${result.pid})`);
|
|
200
|
+
return { success: true, data: result };
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
const statusSub = {
|
|
204
|
+
name: 'status',
|
|
205
|
+
description: 'Show meta-proxy install + process status',
|
|
206
|
+
options: [{ name: 'json', description: 'Machine-readable output', type: 'boolean', default: false }],
|
|
207
|
+
action: async (ctx) => {
|
|
208
|
+
const status = getProxyStatus();
|
|
209
|
+
if (ctx.flags.json) {
|
|
210
|
+
output.printJson(status);
|
|
211
|
+
return { success: true, data: status };
|
|
212
|
+
}
|
|
213
|
+
output.writeln('Meta Proxy');
|
|
214
|
+
output.writeln(` Installation: ${status.installed ? 'ready' : 'not installed'}`);
|
|
215
|
+
output.writeln(` Process: ${status.running ? `running (pid ${status.pid})` : 'not running'}`);
|
|
216
|
+
if (status.stalePidFile)
|
|
217
|
+
output.writeln(' (a stale PID file was found and will be cleared on next start)');
|
|
218
|
+
printProxyConsoleGuidance(status);
|
|
219
|
+
return { success: true, data: status };
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const logsSub = {
|
|
223
|
+
name: 'logs',
|
|
224
|
+
description: 'Show meta-proxy --service logs',
|
|
225
|
+
options: [
|
|
226
|
+
{ name: 'follow', short: 'f', description: 'Stream new log lines as they arrive', type: 'boolean', default: false },
|
|
227
|
+
],
|
|
228
|
+
action: async (ctx) => {
|
|
229
|
+
if (ctx.flags.follow) {
|
|
230
|
+
output.writeln('Following meta-proxy logs — press Ctrl+C to stop.');
|
|
231
|
+
try {
|
|
232
|
+
const watcher = watchProxyLog((line) => output.writeln(line));
|
|
233
|
+
await new Promise((resolve) => {
|
|
234
|
+
process.on('SIGINT', () => {
|
|
235
|
+
watcher.close();
|
|
236
|
+
resolve();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
return { success: true };
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
243
|
+
output.printError(message);
|
|
244
|
+
return { success: false, message, exitCode: 1 };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const tail = readProxyLogTail();
|
|
248
|
+
if (!tail) {
|
|
249
|
+
output.writeln('No log content yet — meta-proxy has never been started in --service mode.');
|
|
250
|
+
return { success: true, data: { empty: true } };
|
|
251
|
+
}
|
|
252
|
+
output.writeln(tail);
|
|
253
|
+
return { success: true };
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
const uninstallSub = {
|
|
257
|
+
name: 'uninstall',
|
|
258
|
+
description: 'Stop the proxy (if running), remove the binary, token, and consent receipt',
|
|
259
|
+
action: async () => {
|
|
260
|
+
const status = getProxyStatus();
|
|
261
|
+
if (status.running) {
|
|
262
|
+
await stopProxy();
|
|
263
|
+
output.writeln('Stopped the running meta-proxy process.');
|
|
264
|
+
}
|
|
265
|
+
const removed = await uninstallProxy();
|
|
266
|
+
const { existsSync, unlinkSync } = await import('node:fs');
|
|
267
|
+
const tokenPath = proxyTokenPath();
|
|
268
|
+
if (existsSync(tokenPath))
|
|
269
|
+
unlinkSync(tokenPath);
|
|
270
|
+
removeInjectedToken();
|
|
271
|
+
revokeConsent('proxy-install', 'proxy-uninstall');
|
|
272
|
+
output.printSuccess(removed ? 'meta-proxy uninstalled.' : 'Nothing was installed — cleaned up any leftover state.');
|
|
273
|
+
return { success: true, data: { removed } };
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
export const proxyLifecycleSubcommands = [installSub, updateSub, startSub, superviseSub, stopSub, statusSub, logsSub, uninstallSub];
|
|
277
|
+
//# sourceMappingURL=proxy-lifecycle.js.map
|