@claude-flow/cli 3.35.1 → 3.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/proxy-lifecycle.d.ts +19 -2
- package/dist/src/commands/proxy-lifecycle.js +49 -5
- package/dist/src/commands/proxy.js +184 -23
- package/dist/src/proxy/install.d.ts +26 -0
- package/dist/src/proxy/install.js +60 -19
- package/dist/src/proxy/lifecycle.d.ts +21 -4
- package/dist/src/proxy/lifecycle.js +38 -8
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.37.0",
|
|
4
4
|
"files": {
|
|
5
5
|
"auto-memory-hook.mjs": "68be7e9a9eba7bf9c4e8a230db7bf61a243b965639f8504842799d6c6ca28762",
|
|
6
6
|
"hook-handler.cjs": "dae295fb9ae2626b89899c19a20cc911541af82b52d2eeb9b214d618b96e9a86",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"signature": "
|
|
11
|
+
"signature": "wjNllO+6A/iFejatdXHUflp2Bx831zKmAHCsJANV8L93X/s/8unB79SJCG9MpiiFFnoCAx+ESPken/NV3oG2BQ==",
|
|
12
12
|
"algorithm": "ed25519"
|
|
13
13
|
}
|
package/catalog-manifest.json
CHANGED
|
@@ -7,13 +7,30 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Command } from '../types.js';
|
|
9
9
|
import { type ProxyStatus } from '../proxy/lifecycle.js';
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Pinned and reviewed; later upgrades remain explicit commands.
|
|
12
|
+
*
|
|
13
|
+
* Keep this the single source of truth — interpolate it rather than writing
|
|
14
|
+
* the version into user-facing strings, or the pin silently drifts out of
|
|
15
|
+
* sync with the text that advertises it.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_PROXY_RELEASE = "0.7.3";
|
|
12
18
|
/**
|
|
13
19
|
* Human-oriented next steps for `ruflo proxy` and `ruflo proxy status`.
|
|
14
20
|
* Keep this independent of the command framework so the state-specific
|
|
15
21
|
* guidance has a small, direct regression-test surface.
|
|
16
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* Lines shown when the binary on disk is an older release than the current
|
|
25
|
+
* pin — empty otherwise, including when the version is unknown (`null`),
|
|
26
|
+
* where the honest move is to say nothing rather than guess.
|
|
27
|
+
*
|
|
28
|
+
* Bumping `DEFAULT_PROXY_RELEASE` only ever changed what a *new* install
|
|
29
|
+
* gets. Someone who installed the previous pin has no reason to run
|
|
30
|
+
* `install` again and no signal that anything moved, so a fix that motivated
|
|
31
|
+
* a bump reaches exactly the people who never needed it. This is the signal.
|
|
32
|
+
*/
|
|
33
|
+
export declare function proxyUpdateGuidance(status: ProxyStatus): string[];
|
|
17
34
|
export declare function proxyConsoleGuidance(status: ProxyStatus): string[];
|
|
18
35
|
export declare function printProxyConsoleGuidance(status: ProxyStatus): void;
|
|
19
36
|
export declare const proxyLifecycleSubcommands: Command[];
|
|
@@ -11,8 +11,14 @@ import { installProxy, uninstallProxy } from '../proxy/install.js';
|
|
|
11
11
|
import { startForeground, startBackground, stopProxy, getProxyStatus, readProxyLogTail, watchProxyLog, ProxyNotInstalledError, ProxyAlreadyRunningError, } from '../proxy/lifecycle.js';
|
|
12
12
|
import { proxyTokenPath } from '../proxy/paths.js';
|
|
13
13
|
import { removeInjectedToken, startTokenRefreshPump } from '../proxy/token-bridge.js';
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Pinned and reviewed; later upgrades remain explicit commands.
|
|
16
|
+
*
|
|
17
|
+
* Keep this the single source of truth — interpolate it rather than writing
|
|
18
|
+
* the version into user-facing strings, or the pin silently drifts out of
|
|
19
|
+
* sync with the text that advertises it.
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_PROXY_RELEASE = '0.7.3';
|
|
16
22
|
const PROXY_COMMAND = 'npx ruflo@latest proxy';
|
|
17
23
|
const AUTH_COMMAND = 'npx ruflo@latest auth';
|
|
18
24
|
/**
|
|
@@ -20,6 +26,25 @@ const AUTH_COMMAND = 'npx ruflo@latest auth';
|
|
|
20
26
|
* Keep this independent of the command framework so the state-specific
|
|
21
27
|
* guidance has a small, direct regression-test surface.
|
|
22
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* Lines shown when the binary on disk is an older release than the current
|
|
31
|
+
* pin — empty otherwise, including when the version is unknown (`null`),
|
|
32
|
+
* where the honest move is to say nothing rather than guess.
|
|
33
|
+
*
|
|
34
|
+
* Bumping `DEFAULT_PROXY_RELEASE` only ever changed what a *new* install
|
|
35
|
+
* gets. Someone who installed the previous pin has no reason to run
|
|
36
|
+
* `install` again and no signal that anything moved, so a fix that motivated
|
|
37
|
+
* a bump reaches exactly the people who never needed it. This is the signal.
|
|
38
|
+
*/
|
|
39
|
+
export function proxyUpdateGuidance(status) {
|
|
40
|
+
if (!status.installed || !status.version || status.version === DEFAULT_PROXY_RELEASE)
|
|
41
|
+
return [];
|
|
42
|
+
return [
|
|
43
|
+
'',
|
|
44
|
+
`An update is available — installed v${status.version}, current pin v${DEFAULT_PROXY_RELEASE}.`,
|
|
45
|
+
` ${PROXY_COMMAND} update Re-verify and replace with v${DEFAULT_PROXY_RELEASE}`,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
23
48
|
export function proxyConsoleGuidance(status) {
|
|
24
49
|
if (!status.installed) {
|
|
25
50
|
return [
|
|
@@ -31,6 +56,7 @@ export function proxyConsoleGuidance(status) {
|
|
|
31
56
|
` ${PROXY_COMMAND} start --service`,
|
|
32
57
|
];
|
|
33
58
|
}
|
|
59
|
+
const update = proxyUpdateGuidance(status);
|
|
34
60
|
if (!status.running) {
|
|
35
61
|
return [
|
|
36
62
|
'',
|
|
@@ -39,6 +65,7 @@ export function proxyConsoleGuidance(status) {
|
|
|
39
65
|
'',
|
|
40
66
|
'Foreground mode (shows live logs; Ctrl+C stops it):',
|
|
41
67
|
` ${PROXY_COMMAND} start`,
|
|
68
|
+
...update,
|
|
42
69
|
'',
|
|
43
70
|
'Optional: enable Cognitum cloud routing (local-only is the default):',
|
|
44
71
|
` ${AUTH_COMMAND} login`,
|
|
@@ -50,6 +77,7 @@ export function proxyConsoleGuidance(status) {
|
|
|
50
77
|
'Meta Proxy is ready.',
|
|
51
78
|
` Logs: ${PROXY_COMMAND} logs`,
|
|
52
79
|
` Stop: ${PROXY_COMMAND} stop`,
|
|
80
|
+
...update,
|
|
53
81
|
'',
|
|
54
82
|
'Optional: enable Cognitum cloud routing (local-only is the default):',
|
|
55
83
|
` ${AUTH_COMMAND} login`,
|
|
@@ -122,10 +150,21 @@ const installSub = {
|
|
|
122
150
|
};
|
|
123
151
|
const updateSub = {
|
|
124
152
|
name: 'update',
|
|
125
|
-
description: 'Re-verify and replace the installed binary with
|
|
126
|
-
options: [
|
|
153
|
+
description: 'Re-verify and replace the installed binary with the pinned release (never automatic)',
|
|
154
|
+
options: [
|
|
155
|
+
// Defaults to the pin, matching `install`. `--release` was previously
|
|
156
|
+
// required, which meant the only in-product way to reach a newer binary
|
|
157
|
+
// was to already know its version number — so bumping the pin moved
|
|
158
|
+
// nothing for anyone who had already installed. Still never automatic:
|
|
159
|
+
// this runs only when a user types it.
|
|
160
|
+
{
|
|
161
|
+
name: 'release',
|
|
162
|
+
description: `Release version to install (default: ${DEFAULT_PROXY_RELEASE})`,
|
|
163
|
+
type: 'string',
|
|
164
|
+
},
|
|
165
|
+
],
|
|
127
166
|
action: async (ctx) => {
|
|
128
|
-
const version = typeof ctx.flags.release === 'string' ? ctx.flags.release :
|
|
167
|
+
const version = typeof ctx.flags.release === 'string' ? ctx.flags.release : DEFAULT_PROXY_RELEASE;
|
|
129
168
|
if (!version) {
|
|
130
169
|
output.printError('ruflo proxy update requires --release <x.y.z>');
|
|
131
170
|
return { success: false, exitCode: 1 };
|
|
@@ -212,6 +251,11 @@ const statusSub = {
|
|
|
212
251
|
}
|
|
213
252
|
output.writeln('Meta Proxy');
|
|
214
253
|
output.writeln(` Installation: ${status.installed ? 'ready' : 'not installed'}`);
|
|
254
|
+
if (status.installed) {
|
|
255
|
+
// "unknown" rather than an assumed version: the manifest is the only
|
|
256
|
+
// record of what was installed, and asking the binary would start it.
|
|
257
|
+
output.writeln(` Version: ${status.version ?? 'unknown (no install manifest)'}`);
|
|
258
|
+
}
|
|
215
259
|
output.writeln(` Process: ${status.running ? `running (pid ${status.pid})` : 'not running'}`);
|
|
216
260
|
if (status.stalePidFile)
|
|
217
261
|
output.writeln(' (a stale PID file was found and will be cleared on next start)');
|
|
@@ -23,7 +23,7 @@ import { clearRateLimitStatus, readRateLimitStatus } from '../funnel/rate-limit-
|
|
|
23
23
|
import { clearQuotaLowStatus, readQuotaLowStatus } from '../funnel/power-saver-notifier.js';
|
|
24
24
|
import { getInstalledCliVersion } from '../init/helper-refresh.js';
|
|
25
25
|
import * as path from 'path';
|
|
26
|
-
import { proxyLifecycleSubcommands, printProxyConsoleGuidance } from './proxy-lifecycle.js';
|
|
26
|
+
import { proxyLifecycleSubcommands, printProxyConsoleGuidance, DEFAULT_PROXY_RELEASE } from './proxy-lifecycle.js';
|
|
27
27
|
import { getProxyStatus } from '../proxy/lifecycle.js';
|
|
28
28
|
const PROXY_CONFIG_FILE = 'proxy-config.toml';
|
|
29
29
|
/**
|
|
@@ -87,61 +87,220 @@ function readDataPlane() {
|
|
|
87
87
|
function writeDataPlane(plane) {
|
|
88
88
|
writeConfigLine('default_data_plane', `"${plane}"`);
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* How to get back to a given plane, for "restore what you had" guidance.
|
|
92
|
+
* `sponsored` is ADR-313's own consent flow and is deliberately absent — this
|
|
93
|
+
* command must not offer a one-flag route into someone else's consent gate.
|
|
94
|
+
*/
|
|
95
|
+
const PLANE_RESTORE_COMMAND = {
|
|
96
|
+
passthrough: 'ruflo proxy config --passthrough',
|
|
97
|
+
local: 'ruflo proxy config --local-only',
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* One line describing a plane in the terms that matter — who serves the
|
|
101
|
+
* request. Kept together so the no-flag report and the restore hints cannot
|
|
102
|
+
* describe the same plane two different ways.
|
|
103
|
+
*/
|
|
104
|
+
function describePlane(plane) {
|
|
105
|
+
switch (plane) {
|
|
106
|
+
case 'cloud':
|
|
107
|
+
return 'Cloud routing is ON — cloud-tier requests go to api.cognitum.one.';
|
|
108
|
+
case 'passthrough':
|
|
109
|
+
return 'Passthrough — requests go to your own Claude subscription. Prompts do not go to Cognitum.';
|
|
110
|
+
case 'local':
|
|
111
|
+
return 'Local-only — requests go to your own backend (Ollama/vLLM/SGLang) and never leave this machine.';
|
|
112
|
+
case 'sponsored':
|
|
113
|
+
return "Sponsored — requests go to Cognitum's sponsored capacity (ADR-313).";
|
|
114
|
+
default:
|
|
115
|
+
return `Unrecognized plane "${plane}" — meta-proxy will fall back to its own default (passthrough).`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `routing_mode` — how the Cloud plane picks a tier (meta-proxy ADR-321
|
|
120
|
+
* rev-2). Values match the Rust `RoutingMode` enum (`src/config.rs`, also
|
|
121
|
+
* `#[serde(rename_all = "snake_case")]`); absent from the file means `auto`,
|
|
122
|
+
* so an existing config keeps working untouched.
|
|
123
|
+
*/
|
|
124
|
+
const ROUTING_MODES = ['auto', 'low', 'mid', 'high'];
|
|
125
|
+
function readRoutingMode() {
|
|
126
|
+
const match = readProxyConfigRaw().match(/^routing_mode\s*=\s*"([^"]*)"/m);
|
|
127
|
+
const value = match?.[1];
|
|
128
|
+
return ROUTING_MODES.includes(value ?? '') ? value : 'auto';
|
|
129
|
+
}
|
|
130
|
+
function writeRoutingMode(mode) {
|
|
131
|
+
writeConfigLine('routing_mode', `"${mode}"`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* ADR-304/321. This is the terminal-side equivalent of the Developer
|
|
135
|
+
* Console's Cloud selector disclosure — meta-proxy#43 M5a covered the
|
|
136
|
+
* console, M5b covers "any terminal flow that explicitly activates Cloud",
|
|
137
|
+
* which is this command.
|
|
138
|
+
*
|
|
139
|
+
* It answers the three questions activating Cloud actually decides, because
|
|
140
|
+
* each has a different answer than the plane the user is leaving:
|
|
141
|
+
* who processes the prompt, who pays for it, and which model runs. The third
|
|
142
|
+
* is the one the previous text omitted entirely, and it is not a detail —
|
|
143
|
+
* on the Cloud plane the client's requested model is deliberately NOT used.
|
|
144
|
+
*/
|
|
90
145
|
const CLOUD_ROUTING_DISCLOSURE = [
|
|
91
146
|
'Enabling cloud routing.',
|
|
92
147
|
'',
|
|
93
|
-
'
|
|
94
|
-
'api.cognitum.one and
|
|
95
|
-
'(Claude / GPT / Gemini / DeepSeek /
|
|
148
|
+
'Who processes your prompts:',
|
|
149
|
+
' Prompts for cloud-tier requests are sent to api.cognitum.one and',
|
|
150
|
+
' forwarded to the selected provider (Claude / GPT / Gemini / DeepSeek /',
|
|
151
|
+
' OpenRouter). Cognitum handles them server-side.',
|
|
152
|
+
'',
|
|
153
|
+
'Who pays:',
|
|
154
|
+
' Cloud-tier requests are metered against your Cognitum account, not',
|
|
155
|
+
' against your own Claude subscription. Your subscription is used only',
|
|
156
|
+
' by the Passthrough plane (the proxy\'s default), which is unaffected',
|
|
157
|
+
' by everything below.',
|
|
158
|
+
'',
|
|
159
|
+
'Which model runs:',
|
|
160
|
+
' The cloud plane picks the tier per prompt instead of using the model',
|
|
161
|
+
' your client asked for (ADR-321). That is the point of the plane: a',
|
|
162
|
+
' trivial question is not served at frontier rates because the client',
|
|
163
|
+
' happened to name a frontier model. The scorer reads prompt shape, not',
|
|
164
|
+
' task difficulty, so pin a tier if you disagree with its judgement:',
|
|
165
|
+
' ruflo proxy config --routing-mode high',
|
|
96
166
|
'',
|
|
97
167
|
'Requests routed to local backends never leave this machine.',
|
|
98
168
|
'',
|
|
99
|
-
'
|
|
169
|
+
'Turning it off is a choice of where to go back to, not one command:',
|
|
170
|
+
' ruflo proxy config --passthrough Your own Claude subscription',
|
|
171
|
+
' ruflo proxy config --local-only Your own local backend',
|
|
100
172
|
].join('\n');
|
|
101
173
|
const configSub = {
|
|
102
174
|
name: 'config',
|
|
103
|
-
description: 'Toggle cloud routing (ADR-304) — local backends only by default',
|
|
175
|
+
description: 'Toggle cloud routing and its tier selection (ADR-304/321) — local backends only by default',
|
|
104
176
|
options: [
|
|
105
177
|
{ name: 'cloud', description: 'Enable cloud routing (requires cloud-routing consent)', type: 'boolean', default: false },
|
|
106
|
-
{ name: 'local-only', description: 'Disable cloud routing
|
|
178
|
+
{ name: 'local-only', description: 'Disable cloud routing; route to your own local backend', type: 'boolean', default: false },
|
|
179
|
+
{
|
|
180
|
+
name: 'passthrough',
|
|
181
|
+
description: "Disable cloud routing; route to your own Claude subscription (meta-proxy's own default)",
|
|
182
|
+
type: 'boolean',
|
|
183
|
+
default: false,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'routing-mode',
|
|
187
|
+
description: `How the cloud plane picks a tier: ${ROUTING_MODES.join(' | ')} (default: auto)`,
|
|
188
|
+
type: 'string',
|
|
189
|
+
},
|
|
107
190
|
{ name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
|
|
108
191
|
],
|
|
109
192
|
action: async (ctx) => {
|
|
110
193
|
const wantCloud = Boolean(ctx.flags.cloud);
|
|
111
194
|
const wantLocalOnly = Boolean(ctx.flags.localOnly ?? ctx.flags['local-only']);
|
|
112
|
-
|
|
113
|
-
|
|
195
|
+
const wantPassthrough = Boolean(ctx.flags.passthrough);
|
|
196
|
+
const rawRoutingMode = ctx.flags.routingMode ?? ctx.flags['routing-mode'];
|
|
197
|
+
const wantRoutingMode = typeof rawRoutingMode === 'string' ? rawRoutingMode : undefined;
|
|
198
|
+
const planeFlags = [
|
|
199
|
+
wantCloud && '--cloud',
|
|
200
|
+
wantLocalOnly && '--local-only',
|
|
201
|
+
wantPassthrough && '--passthrough',
|
|
202
|
+
].filter(Boolean);
|
|
203
|
+
if (planeFlags.length > 1) {
|
|
204
|
+
output.printError(`Pass one plane at a time — got ${planeFlags.join(' and ')}.`);
|
|
205
|
+
return { success: false, exitCode: 1 };
|
|
206
|
+
}
|
|
207
|
+
if (wantRoutingMode !== undefined && !ROUTING_MODES.includes(wantRoutingMode)) {
|
|
208
|
+
output.printError(`Unknown routing mode "${wantRoutingMode}". Valid values: ${ROUTING_MODES.join(', ')}.`);
|
|
114
209
|
return { success: false, exitCode: 1 };
|
|
115
210
|
}
|
|
116
|
-
|
|
211
|
+
const routingMode = wantRoutingMode;
|
|
212
|
+
// A tier only means anything on the Cloud plane, and these flags leave
|
|
213
|
+
// it. Writing both would record a preference the same command just made
|
|
214
|
+
// unreachable, so say so instead of silently picking one.
|
|
215
|
+
const leavingCloud = wantLocalOnly || wantPassthrough;
|
|
216
|
+
if (routingMode && leavingCloud) {
|
|
217
|
+
output.printError(`--routing-mode only governs the cloud plane, which ${planeFlags[0]} turns off. Pass one or the other.`);
|
|
218
|
+
return { success: false, exitCode: 1 };
|
|
219
|
+
}
|
|
220
|
+
if (!wantCloud && !leavingCloud && !routingMode) {
|
|
117
221
|
const plane = readDataPlane();
|
|
118
222
|
output.writeln(`Current data plane: ${plane}`);
|
|
119
|
-
output.writeln(plane
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
223
|
+
output.writeln(` ${describePlane(plane)}`);
|
|
224
|
+
const mode = readRoutingMode();
|
|
225
|
+
output.writeln(mode === 'auto'
|
|
226
|
+
? ' Tier selection: auto — Cognitum scores each prompt and picks low/mid/high.'
|
|
227
|
+
: ` Tier selection: pinned to ${mode}.`);
|
|
228
|
+
if (plane !== 'cloud')
|
|
229
|
+
output.writeln(' (Tier selection applies only while cloud routing is ON.)');
|
|
230
|
+
// meta-proxy routing.rs gates automatic quota failover on the plane
|
|
231
|
+
// being Passthrough — it is the only plane that sees Anthropic's own
|
|
232
|
+
// rate-limit headers. Worth saying, because nothing else reveals that
|
|
233
|
+
// sitting on `local` silently opts you out of ADR-321 entirely.
|
|
234
|
+
if (plane === 'local') {
|
|
235
|
+
output.writeln(' Note: automatic quota failover (ADR-321) applies only on passthrough.');
|
|
236
|
+
output.writeln(' Use your Claude subscription instead: ruflo proxy config --passthrough');
|
|
237
|
+
}
|
|
238
|
+
return { success: true, data: { plane, routingMode: mode } };
|
|
123
239
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
240
|
+
// Setting a tier must never activate Cloud — meta-proxy ADR-321
|
|
241
|
+
// Revision 3 keeps the plane choice and this Cloud-only secondary
|
|
242
|
+
// setting as separate controls, so `--routing-mode` alone writes only
|
|
243
|
+
// `routing_mode` and asks for no consent it does not need.
|
|
244
|
+
if (routingMode && !wantCloud) {
|
|
245
|
+
writeRoutingMode(routingMode);
|
|
246
|
+
output.printSuccess(routingMode === 'auto'
|
|
247
|
+
? 'Cloud tier selection set to auto — Cognitum scores each prompt.'
|
|
248
|
+
: `Cloud tier selection pinned to ${routingMode}.`);
|
|
249
|
+
if (readDataPlane() !== 'cloud') {
|
|
250
|
+
output.writeln(' Cloud routing is currently OFF, so this takes effect once you enable it:');
|
|
251
|
+
output.writeln(' ruflo proxy config --cloud --yes');
|
|
252
|
+
}
|
|
253
|
+
return { success: true, data: { routingMode } };
|
|
129
254
|
}
|
|
130
|
-
|
|
255
|
+
if (leavingCloud) {
|
|
256
|
+
const plane = wantPassthrough ? 'passthrough' : 'local';
|
|
257
|
+
writeDataPlane(plane);
|
|
258
|
+
revokeConsent('cloud-routing', wantPassthrough ? 'proxy-config-passthrough' : 'proxy-config-local-only');
|
|
259
|
+
output.printSuccess(`Cloud routing disabled — now on ${plane}.`);
|
|
260
|
+
output.writeln(` ${describePlane(plane)}`);
|
|
261
|
+
// Naming the other option here is the point: "disable cloud" has two
|
|
262
|
+
// destinations, and the one you land on decides whether your own
|
|
263
|
+
// subscription is used at all.
|
|
264
|
+
if (plane === 'local') {
|
|
265
|
+
output.writeln(' Your own Claude subscription is NOT used on this plane. To use it instead:');
|
|
266
|
+
output.writeln(' ruflo proxy config --passthrough');
|
|
267
|
+
}
|
|
268
|
+
return { success: true, data: { plane } };
|
|
269
|
+
}
|
|
270
|
+
// wantCloud. Read the plane being left BEFORE overwriting it — this is
|
|
271
|
+
// the only moment ruflo knows where the user was, and "how do I get back"
|
|
272
|
+
// is otherwise unanswerable from the config file afterwards.
|
|
273
|
+
const previousPlane = readDataPlane();
|
|
131
274
|
if (!hasConsent('cloud-routing')) {
|
|
132
275
|
output.writeln(CLOUD_ROUTING_DISCLOSURE);
|
|
133
276
|
output.writeln('');
|
|
134
277
|
if (!ctx.flags.yes) {
|
|
135
278
|
output.writeln('Re-run with --yes to confirm: ruflo proxy config --cloud --yes');
|
|
279
|
+
// Nothing is written on the unconfirmed path — including any
|
|
280
|
+
// --routing-mode passed alongside, which would otherwise leave a
|
|
281
|
+
// trace of an activation the user never confirmed.
|
|
136
282
|
return { success: true, data: { confirmed: false } };
|
|
137
283
|
}
|
|
138
284
|
recordConsent('cloud-routing', true, 'proxy-config-cloud');
|
|
139
285
|
}
|
|
140
286
|
writeDataPlane('cloud');
|
|
287
|
+
if (routingMode)
|
|
288
|
+
writeRoutingMode(routingMode);
|
|
289
|
+
const effectiveMode = routingMode ?? readRoutingMode();
|
|
141
290
|
output.printSuccess('Cloud routing enabled.');
|
|
291
|
+
output.writeln(effectiveMode === 'auto'
|
|
292
|
+
? ' Tier selection: auto — Cognitum scores each prompt and picks low/mid/high.'
|
|
293
|
+
: ` Tier selection: pinned to ${effectiveMode}.`);
|
|
142
294
|
output.writeln(' Requests routed to local backends still never leave this machine.');
|
|
143
|
-
|
|
144
|
-
|
|
295
|
+
const restore = previousPlane === 'cloud' ? undefined : PLANE_RESTORE_COMMAND[previousPlane];
|
|
296
|
+
if (restore) {
|
|
297
|
+
output.writeln(` Previous plane: ${previousPlane}. Restore it with:`);
|
|
298
|
+
output.writeln(` ${restore}`);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
output.writeln(' Turn it off with: ruflo proxy config --passthrough (or --local-only)');
|
|
302
|
+
}
|
|
303
|
+
return { success: true, data: { plane: 'cloud', routingMode: effectiveMode, previousPlane } };
|
|
145
304
|
},
|
|
146
305
|
};
|
|
147
306
|
const SPONSOR_DISCLOSURE = [
|
|
@@ -378,10 +537,12 @@ export const proxyCommand = {
|
|
|
378
537
|
trainingShareEnableSub, trainingShareDisableSub, trainingShareStatusSub,
|
|
379
538
|
],
|
|
380
539
|
examples: [
|
|
381
|
-
{ command: 'ruflo proxy install --yes', description:
|
|
540
|
+
{ command: 'ruflo proxy install --yes', description: `Install the signed Meta-Proxy v${DEFAULT_PROXY_RELEASE} binary` },
|
|
382
541
|
{ command: 'ruflo proxy start', description: 'Start meta-proxy in the foreground' },
|
|
383
542
|
{ command: 'ruflo proxy status', description: 'Show install + process status' },
|
|
384
543
|
{ command: 'ruflo proxy config --cloud --yes', description: 'Enable cloud routing (ADR-304)' },
|
|
544
|
+
{ command: 'ruflo proxy config --routing-mode high', description: 'Pin the cloud tier instead of auto (ADR-321)' },
|
|
545
|
+
{ command: 'ruflo proxy config --passthrough', description: 'Turn cloud routing off, back to your Claude subscription' },
|
|
385
546
|
{ command: 'ruflo proxy config --local-only', description: 'Revert to local-only routing' },
|
|
386
547
|
{ command: 'ruflo proxy sponsor-status', description: 'Show current sponsored-mode state' },
|
|
387
548
|
{ command: 'ruflo proxy sponsor-enable --yes', description: 'Opt into sponsored downtime capacity' },
|
|
@@ -11,6 +11,32 @@
|
|
|
11
11
|
export declare class ExtractionError extends Error {
|
|
12
12
|
constructor(message: string);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Extracts the archive via the OS's own tools — `tar` for `.tar.gz`
|
|
16
|
+
* (present on macOS/Linux/Windows 10+), PowerShell `Expand-Archive`
|
|
17
|
+
* specifically for `.zip` on Windows. Zero new archive-parsing dependency,
|
|
18
|
+
* matching this repo's existing taste for shelling out over adding a parser dep.
|
|
19
|
+
*
|
|
20
|
+
* `Expand-Archive` stays the PRIMARY `.zip` path — bsdtar's zip support is
|
|
21
|
+
* still not something to lean on by default. But `Microsoft.PowerShell.Archive`
|
|
22
|
+
* is a *script* module resolved through PowerShell's module autoloading, so it
|
|
23
|
+
* can fail for environment reasons entirely unrelated to the archive. Observed
|
|
24
|
+
* in the wild on a healthy Windows 11 box, from ruflo's own `-NonInteractive`
|
|
25
|
+
* child process:
|
|
26
|
+
*
|
|
27
|
+
* Expand-Archive : The 'Expand-Archive' command was found in the module
|
|
28
|
+
* 'Microsoft.PowerShell.Archive', but the module could not be loaded.
|
|
29
|
+
*
|
|
30
|
+
* The same command succeeded on the same machine minutes later, so this is
|
|
31
|
+
* intermittent rather than a hard platform break. Previously that aborted the
|
|
32
|
+
* install outright with nothing to fall back on, stranding an already
|
|
33
|
+
* downloaded-and-verified archive. Retrying through bsdtar costs one extra
|
|
34
|
+
* process on a path that was going to fail anyway.
|
|
35
|
+
*
|
|
36
|
+
* Exported for tests — extraction is the only platform-divergent step in the
|
|
37
|
+
* install pipeline and had no direct coverage.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractArchive(archivePath: string, extractDir: string, ext: 'zip' | 'tar.gz'): Promise<void>;
|
|
14
40
|
export interface InstallOptions {
|
|
15
41
|
version: string;
|
|
16
42
|
log?: (line: string) => void;
|
|
@@ -23,28 +23,23 @@ export class ExtractionError extends Error {
|
|
|
23
23
|
function binaryNameInArchive() {
|
|
24
24
|
return process.platform === 'win32' ? 'meta-proxy.exe' : 'meta-proxy';
|
|
25
25
|
}
|
|
26
|
+
/** `tar` handles `.tar.gz` everywhere, and `.zip` via bsdtar on Windows 10+. */
|
|
27
|
+
async function extractWithTar(archivePath, extractDir, flags) {
|
|
28
|
+
const { SafeExecutor } = await import('@claude-flow/security');
|
|
29
|
+
const exec = new SafeExecutor({ allowedCommands: ['tar'], timeout: 60_000 });
|
|
30
|
+
const result = await exec.execute('tar', [flags, archivePath, '-C', extractDir]);
|
|
31
|
+
if (result.exitCode !== 0) {
|
|
32
|
+
throw new ExtractionError(`tar extraction failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
26
35
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* this repo's existing taste for shelling out over adding a parser dep.
|
|
36
|
+
* Single-quoted literal paths (doubling any embedded single quote per
|
|
37
|
+
* PowerShell string-literal escaping) passed as ONE argv element to
|
|
38
|
+
* `-Command`. shell:false means no OS shell ever tokenizes this string —
|
|
39
|
+
* only powershell.exe's own parser does.
|
|
32
40
|
*/
|
|
33
|
-
async function
|
|
41
|
+
async function extractWithPowerShell(archivePath, extractDir) {
|
|
34
42
|
const { SafeExecutor } = await import('@claude-flow/security');
|
|
35
|
-
fs.mkdirSync(extractDir, { recursive: true });
|
|
36
|
-
if (ext === 'tar.gz') {
|
|
37
|
-
const exec = new SafeExecutor({ allowedCommands: ['tar'], timeout: 60_000 });
|
|
38
|
-
const result = await exec.execute('tar', ['xzf', archivePath, '-C', extractDir]);
|
|
39
|
-
if (result.exitCode !== 0) {
|
|
40
|
-
throw new ExtractionError(`tar extraction failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
41
|
-
}
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
// .zip — PowerShell Expand-Archive, single-quoted literal paths (doubling
|
|
45
|
-
// any embedded single quote per PowerShell string-literal escaping) passed
|
|
46
|
-
// as ONE argv element to -Command. shell:false means no OS shell ever
|
|
47
|
-
// tokenizes this string — only powershell.exe's own parser does.
|
|
48
43
|
const escape = (p) => p.replace(/'/g, "''");
|
|
49
44
|
const command = `Expand-Archive -LiteralPath '${escape(archivePath)}' -DestinationPath '${escape(extractDir)}' -Force`;
|
|
50
45
|
const exec = new SafeExecutor({ allowedCommands: ['powershell', 'powershell.exe'], timeout: 60_000 });
|
|
@@ -53,6 +48,52 @@ async function extractArchive(archivePath, extractDir, ext) {
|
|
|
53
48
|
throw new ExtractionError(`Expand-Archive failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
54
49
|
}
|
|
55
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Extracts the archive via the OS's own tools — `tar` for `.tar.gz`
|
|
53
|
+
* (present on macOS/Linux/Windows 10+), PowerShell `Expand-Archive`
|
|
54
|
+
* specifically for `.zip` on Windows. Zero new archive-parsing dependency,
|
|
55
|
+
* matching this repo's existing taste for shelling out over adding a parser dep.
|
|
56
|
+
*
|
|
57
|
+
* `Expand-Archive` stays the PRIMARY `.zip` path — bsdtar's zip support is
|
|
58
|
+
* still not something to lean on by default. But `Microsoft.PowerShell.Archive`
|
|
59
|
+
* is a *script* module resolved through PowerShell's module autoloading, so it
|
|
60
|
+
* can fail for environment reasons entirely unrelated to the archive. Observed
|
|
61
|
+
* in the wild on a healthy Windows 11 box, from ruflo's own `-NonInteractive`
|
|
62
|
+
* child process:
|
|
63
|
+
*
|
|
64
|
+
* Expand-Archive : The 'Expand-Archive' command was found in the module
|
|
65
|
+
* 'Microsoft.PowerShell.Archive', but the module could not be loaded.
|
|
66
|
+
*
|
|
67
|
+
* The same command succeeded on the same machine minutes later, so this is
|
|
68
|
+
* intermittent rather than a hard platform break. Previously that aborted the
|
|
69
|
+
* install outright with nothing to fall back on, stranding an already
|
|
70
|
+
* downloaded-and-verified archive. Retrying through bsdtar costs one extra
|
|
71
|
+
* process on a path that was going to fail anyway.
|
|
72
|
+
*
|
|
73
|
+
* Exported for tests — extraction is the only platform-divergent step in the
|
|
74
|
+
* install pipeline and had no direct coverage.
|
|
75
|
+
*/
|
|
76
|
+
export async function extractArchive(archivePath, extractDir, ext) {
|
|
77
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
78
|
+
if (ext === 'tar.gz') {
|
|
79
|
+
await extractWithTar(archivePath, extractDir, 'xzf');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await extractWithPowerShell(archivePath, extractDir);
|
|
84
|
+
}
|
|
85
|
+
catch (primary) {
|
|
86
|
+
const primaryMessage = primary instanceof Error ? primary.message : String(primary);
|
|
87
|
+
try {
|
|
88
|
+
// 'xf', not 'xzf' — a zip is not gzip-compressed; bsdtar sniffs the format.
|
|
89
|
+
await extractWithTar(archivePath, extractDir, 'xf');
|
|
90
|
+
}
|
|
91
|
+
catch (fallback) {
|
|
92
|
+
const fallbackMessage = fallback instanceof Error ? fallback.message : String(fallback);
|
|
93
|
+
throw new ExtractionError(`zip extraction failed via both available extractors. Expand-Archive: ${primaryMessage} — tar fallback: ${fallbackMessage}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
56
97
|
/**
|
|
57
98
|
* Full install pipeline. Refuses (throws) on any verification failure —
|
|
58
99
|
* never writes a partially-verified binary into the live install path.
|
|
@@ -3,10 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Adapts daemon.ts's proven pattern (PID file, O_EXCL lockfile for atomic
|
|
5
5
|
* check-then-start, signal-0 liveness, SIGTERM->1000ms->SIGKILL) to a
|
|
6
|
-
* native binary instead of a forked Node process.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* native binary instead of a forked Node process. `spawn()` here passes zero
|
|
7
|
+
* arguments, always — starting the server is the argument-free behavior and
|
|
8
|
+
* the only one this module wants.
|
|
9
|
+
*
|
|
10
|
+
* That is now a choice rather than a constraint. The 2026-07-16 note here
|
|
11
|
+
* ("the binary takes no CLI flags — no `--version`/`--help`, any invocation
|
|
12
|
+
* just starts the server") was true of the release pinned at the time, and
|
|
13
|
+
* stopped being true: meta-proxy v0.7.2 handles `--help` before binding, and
|
|
14
|
+
* v0.7.3 makes `--help`/`--version` win from any argv position. Do not read
|
|
15
|
+
* the old note as "the binary cannot be asked what it is" — it can. The
|
|
16
|
+
* installed version is still read from the install manifest rather than by
|
|
17
|
+
* executing the binary, because a filesystem read cannot start a listener
|
|
18
|
+
* and an exec of an old build can (a `--version` probe against 0.4.0 leaves
|
|
19
|
+
* a daemon bound to 127.0.0.1:11435).
|
|
10
20
|
*
|
|
11
21
|
* Foreground `start` (the ADR-307 default) uses `stdio: 'inherit'` and
|
|
12
22
|
* blocks directly — simplest and safest, no log-file redirection needed.
|
|
@@ -31,6 +41,13 @@ export interface ProxyStatus {
|
|
|
31
41
|
running: boolean;
|
|
32
42
|
pid: number | null;
|
|
33
43
|
stalePidFile: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The release recorded by the install that produced the binary on disk, or
|
|
46
|
+
* null when unknown — either nothing is installed, or the binary predates
|
|
47
|
+
* the manifest / the manifest was hand-removed. Callers must treat null as
|
|
48
|
+
* "cannot tell", never as "up to date".
|
|
49
|
+
*/
|
|
50
|
+
version: string | null;
|
|
34
51
|
}
|
|
35
52
|
export declare function getProxyStatus(): ProxyStatus;
|
|
36
53
|
/**
|
|
@@ -3,10 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Adapts daemon.ts's proven pattern (PID file, O_EXCL lockfile for atomic
|
|
5
5
|
* check-then-start, signal-0 liveness, SIGTERM->1000ms->SIGKILL) to a
|
|
6
|
-
* native binary instead of a forked Node process.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* native binary instead of a forked Node process. `spawn()` here passes zero
|
|
7
|
+
* arguments, always — starting the server is the argument-free behavior and
|
|
8
|
+
* the only one this module wants.
|
|
9
|
+
*
|
|
10
|
+
* That is now a choice rather than a constraint. The 2026-07-16 note here
|
|
11
|
+
* ("the binary takes no CLI flags — no `--version`/`--help`, any invocation
|
|
12
|
+
* just starts the server") was true of the release pinned at the time, and
|
|
13
|
+
* stopped being true: meta-proxy v0.7.2 handles `--help` before binding, and
|
|
14
|
+
* v0.7.3 makes `--help`/`--version` win from any argv position. Do not read
|
|
15
|
+
* the old note as "the binary cannot be asked what it is" — it can. The
|
|
16
|
+
* installed version is still read from the install manifest rather than by
|
|
17
|
+
* executing the binary, because a filesystem read cannot start a listener
|
|
18
|
+
* and an exec of an old build can (a `--version` probe against 0.4.0 leaves
|
|
19
|
+
* a daemon bound to 127.0.0.1:11435).
|
|
10
20
|
*
|
|
11
21
|
* Foreground `start` (the ADR-307 default) uses `stdio: 'inherit'` and
|
|
12
22
|
* blocks directly — simplest and safest, no log-file redirection needed.
|
|
@@ -20,7 +30,7 @@
|
|
|
20
30
|
*/
|
|
21
31
|
import { spawn } from 'node:child_process';
|
|
22
32
|
import * as fs from 'node:fs';
|
|
23
|
-
import { proxyBinaryPath, proxyPidFilePath, proxyLockFilePath, proxyLogFilePath } from './paths.js';
|
|
33
|
+
import { proxyBinaryPath, proxyPidFilePath, proxyLockFilePath, proxyLogFilePath, proxyInstallManifestPath, } from './paths.js';
|
|
24
34
|
export class ProxyNotInstalledError extends Error {
|
|
25
35
|
constructor() {
|
|
26
36
|
super('meta-proxy is not installed. Run: ruflo proxy install');
|
|
@@ -51,19 +61,39 @@ function isProcessRunning(pid) {
|
|
|
51
61
|
return false;
|
|
52
62
|
}
|
|
53
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The installed release, from `install.ts`'s manifest. Deliberately a
|
|
66
|
+
* filesystem read and not `meta-proxy --version`: exec'ing the binary to ask
|
|
67
|
+
* its version is exactly the probe that starts a listener on the older
|
|
68
|
+
* builds this is most needed to detect.
|
|
69
|
+
*
|
|
70
|
+
* Never throws — a missing, unreadable, or malformed manifest is a normal
|
|
71
|
+
* "unknown", not a reason to fail `proxy status`.
|
|
72
|
+
*/
|
|
73
|
+
function readInstalledVersion() {
|
|
74
|
+
try {
|
|
75
|
+
const raw = fs.readFileSync(proxyInstallManifestPath(), 'utf-8');
|
|
76
|
+
const manifest = JSON.parse(raw);
|
|
77
|
+
return typeof manifest.version === 'string' && manifest.version ? manifest.version : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
54
83
|
export function getProxyStatus() {
|
|
55
84
|
const installed = fs.existsSync(proxyBinaryPath());
|
|
85
|
+
const version = installed ? readInstalledVersion() : null;
|
|
56
86
|
const pidPath = proxyPidFilePath();
|
|
57
87
|
if (!fs.existsSync(pidPath)) {
|
|
58
|
-
return { installed, running: false, pid: null, stalePidFile: false };
|
|
88
|
+
return { installed, running: false, pid: null, stalePidFile: false, version };
|
|
59
89
|
}
|
|
60
90
|
const raw = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
61
91
|
const pid = parseInt(raw, 10);
|
|
62
92
|
if (!Number.isFinite(pid)) {
|
|
63
|
-
return { installed, running: false, pid: null, stalePidFile: true };
|
|
93
|
+
return { installed, running: false, pid: null, stalePidFile: true, version };
|
|
64
94
|
}
|
|
65
95
|
const running = isProcessRunning(pid);
|
|
66
|
-
return { installed, running, pid: running ? pid : null, stalePidFile: !running };
|
|
96
|
+
return { installed, running, pid: running ? pid : null, stalePidFile: !running, version };
|
|
67
97
|
}
|
|
68
98
|
function writePidFile(pid) {
|
|
69
99
|
fs.writeFileSync(proxyPidFilePath(), String(pid), 'utf-8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.37.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"optionalDependencies": {
|
|
123
123
|
"@agntcy/slim-bindings": "2.0.0-alpha.5",
|
|
124
124
|
"@napi-rs/keyring": "1.3.0",
|
|
125
|
-
"@claude-flow/memory": "^3.0.0-alpha.
|
|
125
|
+
"@claude-flow/memory": "^3.0.0-alpha.22",
|
|
126
126
|
"@metaharness/darwin": "~0.9.0",
|
|
127
127
|
"@metaharness/flywheel": "~0.1.10",
|
|
128
128
|
"@metaharness/radio": "~0.1.0",
|