@claude-flow/cli 3.63.3 → 3.63.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/README.md +1 -1
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/commands/hooks.js +1 -1
  5. package/dist/src/commands/mcp.js +12 -1
  6. package/dist/src/commands/status.js +2 -1
  7. package/dist/src/mcp-server.d.ts +9 -0
  8. package/dist/src/mcp-server.js +23 -6
  9. package/dist/src/mcp-tools/agentbbs-federation.js +8 -2
  10. package/dist/src/mcp-tools/agentbbs-tools.js +10 -2
  11. package/dist/src/mcp-tools/hooks-tools.js +1 -0
  12. package/dist/src/mcp-tools/http-fetch-tools.d.ts +1 -1
  13. package/dist/src/mcp-tools/http-fetch-tools.js +95 -9
  14. package/dist/src/mcp-tools/system-tools.js +1 -0
  15. package/dist/src/mcp-tools/task-tools.js +3 -1
  16. package/dist/src/mcp-tools/workflow-tools.js +3 -1
  17. package/dist/src/mods/claude-installs.js +108 -3
  18. package/dist/src/services/worker-daemon.d.ts +3 -1
  19. package/dist/src/services/worker-daemon.js +5 -3
  20. package/node_modules/@claude-flow/mcp/dist/transport/websocket.d.ts +2 -0
  21. package/node_modules/@claude-flow/mcp/dist/transport/websocket.d.ts.map +1 -1
  22. package/node_modules/@claude-flow/mcp/dist/transport/websocket.js +28 -3
  23. package/node_modules/@claude-flow/mcp/dist/transport/websocket.js.map +1 -1
  24. package/node_modules/@claude-flow/mcp/package.json +1 -1
  25. package/node_modules/@claude-flow/plugin-agent-federation/README.md +1 -1
  26. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
  27. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
  28. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.d.ts +8 -0
  29. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.d.ts.map +1 -1
  30. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.js +11 -1
  31. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.js.map +1 -1
  32. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.d.ts +4 -0
  33. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.d.ts.map +1 -1
  34. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.js +16 -3
  35. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.js.map +1 -1
  36. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
  37. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +17 -4
  38. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
  39. package/node_modules/@claude-flow/plugin-agent-federation/dist/transport/midstream-aware-loader.d.ts.map +1 -1
  40. package/node_modules/@claude-flow/plugin-agent-federation/dist/transport/midstream-aware-loader.js +3 -1
  41. package/node_modules/@claude-flow/plugin-agent-federation/dist/transport/midstream-aware-loader.js.map +1 -1
  42. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
  43. package/package.json +5 -5
  44. package/plugins/ruflo-console/dist/tui.mjs +154 -154
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.63.3",
3
+ "version": "3.63.5",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
6
6
  "hook-handler.cjs": "24502a3bcb78770971147940515c992e35b6736ae14ee2dc54e07d4cdef6a1d8",
@@ -9,6 +9,6 @@
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "Zz2QVu1NSwEjxVSpJ/h5IBcWqHfoOeTlJl0msaJ6ugwPelkwD+ejKGQ/weOBp++gxuXnFExbWmEKZOe/HMt1Cw==",
12
+ "signature": "G+rxSA+IVcyJKtcRpr+UAcy53YlkPTcC5WI9HN0Cdo+KHHGuh2dTOKGVg2Xw2Y09tJYdeFwymgLRTf9hxb/1DA==",
13
13
  "algorithm": "ed25519"
14
14
  }
package/README.md CHANGED
@@ -244,7 +244,7 @@ This adds slash commands and agent definitions. `ruflo-core` (installed above) d
244
244
  | [**ruflo-browser**](plugins/ruflo-browser/README.md) | Automate browser testing with Playwright |
245
245
  | [**ruflo-jujutsu**](plugins/ruflo-jujutsu/README.md) | Analyze git diffs, score risk, suggest reviewers |
246
246
  | [**ruflo-docs**](plugins/ruflo-docs/README.md) | Generate and maintain documentation automatically |
247
- | [**agentic-qe-fleet**](https://github.com/proffesor-for-testing/agentic-qe/tree/v3.15.13/plugins/agentic-qe-fleet) | External, maintained by [Agentic QE](https://github.com/proffesor-for-testing/agentic-qe): 11 QE agents, test generation, coverage, chaos and TDD skills, the `agentic-qe` MCP server, and a guard for AQE learning data (`/plugin install agentic-qe-fleet@ruflo`). Install it from one marketplace only, `agentic-qe` or `ruflo`: enabled from both, its mod and commands load twice |
247
+ | [**agentic-qe-fleet**](https://github.com/proffesor-for-testing/agentic-qe/tree/v3.15.15/plugins/agentic-qe-fleet) | External, maintained by [Agentic QE](https://github.com/proffesor-for-testing/agentic-qe): 11 QE agents, test generation, coverage, chaos and TDD skills, the `agentic-qe` MCP server, and a guard for AQE learning data (`/plugin install agentic-qe-fleet@ruflo`). Install it from one marketplace only, `agentic-qe` or `ruflo`: enabled from both, its mod and commands load twice |
248
248
 
249
249
  #### Security & Compliance
250
250
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-26T16:17:30.971Z",
5
- "gitSha": "29b9894d",
4
+ "generatedAt": "2026-10-27T19:04:24.259Z",
5
+ "gitSha": "d635d422",
6
6
  "catalog": {
7
7
  "agents": 177,
8
8
  "tools": 444,
@@ -2489,7 +2489,7 @@ const workerStatusCommand = {
2489
2489
  });
2490
2490
  if (result.summary) {
2491
2491
  output.writeln();
2492
- output.writeln(`Total: ${result.summary.total} | Running: ${output.highlight(String(result.summary.running))} | Completed: ${output.success(String(result.summary.completed))} | Failed: ${output.error(String(result.summary.failed))}`);
2492
+ output.writeln(`Total: ${result.summary.total} | Pending: ${String(result.summary.pending ?? 0)} | Running: ${output.highlight(String(result.summary.running))} | Completed: ${output.success(String(result.summary.completed))} | Failed: ${output.error(String(result.summary.failed))}`);
2493
2493
  }
2494
2494
  }
2495
2495
  else {
@@ -8,7 +8,7 @@
8
8
  import { output } from '../output.js';
9
9
  import { confirm } from '../prompt.js';
10
10
  import { installParentDeathWatchdog } from '../runtime/parent-death-watchdog.js';
11
- import { getServerManager, getMCPServerStatus, resolveMcpHttpAuthToken, isLoopbackHost, filterAdvertisedMcpTools, parseMcpToolSelection, } from '../mcp-server.js';
11
+ import { getServerManager, getMCPServerStatus, resolveMcpHttpAuthToken, ensureDefaultMcpToken, isUnauthenticatedHttpAllowed, isLoopbackHost, filterAdvertisedMcpTools, parseMcpToolSelection, } from '../mcp-server.js';
12
12
  import { listMCPTools, callMCPTool, hasTool } from '../mcp-client.js';
13
13
  // MCP tools categories
14
14
  const TOOL_CATEGORIES = [
@@ -187,6 +187,13 @@ const startCommand = {
187
187
  output.printError(error.message);
188
188
  return { success: false, exitCode: 1 };
189
189
  }
190
+ // Network transports never start open by default: with no token given, use the per-user one.
191
+ let defaultTokenFile;
192
+ if (!authToken && (transport === 'http' || transport === 'websocket') && !isUnauthenticatedHttpAllowed()) {
193
+ const made = ensureDefaultMcpToken();
194
+ authToken = made.token;
195
+ defaultTokenFile = made.file;
196
+ }
190
197
  const daemon = ctx.flags.daemon ?? false;
191
198
  const force = ctx.flags.force ?? false;
192
199
  output.writeln();
@@ -304,6 +311,8 @@ const startCommand = {
304
311
  output.writeln(output.dim(` RPC: http://${host}:${port}/rpc`));
305
312
  if (authToken) {
306
313
  output.writeln(output.dim(' Auth: bearer token required (Authorization: Bearer <token>); /health is public'));
314
+ if (defaultTokenFile)
315
+ output.writeln(output.dim(` Token (none was given): ${defaultTokenFile} - set RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP=1 to run without one`));
307
316
  }
308
317
  else if (isLoopbackHost(host)) {
309
318
  output.printWarning('No auth token set: any local process (and DNS-rebinding web pages) can call tools. Set RUFLO_MCP_HTTP_TOKEN to require a bearer token.');
@@ -314,6 +323,8 @@ const startCommand = {
314
323
  }
315
324
  else if (transport === 'websocket') {
316
325
  output.writeln(output.dim(` WebSocket: ws://${host}:${port}/ws`));
326
+ if (authToken)
327
+ output.writeln(output.dim(` Auth: bearer token required (Authorization: Bearer <token> or ?token=)${defaultTokenFile ? `; token in ${defaultTokenFile}` : ''}`));
317
328
  }
318
329
  if (daemon) {
319
330
  output.writeln(output.dim(' Running in background mode'));
@@ -152,7 +152,7 @@ async function getSystemStatus() {
152
152
  backend: 'none',
153
153
  performance: { searchTime: 0, cacheHitRate: 0 }
154
154
  },
155
- tasks: { total: 0, pending: 0, running: 0, completed: 0, failed: 0 },
155
+ tasks: { total: 0, pending: 0, running: 0, completed: 0, failed: 0, cancelled: 0 },
156
156
  performance: {
157
157
  cpuUsage: 0,
158
158
  memoryUsage: 0,
@@ -217,6 +217,7 @@ function displayStatus(status) {
217
217
  { status: 'Running', count: status.tasks.running },
218
218
  { status: 'Completed', count: status.tasks.completed },
219
219
  { status: 'Failed', count: status.tasks.failed },
220
+ { status: 'Cancelled', count: status.tasks.cancelled },
220
221
  { status: output.bold('Total'), count: status.tasks.total }
221
222
  ]
222
223
  });
@@ -77,6 +77,15 @@ export declare function isUnauthenticatedHttpAllowed(env?: NodeJS.ProcessEnv): b
77
77
  * refused, with or without a token. Exported for tests.
78
78
  */
79
79
  export declare function shouldRefuseUnauthenticatedHttp(host: string, env?: NodeJS.ProcessEnv, authenticated?: boolean): boolean;
80
+ /**
81
+ * The bearer token a token-less `mcp start -t http|websocket` falls back to, so that a loopback
82
+ * server is never open to every local process. Kept in `~/.claude-flow/mcp-http-token` (mode 0600)
83
+ * so clients can read it; created on first use. Exported for tests.
84
+ */
85
+ export declare function ensureDefaultMcpToken(homeDir?: string): {
86
+ token: string;
87
+ file: string;
88
+ };
80
89
  /**
81
90
  * Resolve the HTTP bearer token. Precedence: explicit value (CLI flag), token
82
91
  * file, then RUFLO_MCP_HTTP_TOKEN. An unset/empty env var means "no token";
@@ -19,7 +19,7 @@
19
19
  import { EventEmitter } from 'events';
20
20
  import { execFileSync } from 'child_process';
21
21
  import { request as httpRequestFn } from 'http';
22
- import { randomUUID } from 'crypto';
22
+ import { randomUUID, randomBytes } from 'crypto';
23
23
  import * as path from 'path';
24
24
  import * as fs from 'fs';
25
25
  import * as os from 'os';
@@ -158,6 +158,28 @@ function raisedRateLimits(options) {
158
158
  }
159
159
  return raised;
160
160
  }
161
+ /**
162
+ * The bearer token a token-less `mcp start -t http|websocket` falls back to, so that a loopback
163
+ * server is never open to every local process. Kept in `~/.claude-flow/mcp-http-token` (mode 0600)
164
+ * so clients can read it; created on first use. Exported for tests.
165
+ */
166
+ export function ensureDefaultMcpToken(homeDir = os.homedir()) {
167
+ const dir = path.join(homeDir, '.claude-flow');
168
+ const file = path.join(dir, 'mcp-http-token');
169
+ try {
170
+ const existing = fs.readFileSync(file, 'utf8').trim();
171
+ if (AUTH_TOKEN_PATTERN.test(existing))
172
+ return { token: existing, file };
173
+ }
174
+ catch {
175
+ // none yet
176
+ }
177
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
178
+ const token = randomBytes(32).toString('hex');
179
+ fs.writeFileSync(file, token + '\n', { mode: 0o600 });
180
+ fs.chmodSync(file, 0o600);
181
+ return { token, file };
182
+ }
161
183
  /** Printable ASCII without space (RFC 6750 token-ish), 16-512 chars. */
162
184
  const AUTH_TOKEN_PATTERN = /^[\x21-\x7e]{16,512}$/;
163
185
  /**
@@ -824,11 +846,6 @@ export class MCPServerManager extends EventEmitter {
824
846
  // Authorization gate first, before any import or bind: a refused start must
825
847
  // not depend on (or touch) anything else.
826
848
  const authToken = this.options.authToken || undefined;
827
- if (authToken && this.options.transport !== 'http') {
828
- // The standalone websocket transport has no working token check, so a
829
- // token there would be a false promise of protection.
830
- throw new Error('An MCP HTTP auth token is only supported with --transport http');
831
- }
832
849
  if (shouldRefuseUnauthenticatedHttp(this.options.host, process.env, !!authToken)) {
833
850
  // No ToolAuthorizer is wired below: @claude-flow/mcp's tool-call
834
851
  // authorization is fully opt-in (requireToolAuthorization/toolAuthorizer),
@@ -51,7 +51,7 @@
51
51
  * @module @claude-flow/cli/mcp-tools/agentbbs-federation
52
52
  */
53
53
  import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, chmodSync } from 'node:fs';
54
- import { join } from 'node:path';
54
+ import { isAbsolute, join, relative, resolve } from 'node:path';
55
55
  import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
56
56
  import { createRequire } from 'node:module';
57
57
  // Lazy, CommonJS-style load of node:http: an ESM `import … from 'node:http'`
@@ -132,7 +132,13 @@ function peersPath(basePath) {
132
132
  return join(basePath, 'peers.json');
133
133
  }
134
134
  function roomLogPath(basePath, roomId) {
135
- return join(basePath, `room-${roomId}.jsonl`);
135
+ // The room id reaches a file name: validate it, then make sure the result stays under basePath.
136
+ validateRoomId(roomId);
137
+ const file = join(basePath, `room-${roomId}.jsonl`);
138
+ const rel = relative(resolve(basePath), resolve(file));
139
+ if (rel.startsWith('..') || isAbsolute(rel))
140
+ throw new Error('Invalid room id');
141
+ return file;
136
142
  }
137
143
  /**
138
144
  * Load or create this host's long-lived Ed25519 identity.
@@ -35,7 +35,7 @@
35
35
  * @module @claude-flow/cli/mcp-tools/agentbbs
36
36
  */
37
37
  import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, statSync, openSync, readSync, closeSync } from 'node:fs';
38
- import { resolve, isAbsolute, join } from 'node:path';
38
+ import { resolve, isAbsolute, join, relative } from 'node:path';
39
39
  import { randomBytes, createHash } from 'node:crypto';
40
40
  import { execFileSync } from 'node:child_process';
41
41
  import { getNodeIdentity, signEnvelope, addPeer, removePeer, readPeers, syncRoomFromPeer, serveFederation, validateRoomId as fedValidateRoomId, MAX_PEERS, MAX_HOPS, } from './agentbbs-federation.js';
@@ -88,6 +88,9 @@ function validateRoomLabel(label) {
88
88
  if (!/^[A-Za-z0-9_.\-:/@#]+$/.test(label)) {
89
89
  throw new Error('roomLabel may only contain [A-Za-z0-9_.\\-:/@#]');
90
90
  }
91
+ // The label becomes part of the room id, and the room id part of a file name.
92
+ if (label.includes('..'))
93
+ throw new Error('roomLabel must not contain ..');
91
94
  return label;
92
95
  }
93
96
  function validateRoomId(roomId) {
@@ -120,7 +123,12 @@ function roomIdFromLabel(label) {
120
123
  return `${norm}-${h}`;
121
124
  }
122
125
  function roomLogPath(basePath, roomId) {
123
- return join(basePath, `room-${roomId}.jsonl`);
126
+ validateRoomId(roomId);
127
+ const file = join(basePath, `room-${roomId}.jsonl`);
128
+ const rel = relative(resolve(basePath), resolve(file));
129
+ if (rel.startsWith('..') || isAbsolute(rel))
130
+ throw new Error('roomId escapes the BBS directory');
131
+ return file;
124
132
  }
125
133
  function roomsRegistryPath(basePath) {
126
134
  return join(basePath, 'rooms.json');
@@ -4817,6 +4817,7 @@ export const hooksWorkerStatus = {
4817
4817
  workers,
4818
4818
  summary: {
4819
4819
  total: workers.length,
4820
+ pending: workers.filter(w => w.status === 'pending').length,
4820
4821
  running: workers.filter(w => w.status === 'running').length,
4821
4822
  completed: workers.filter(w => w.status === 'completed').length,
4822
4823
  failed: workers.filter(w => w.status === 'failed').length,
@@ -15,7 +15,7 @@
15
15
  * - DEFAULT-REFUSES Authorization / Cookie / X-Auth-* (CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 opt-in)
16
16
  * - hard timeout 30s default, 60s ceiling
17
17
  * - response truncated to 256KB default, 1MB ceiling
18
- * - no redirects auto-followed beyond fetch's default; status reported as-is
18
+ * - redirects followed manually, max 10, every hop re-validated by the same allowlist
19
19
  *
20
20
  * @module @claude-flow/cli/mcp-tools/http-fetch
21
21
  */
@@ -15,7 +15,7 @@
15
15
  * - DEFAULT-REFUSES Authorization / Cookie / X-Auth-* (CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 opt-in)
16
16
  * - hard timeout 30s default, 60s ceiling
17
17
  * - response truncated to 256KB default, 1MB ceiling
18
- * - no redirects auto-followed beyond fetch's default; status reported as-is
18
+ * - redirects followed manually, max 10, every hop re-validated by the same allowlist
19
19
  *
20
20
  * @module @claude-flow/cli/mcp-tools/http-fetch
21
21
  */
@@ -23,6 +23,8 @@ const DEFAULT_TIMEOUT_MS = 30_000;
23
23
  const MAX_TIMEOUT_MS = 60_000;
24
24
  const DEFAULT_MAX_RESPONSE_BYTES = 256 * 1024; // 256 KB
25
25
  const MAX_RESPONSE_BYTES = 1024 * 1024; // 1 MB
26
+ const MAX_REDIRECTS = 10;
27
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
26
28
  const DEFAULT_USER_AGENT = 'ruflo-http-fetch/1.0';
27
29
  const FORBIDDEN_HEADER_PREFIXES = ['x-auth-', 'x-api-key'];
28
30
  const FORBIDDEN_HEADERS_EXACT = ['authorization', 'cookie', 'set-cookie', 'proxy-authorization'];
@@ -230,13 +232,97 @@ export async function httpFetchExecute(input) {
230
232
  const controller = new AbortController();
231
233
  const timer = setTimeout(() => controller.abort(), timeoutMs);
232
234
  try {
233
- const response = await fetch(parsed.toString(), {
234
- method,
235
- headers,
236
- body: method === 'GET' || method === 'HEAD' ? undefined : body,
237
- signal: controller.signal,
238
- // Keep redirect default ('follow') — caller sees final status/url.
239
- });
235
+ let currentUrl = parsed;
236
+ let currentMethod = method;
237
+ let currentBody = body;
238
+ let response;
239
+ let redirects = 0;
240
+ // Do not let fetch follow redirects on its own: only the initial URL was
241
+ // validated, so an automatic follow could reach loopback/RFC1918 or switch
242
+ // protocols after passing the allowlist. Every hop is validated with the
243
+ // same rules; sensitive headers are dropped when the origin changes.
244
+ while (true) {
245
+ response = await fetch(currentUrl.toString(), {
246
+ method: currentMethod,
247
+ headers,
248
+ body: currentMethod === 'GET' || currentMethod === 'HEAD' ? undefined : currentBody,
249
+ signal: controller.signal,
250
+ redirect: 'manual',
251
+ });
252
+ if (!REDIRECT_STATUSES.has(response.status))
253
+ break;
254
+ const location = response.headers.get('location');
255
+ if (!location)
256
+ break;
257
+ if (redirects++ >= MAX_REDIRECTS) {
258
+ try {
259
+ await response.body?.cancel();
260
+ }
261
+ catch { /* ignore */ }
262
+ return {
263
+ success: false,
264
+ status: response.status,
265
+ statusText: response.statusText,
266
+ headers: {},
267
+ body: '',
268
+ bodyTruncated: false,
269
+ bytesRead: 0,
270
+ durationMs: Date.now() - startedAt,
271
+ url: currentUrl.toString(),
272
+ method,
273
+ error: `too many redirects (max ${MAX_REDIRECTS})`,
274
+ errorCode: 'REDIRECT_LIMIT',
275
+ };
276
+ }
277
+ let nextUrl;
278
+ try {
279
+ nextUrl = validateUrl(new URL(location, currentUrl).toString());
280
+ }
281
+ catch (e) {
282
+ try {
283
+ await response.body?.cancel();
284
+ }
285
+ catch { /* ignore */ }
286
+ const err = e;
287
+ return {
288
+ success: false,
289
+ status: response.status,
290
+ statusText: response.statusText,
291
+ headers: {},
292
+ body: '',
293
+ bodyTruncated: false,
294
+ bytesRead: 0,
295
+ durationMs: Date.now() - startedAt,
296
+ url: currentUrl.toString(),
297
+ method,
298
+ error: `redirect blocked: ${err.message}`,
299
+ errorCode: err.code ?? 'REDIRECT_BLOCKED',
300
+ };
301
+ }
302
+ // The redirect response body is not returned; release the connection
303
+ // before issuing the next request. A body-less 3xx is also fine.
304
+ try {
305
+ await response.body?.cancel();
306
+ }
307
+ catch { /* ignore */ }
308
+ if (nextUrl.origin !== currentUrl.origin) {
309
+ for (const key of Object.keys(headers)) {
310
+ const lower = key.toLowerCase();
311
+ if (lower === 'authorization' || lower === 'cookie' || lower === 'proxy-authorization') {
312
+ delete headers[key];
313
+ }
314
+ }
315
+ }
316
+ // Match fetch's redirect method rules: 303 becomes GET except for
317
+ // GET/HEAD, while 301/302 only rewrite POST to GET. 307/308 preserve
318
+ // both method and body.
319
+ if ((response.status === 303 && currentMethod !== 'GET' && currentMethod !== 'HEAD') ||
320
+ ((response.status === 301 || response.status === 302) && currentMethod === 'POST')) {
321
+ currentMethod = 'GET';
322
+ currentBody = undefined;
323
+ }
324
+ currentUrl = nextUrl;
325
+ }
240
326
  // Drain the body with byte budget. Use array-buffer first to bound size
241
327
  // deterministically before decoding to UTF-8.
242
328
  const reader = response.body?.getReader();
@@ -286,7 +372,7 @@ export async function httpFetchExecute(input) {
286
372
  bodyTruncated,
287
373
  bytesRead,
288
374
  durationMs: Date.now() - startedAt,
289
- url: parsed.toString(),
375
+ url: currentUrl.toString(),
290
376
  method,
291
377
  };
292
378
  }
@@ -633,6 +633,7 @@ export const systemTools = [
633
633
  running: tasks.filter(t => t.status === 'in_progress').length,
634
634
  completed: tasks.filter(t => t.status === 'completed').length,
635
635
  failed: tasks.filter(t => t.status === 'failed').length,
636
+ cancelled: tasks.filter(t => t.status === 'cancelled').length,
636
637
  };
637
638
  },
638
639
  },
@@ -208,7 +208,9 @@ export const taskTools = [
208
208
  // Apply filters
209
209
  if (input.status) {
210
210
  // Support comma-separated status values. 'all' (sent by `status tasks`) means no status filter.
211
- const statuses = input.status.split(',').map(s => s.trim());
211
+ // Each value goes through statusOf, the reader task_update uses, so "done", "canceled",
212
+ // "running" or "Completed" match the stored status rather than matching nothing.
213
+ const statuses = input.status.split(',').map(s => s.trim()).map(s => statusOf(s) ?? s);
212
214
  if (!statuses.includes('all')) {
213
215
  tasks = tasks.filter(t => statuses.includes(t.status));
214
216
  }
@@ -20,7 +20,9 @@ function parseCondition(when) {
20
20
  if (expression === 'true' || expression === 'false') {
21
21
  return { constant: expression === 'true' };
22
22
  }
23
- const match = expression.match(/^([a-zA-Z_]\w*)\s*===?\s*(?:'([^']*)'|"([^"]*)"|(-?\d+(?:\.\d+)?)|(true|false))$/);
23
+ // Variable names follow the `{{…}}` interpolation syntax, so a condition can
24
+ // read a task step's output, stored as `<stepId>.output` (`step-1.output`).
25
+ const match = expression.match(/^([a-zA-Z_][\w.-]*)\s*===?\s*(?:'([^']*)'|"([^"]*)"|(-?\d+(?:\.\d+)?)|(true|false))$/);
24
26
  if (!match) {
25
27
  return new Error('condition config.when must compare a variable to a quoted string, number, or boolean using == or ===');
26
28
  }
@@ -7,8 +7,8 @@
7
7
  * (a 2.1.107 npm-global beside a current native one, say) silently decides.
8
8
  */
9
9
  import { execFileSync } from 'node:child_process';
10
- import { existsSync, realpathSync, statSync } from 'node:fs';
11
- import { delimiter, join } from 'node:path';
10
+ import { closeSync, existsSync, fstatSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
11
+ import { delimiter, dirname, join, resolve } from 'node:path';
12
12
  export const MODS_DEFAULT_ON = '2.1.287';
13
13
  export const MODS_FIRST = '2.1.277';
14
14
  const parse = (v) => v.split('.').map((n) => Number(n));
@@ -52,6 +52,98 @@ export const defaultVersionOf = (path) => {
52
52
  return null;
53
53
  }
54
54
  };
55
+ /** Known npm/cmd-shim Node launcher forms (legacy basedir and basedir_win).
56
+ * Match the whole template, not comments or arbitrary shell code. Unknown npm
57
+ * versions/formats simply keep the independent probe result. Never execute it.
58
+ */
59
+ const npmCmdShim = (entry, modern) => [
60
+ '@ECHO off', 'GOTO start', ':find_dp0', 'SET dp0=%~dp0', 'EXIT /b', ':start',
61
+ 'SETLOCAL', 'CALL :find_dp0', '', 'IF EXIST "%dp0%\\node.exe" (',
62
+ ' SET "_prog=%dp0%\\node.exe"', ') ELSE (', ' SET "_prog=node"',
63
+ ...(!modern ? [' SET PATHEXT=%PATHEXT:;.JS;=;%'] : []), ')', '',
64
+ 'endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & '
65
+ + (modern ? 'set PATHEXT=%PATHEXT:;.JS;=;% & ' : '')
66
+ + `"%_prog%" "%dp0%\\${entry.replaceAll('/', '\\')}" %*`, '',
67
+ ].join('\r\n');
68
+ const npmPosixShim = (entry, modern) => [
69
+ '#!/bin/sh', 'basedir=$(dirname "$(echo "$0" | sed -e \'s,\\\\,/,g\')")',
70
+ ...(modern ? ['basedir_win="$basedir"'] : []), '',
71
+ modern ? 'case `uname -a` in' : 'case `uname` in',
72
+ ...(modern ? [
73
+ ' *CYGWIN*|*MINGW*|*MSYS*)', ' if command -v cygpath > /dev/null 2>&1; then',
74
+ ' basedir_win=`cygpath -w "$basedir"`', ' fi', ' ;;', ' *WSL2*)',
75
+ ' if command -v wslpath > /dev/null 2>&1; then',
76
+ ' basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"',
77
+ ' if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then',
78
+ ' echo "Error: wslpath failed to convert path. WSL environment may be misconfigured." >&2',
79
+ ' exit 1', ' fi', ' fi', ' ;;',
80
+ ] : [
81
+ ' *CYGWIN*|*MINGW*|*MSYS*)', ' if command -v cygpath > /dev/null 2>&1; then',
82
+ ' basedir=`cygpath -w "$basedir"`', ' fi', ' ;;',
83
+ ]), 'esac', '',
84
+ ...(modern ? [
85
+ 'PROG_EXE="$basedir/node.exe"', 'if ! [ -x "$PROG_EXE" ]; then',
86
+ ' PROG_EXE="$basedir/node"', ' if ! [ -x "$PROG_EXE" ]; then',
87
+ ' PROG_EXE=node', ' if ! [ -x "$PROG_EXE" ]; then', ' PROG_EXE=node.exe',
88
+ ' fi', ' fi', 'fi', '', `exec "$PROG_EXE" "$basedir_win/${entry}" "$@"`,
89
+ ] : [
90
+ 'if [ -x "$basedir/node" ]; then', ` exec "$basedir/node" "$basedir/${entry}" "$@"`,
91
+ 'else ', ` exec node "$basedir/${entry}" "$@"`, 'fi',
92
+ ]), '',
93
+ ].join('\n');
94
+ // A no-shebang/native bin uses npm's direct-exec forms, not a Node wrapper.
95
+ const npmDirectCmdShim = (entry) => [
96
+ '@ECHO off', 'GOTO start', ':find_dp0', 'SET dp0=%~dp0', 'EXIT /b', ':start',
97
+ 'SETLOCAL', 'CALL :find_dp0', `"%dp0%\\${entry.replaceAll('/', '\\')}" %*`, '',
98
+ ].join('\r\n');
99
+ const npmDirectPosixShim = (entry, modern) => {
100
+ const nodeShim = npmPosixShim(entry, modern);
101
+ const preamble = nodeShim.slice(0, nodeShim.indexOf(modern ? 'PROG_EXE=' : 'if [ -x'));
102
+ return preamble + `exec "$basedir/${entry}" "$@"\n`;
103
+ };
104
+ function npmShimTarget(path, cmd) {
105
+ let fd;
106
+ try {
107
+ fd = openSync(path, 'r');
108
+ const stat = fstatSync(fd);
109
+ if (!stat.isFile() || stat.size > 64 * 1024)
110
+ return null;
111
+ // Also cap the actual read if a file grows after stat.
112
+ const bytes = Buffer.alloc(64 * 1024 + 1);
113
+ let size = 0;
114
+ while (size < bytes.length) {
115
+ const n = readSync(fd, bytes, size, bytes.length - size, null);
116
+ if (!n)
117
+ break;
118
+ size += n;
119
+ }
120
+ if (size > 64 * 1024)
121
+ return null;
122
+ const text = bytes.subarray(0, size).toString('utf8').replace(/\r\n/g, '\n').trimEnd();
123
+ const targets = cmd
124
+ ? [...text.matchAll(/"%dp0%\\([^"\r\n]+)" +%\*$/gm)].map(m => m[1].replaceAll('\\', '/'))
125
+ : [...text.matchAll(/^\s*exec (?:[^\r\n]* )?"\$basedir(?:_win)?\/([^"\r\n]+)" +"\$@"$/gm)].map(m => m[1]);
126
+ const entry = targets[0];
127
+ if (!entry || targets.some(t => t !== entry) || !/^[a-zA-Z0-9@._ /-]+$/.test(entry) || entry.startsWith('/'))
128
+ return null;
129
+ const template = cmd ? npmCmdShim : npmPosixShim;
130
+ const normalize = (value) => value.replace(/\r\n/g, '\n').trimEnd();
131
+ const node = [false, true].some(modern => normalize(template(entry, modern)) === text);
132
+ const direct = cmd ? normalize(npmDirectCmdShim(entry)) === text
133
+ : [false, true].some(modern => normalize(npmDirectPosixShim(entry, modern)) === text);
134
+ if (!node && !direct)
135
+ return null;
136
+ const target = realpathSync(resolve(dirname(path), entry));
137
+ return statSync(target).isFile() ? { path: target, mode: node ? 'node' : 'direct' } : null;
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ finally {
143
+ if (fd !== undefined)
144
+ closeSync(fd);
145
+ }
146
+ }
55
147
  /** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
56
148
  export function findClaudeInstalls(env, home, versionOf = defaultVersionOf) {
57
149
  const names = process.platform === 'win32' ? ['claude.exe', 'claude.cmd', 'claude'] : ['claude'];
@@ -59,6 +151,7 @@ export function findClaudeInstalls(env, home, versionOf = defaultVersionOf) {
59
151
  const seen = new Set();
60
152
  const installs = [];
61
153
  for (const dir of dirs) {
154
+ let cmd;
62
155
  for (const name of names) {
63
156
  const path = join(dir, name);
64
157
  let real;
@@ -73,7 +166,19 @@ export function findClaudeInstalls(env, home, versionOf = defaultVersionOf) {
73
166
  if (seen.has(real))
74
167
  continue;
75
168
  seen.add(real);
76
- installs.push({ path, version: versionOf(path) });
169
+ let version = versionOf(path);
170
+ if (process.platform === 'win32' && name === 'claude' && version === null && cmd?.version) {
171
+ const target = npmShimTarget(path, false);
172
+ const cmdTarget = npmShimTarget(cmd.path, true);
173
+ if (target && cmdTarget && target.path === cmdTarget.path && target.mode === cmdTarget.mode)
174
+ version = cmd.version;
175
+ }
176
+ const install = { path, version };
177
+ installs.push(install);
178
+ // Only a .cmd actually probed in this directory can supply a version.
179
+ // A realpath-deduplicated alias in another directory proves nothing here.
180
+ if (name === 'claude.cmd')
181
+ cmd = install;
77
182
  }
78
183
  }
79
184
  return installs;
@@ -428,7 +428,9 @@ export declare class WorkerDaemon extends EventEmitter {
428
428
  /**
429
429
  * Manually trigger a worker
430
430
  */
431
- triggerWorker(type: WorkerType): Promise<WorkerResult>;
431
+ triggerWorker(type: WorkerType, opts?: {
432
+ manualTrigger?: boolean;
433
+ }): Promise<WorkerResult>;
432
434
  /**
433
435
  * Enable/disable a worker
434
436
  */
@@ -952,7 +952,7 @@ export class WorkerDaemon extends EventEmitter {
952
952
  }
953
953
  try {
954
954
  this.log('info', `Dequeued ${trigger}${workerId ? ` (id=${workerId})` : ''} from MCP dispatch queue`);
955
- await this.triggerWorker(trigger);
955
+ await this.triggerWorker(trigger, { manualTrigger: false });
956
956
  }
957
957
  catch (err) {
958
958
  this.log('warn', `Queued worker ${trigger} failed: ${err.message}`);
@@ -1926,7 +1926,7 @@ export class WorkerDaemon extends EventEmitter {
1926
1926
  /**
1927
1927
  * Manually trigger a worker
1928
1928
  */
1929
- async triggerWorker(type) {
1929
+ async triggerWorker(type, opts = {}) {
1930
1930
  const workerConfig = this.config.workers.find(w => w.type === type);
1931
1931
  if (!workerConfig) {
1932
1932
  throw new Error(`Unknown worker type: ${type}`);
@@ -1941,7 +1941,9 @@ export class WorkerDaemon extends EventEmitter {
1941
1941
  // #2661 root-fix — an explicit manual trigger bypasses the repository-
1942
1942
  // supervisor gate (still budget/dedup-gated) — see runWorkerLogic()'s
1943
1943
  // doc comment.
1944
- return this.executeWorker(workerConfig, { manualTrigger: true });
1944
+ // Only an operator's own `daemon trigger` is manual. A file in daemon-queue/ can come from a
1945
+ // cloned repo, so queue runs pass `manualTrigger: false` and stay behind the supervisor gate.
1946
+ return this.executeWorker(workerConfig, { manualTrigger: opts.manualTrigger ?? true });
1945
1947
  }
1946
1948
  /**
1947
1949
  * Enable/disable a worker
@@ -58,6 +58,8 @@ export declare class WebSocketTransport extends EventEmitter implements ITranspo
58
58
  getClientInfo(clientId: string): ClientConnection | undefined;
59
59
  disconnectClient(clientId: string, reason?: string): boolean;
60
60
  private setupWebSocketHandlers;
61
+ /** Token from `Authorization: Bearer` or `?token=`, compared in constant time. */
62
+ private upgradeHasValidToken;
61
63
  private handleMessage;
62
64
  private parseMessage;
63
65
  private serializeMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/transport/websocket.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/transport/websocket.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAmB,SAAS,EAAW,MAAM,IAAI,CAAC;AAIzD,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAGb,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,OAAO,EACP,UAAU,EACX,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,6EAA6E;IAC7E,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,SAAS,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,IAAI,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,qBAAa,kBAAmB,SAAQ,YAAa,YAAW,UAAU;IAkBtE,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAlBzB,SAAgB,IAAI,EAAE,aAAa,CAAe;IAElD,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,mBAAmB,CAAC,CAAsB;IAClD,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,GAAG,CAAC,CAAkB;IAC9B,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAAK;IAE9B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,gBAAgB,CAAK;gBAGV,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,wBAAwB;IAK7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA8CtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkC3B,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAIxC,cAAc,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI;IAI5C,eAAe,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAajD,gBAAgB,CAAC,YAAY,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9D,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBrF,UAAU,IAAI,MAAM,EAAE;IAItB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAI7D,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAA2B,GAAG,OAAO;IAc9E,OAAO,CAAC,sBAAsB;IA4D9B,kFAAkF;IAClF,OAAO,CAAC,oBAAoB;YAiBd,aAAa;IAuE3B,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,cAAc;IAsBtB,OAAO,CAAC,aAAa;CAMtB;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,wBAAwB,GAC/B,kBAAkB,CAEpB"}