@claude-flow/cli 3.36.0 → 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.
@@ -1 +1 @@
1
- 3.35.0
1
+ 3.33.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.36.0",
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": "quiBH2e5gpMkXXiCB2Y0Qxq7NCra4RozthLNqF8/fsISogpAmNX6N6r2UA9UJljvMyXwgAEtAtChXx4g1zh/AQ==",
11
+ "signature": "wjNllO+6A/iFejatdXHUflp2Bx831zKmAHCsJANV8L93X/s/8unB79SJCG9MpiiFFnoCAx+ESPken/NV3oG2BQ==",
12
12
  "algorithm": "ed25519"
13
13
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 4,
4
- "generatedAt": "2026-08-10T17:40:16.814Z",
5
- "gitSha": "f35c545f",
4
+ "generatedAt": "2026-08-11T17:05:49.193Z",
5
+ "gitSha": "6ce18b5a",
6
6
  "catalog": {
7
7
  "agents": 164,
8
8
  "tools": 397,
@@ -7,13 +7,30 @@
7
7
  */
8
8
  import type { Command } from '../types.js';
9
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";
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
- /** Pinned and reviewed; later upgrades remain explicit commands. */
15
- export const DEFAULT_PROXY_RELEASE = '0.4.0';
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 a specific version (never automatic)',
126
- options: [{ name: 'release', description: 'Release version to install', type: 'string', required: true }],
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 : undefined;
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
- 'With cloud routing ON, prompts for cloud-tier requests are sent to',
94
- 'api.cognitum.one and forwarded to the selected provider',
95
- '(Claude / GPT / Gemini / DeepSeek / OpenRouter).',
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
- 'Disable anytime: ruflo proxy config --local-only',
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, revert to local-only routing', type: 'boolean', default: false },
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
- if (wantCloud && wantLocalOnly) {
113
- output.printError('Pass either --cloud or --local-only, not both.');
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
- if (!wantCloud && !wantLocalOnly) {
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 === 'cloud'
120
- ? 'Cloud routing is ON — cloud-tier requests go to api.cognitum.one.'
121
- : 'Cloud routing is OFF — requests never leave this machine (or use your own Claude subscription on Passthrough).');
122
- return { success: true, data: { plane } };
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
- if (wantLocalOnly) {
125
- writeDataPlane('local');
126
- revokeConsent('cloud-routing', 'proxy-config-local-only');
127
- output.printSuccess('Cloud routing disabled reverted to local-only routing.');
128
- return { success: true, data: { plane: 'local' } };
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
- // wantCloud
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
- output.writeln(' Disable anytime: ruflo proxy config --local-only');
144
- return { success: true, data: { plane: 'cloud' } };
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: 'Install the signed Meta-Proxy v0.4.0 binary' },
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
- * Extracts the archive via the OS's own tools — `tar` for `.tar.gz`
28
- * (present on macOS/Linux/Windows 10+), PowerShell `Expand-Archive`
29
- * specifically for `.zip` on Windows (not tar's bsdtar zip support not
30
- * reliable enough to lean on). Zero new archive-parsing dependency, matching
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 extractArchive(archivePath, extractDir, ext) {
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. The binary itself takes
7
- * no CLI flags confirmed empirically (2026-07-16): `meta-proxy.exe` has
8
- * no `--version`/`--help`, and any invocation just starts the server reading
9
- * its own config file — so `spawn()` here passes zero arguments, always.
6
+ * native binary instead of a forked Node process. `spawn()` here passes zero
7
+ * arguments, alwaysstarting 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. The binary itself takes
7
- * no CLI flags confirmed empirically (2026-07-16): `meta-proxy.exe` has
8
- * no `--version`/`--help`, and any invocation just starts the server reading
9
- * its own config file — so `spawn()` here passes zero arguments, always.
6
+ * native binary instead of a forked Node process. `spawn()` here passes zero
7
+ * arguments, alwaysstarting 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.36.0",
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.21",
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",