@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.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/README.md +1 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/hooks.js +1 -1
- package/dist/src/commands/mcp.js +12 -1
- package/dist/src/commands/status.js +2 -1
- package/dist/src/mcp-server.d.ts +9 -0
- package/dist/src/mcp-server.js +23 -6
- package/dist/src/mcp-tools/agentbbs-federation.js +8 -2
- package/dist/src/mcp-tools/agentbbs-tools.js +10 -2
- package/dist/src/mcp-tools/hooks-tools.js +1 -0
- package/dist/src/mcp-tools/http-fetch-tools.d.ts +1 -1
- package/dist/src/mcp-tools/http-fetch-tools.js +95 -9
- package/dist/src/mcp-tools/system-tools.js +1 -0
- package/dist/src/mcp-tools/task-tools.js +3 -1
- package/dist/src/mcp-tools/workflow-tools.js +3 -1
- package/dist/src/mods/claude-installs.js +108 -3
- package/dist/src/services/worker-daemon.d.ts +3 -1
- package/dist/src/services/worker-daemon.js +5 -3
- package/node_modules/@claude-flow/mcp/dist/transport/websocket.d.ts +2 -0
- package/node_modules/@claude-flow/mcp/dist/transport/websocket.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/websocket.js +28 -3
- package/node_modules/@claude-flow/mcp/dist/transport/websocket.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/README.md +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.d.ts +8 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.js +11 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.d.ts +4 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.js +16 -3
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +17 -4
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/transport/midstream-aware-loader.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/transport/midstream-aware-loader.js +3 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/transport/midstream-aware-loader.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
- package/package.json +5 -5
- package/plugins/ruflo-console/dist/tui.mjs +154 -154
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.63.
|
|
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": "
|
|
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.
|
|
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
|
|
package/catalog-manifest.json
CHANGED
|
@@ -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 {
|
package/dist/src/commands/mcp.js
CHANGED
|
@@ -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
|
});
|
package/dist/src/mcp-server.d.ts
CHANGED
|
@@ -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";
|
package/dist/src/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
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
|
-
* -
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAmB,SAAS,EAAW,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/transport/websocket.ts"],"names":[],"mappings":"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"}
|