@claude-flow/cli 3.57.2 → 3.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/README.md +1 -0
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/agent.js +4 -1
- package/dist/src/commands/mcp.js +66 -6
- package/dist/src/commands/memory.js +53 -2
- package/dist/src/mcp-server.d.ts +5 -0
- package/dist/src/mcp-server.js +39 -0
- package/dist/src/mcp-tools/agent-tools.js +28 -0
- package/dist/src/mcp-tools/claims-tools.js +25 -15
- package/dist/src/memory/memory-initializer.js +43 -4
- package/dist/src/memory/memory-peek.d.ts +28 -0
- package/dist/src/memory/memory-peek.js +222 -0
- package/dist/src/memory/model-cache-dir.d.ts +11 -0
- package/dist/src/memory/model-cache-dir.js +17 -0
- package/dist/src/ruvector/task-embedder.js +38 -4
- package/node_modules/@claude-flow/codex/README.md +78 -0
- package/node_modules/@claude-flow/codex/dist/activity/cli.d.ts +3 -0
- package/node_modules/@claude-flow/codex/dist/activity/cli.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/cli.js +115 -0
- package/node_modules/@claude-flow/codex/dist/activity/cli.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/files.d.ts +14 -0
- package/node_modules/@claude-flow/codex/dist/activity/files.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/files.js +142 -0
- package/node_modules/@claude-flow/codex/dist/activity/files.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/parser.d.ts +21 -0
- package/node_modules/@claude-flow/codex/dist/activity/parser.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/parser.js +179 -0
- package/node_modules/@claude-flow/codex/dist/activity/parser.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/reader.d.ts +20 -0
- package/node_modules/@claude-flow/codex/dist/activity/reader.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/reader.js +106 -0
- package/node_modules/@claude-flow/codex/dist/activity/reader.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/sanitize.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/activity/sanitize.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/sanitize.js +27 -0
- package/node_modules/@claude-flow/codex/dist/activity/sanitize.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/types.d.ts +31 -0
- package/node_modules/@claude-flow/codex/dist/activity/types.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/types.js +5 -0
- package/node_modules/@claude-flow/codex/dist/activity/types.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/viewer.d.ts +21 -0
- package/node_modules/@claude-flow/codex/dist/activity/viewer.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/viewer.js +168 -0
- package/node_modules/@claude-flow/codex/dist/activity/viewer.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/cli.js +3 -0
- package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/server.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/server.js +5 -3
- package/node_modules/@claude-flow/mcp/dist/server.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/types.d.ts +11 -0
- package/node_modules/@claude-flow/mcp/dist/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ruflo-console/dist/tui.mjs +124 -124
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.58.0",
|
|
4
4
|
"files": {
|
|
5
5
|
"auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
|
|
6
6
|
"hook-handler.cjs": "bc12b3cae92d5a7c8deef699fa00d0ef07b57625c18dbeac4387f15725b8e09f",
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
"router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
|
-
"signature": "
|
|
12
|
+
"signature": "xqclyZcKpZly9uW8nOyUEwAcR+R/7JlzjHdKXYJ5I2lWeBWYWogZZjrijQEQMGSNvN61cVs3Xtyob91K7mxIBQ==",
|
|
13
13
|
"algorithm": "ed25519"
|
|
14
14
|
}
|
package/README.md
CHANGED
|
@@ -240,6 +240,7 @@ This adds slash commands and agent definitions. `ruflo-core` (installed above) d
|
|
|
240
240
|
| [**ruflo-browser**](plugins/ruflo-browser/README.md) | Automate browser testing with Playwright |
|
|
241
241
|
| [**ruflo-jujutsu**](plugins/ruflo-jujutsu/README.md) | Analyze git diffs, score risk, suggest reviewers |
|
|
242
242
|
| [**ruflo-docs**](plugins/ruflo-docs/README.md) | Generate and maintain documentation automatically |
|
|
243
|
+
| [**agentic-qe-fleet**](https://github.com/proffesor-for-testing/agentic-qe/tree/v3.15.10/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 |
|
|
243
244
|
|
|
244
245
|
#### Security & Compliance
|
|
245
246
|
|
package/catalog-manifest.json
CHANGED
|
@@ -136,6 +136,7 @@ const spawnCommand = {
|
|
|
136
136
|
const result = await callMCPTool('agent_spawn', {
|
|
137
137
|
agentType,
|
|
138
138
|
id: agentName,
|
|
139
|
+
name: agentName,
|
|
139
140
|
config: {
|
|
140
141
|
provider: ctx.flags.provider || 'anthropic',
|
|
141
142
|
model: ctx.flags.model,
|
|
@@ -214,7 +215,9 @@ const listCommand = {
|
|
|
214
215
|
try {
|
|
215
216
|
// Call MCP tool to list agents
|
|
216
217
|
const result = await callMCPTool('agent_list', {
|
|
217
|
-
|
|
218
|
+
// agent_list has no 'all' status: --all means "don't hide terminated agents".
|
|
219
|
+
status: ctx.flags.status || undefined,
|
|
220
|
+
includeTerminated: ctx.flags.all ? true : undefined,
|
|
218
221
|
agentType: ctx.flags.type || undefined,
|
|
219
222
|
limit: 100,
|
|
220
223
|
});
|
package/dist/src/commands/mcp.js
CHANGED
|
@@ -34,6 +34,14 @@ function formatUptime(seconds) {
|
|
|
34
34
|
const mins = Math.floor((seconds % 3600) / 60);
|
|
35
35
|
return `${hours}h ${mins}m`;
|
|
36
36
|
}
|
|
37
|
+
function readIntegerOption(flags, name, envName) {
|
|
38
|
+
const camelName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
39
|
+
const input = flags[camelName] ?? flags[name] ?? process.env[envName];
|
|
40
|
+
return input === undefined ? undefined : Number(input);
|
|
41
|
+
}
|
|
42
|
+
function isOutOfRange(value, min, max) {
|
|
43
|
+
return value !== undefined && (!Number.isSafeInteger(value) || value < min || value > max);
|
|
44
|
+
}
|
|
37
45
|
// Start MCP server
|
|
38
46
|
const startCommand = {
|
|
39
47
|
name: 'start',
|
|
@@ -71,6 +79,31 @@ const startCommand = {
|
|
|
71
79
|
description: 'HTTP request timeout in milliseconds (default 30000; env RUFLO_MCP_REQUEST_TIMEOUT_MS)',
|
|
72
80
|
type: 'number'
|
|
73
81
|
},
|
|
82
|
+
{
|
|
83
|
+
name: 'rate-limit-per-ip',
|
|
84
|
+
description: 'HTTP requests allowed per client IP per window (env RUFLO_MCP_RATE_LIMIT_PER_IP)',
|
|
85
|
+
type: 'number'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'rate-limit-window-ms',
|
|
89
|
+
description: 'HTTP rate limit window in milliseconds (env RUFLO_MCP_RATE_LIMIT_WINDOW_MS)',
|
|
90
|
+
type: 'number'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'rate-limit-per-session',
|
|
94
|
+
description: 'Request burst allowed per MCP session, refilled over 10s; also bounded by the server-wide bucket (env RUFLO_MCP_RATE_LIMIT_PER_SESSION)',
|
|
95
|
+
type: 'number'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'rate-limit-global-rps',
|
|
99
|
+
description: 'Server-wide requests per second across all sessions (env RUFLO_MCP_RATE_LIMIT_GLOBAL_RPS)',
|
|
100
|
+
type: 'number'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'rate-limit-global-burst',
|
|
104
|
+
description: 'Server-wide request burst across all sessions (env RUFLO_MCP_RATE_LIMIT_GLOBAL_BURST)',
|
|
105
|
+
type: 'number'
|
|
106
|
+
},
|
|
74
107
|
{
|
|
75
108
|
name: 'auth-token-file',
|
|
76
109
|
description: 'Read the HTTP bearer token from this file (http transport). Preferred over --auth-token; env RUFLO_MCP_HTTP_TOKEN also works',
|
|
@@ -100,6 +133,7 @@ const startCommand = {
|
|
|
100
133
|
{ command: 'claude-flow mcp start', description: 'Start with defaults (stdio)' },
|
|
101
134
|
{ command: 'claude-flow mcp start -p 8080 -t http', description: 'Start HTTP server' },
|
|
102
135
|
{ command: 'claude-flow mcp start -t http --request-timeout-ms 120000', description: 'Allow slower cold-start HTTP tools' },
|
|
136
|
+
{ command: 'claude-flow mcp start -t http --rate-limit-per-ip 600 --rate-limit-per-session 500 --rate-limit-global-burst 1000', description: 'Raise rate limits for a shared control plane' },
|
|
103
137
|
{ command: 'RUFLO_MCP_HTTP_TOKEN=<16+ chars> claude-flow mcp start -t http', description: 'HTTP server requiring "Authorization: Bearer <token>" on every request (except GET /health)' },
|
|
104
138
|
{ command: 'RUFLO_MCP_HTTP_TOKEN=<16+ chars> claude-flow mcp start -t http --host 0.0.0.0', description: 'Non-loopback bind: allowed only with a token (or RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP=1)' },
|
|
105
139
|
{ command: 'claude-flow mcp start -d', description: 'Start as daemon' },
|
|
@@ -112,15 +146,36 @@ const startCommand = {
|
|
|
112
146
|
const tools = ctx.flags.tools
|
|
113
147
|
|| process.env.CLAUDE_FLOW_MCP_TOOLS
|
|
114
148
|
|| 'all';
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
?? process.env.RUFLO_MCP_REQUEST_TIMEOUT_MS;
|
|
118
|
-
const requestTimeoutMs = timeoutInput === undefined ? undefined : Number(timeoutInput);
|
|
119
|
-
if (requestTimeoutMs !== undefined &&
|
|
120
|
-
(!Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 1 || requestTimeoutMs > 3_600_000)) {
|
|
149
|
+
const requestTimeoutMs = readIntegerOption(ctx.flags, 'request-timeout-ms', 'RUFLO_MCP_REQUEST_TIMEOUT_MS');
|
|
150
|
+
if (isOutOfRange(requestTimeoutMs, 1, 3_600_000)) {
|
|
121
151
|
output.printError('Request timeout must be an integer from 1 to 3600000 milliseconds');
|
|
122
152
|
return { success: false, exitCode: 1 };
|
|
123
153
|
}
|
|
154
|
+
const rateLimitPerIp = readIntegerOption(ctx.flags, 'rate-limit-per-ip', 'RUFLO_MCP_RATE_LIMIT_PER_IP');
|
|
155
|
+
if (isOutOfRange(rateLimitPerIp, 1, 100_000)) {
|
|
156
|
+
output.printError('HTTP rate limit must be an integer from 1 to 100000 requests per window');
|
|
157
|
+
return { success: false, exitCode: 1 };
|
|
158
|
+
}
|
|
159
|
+
const rateLimitWindowMs = readIntegerOption(ctx.flags, 'rate-limit-window-ms', 'RUFLO_MCP_RATE_LIMIT_WINDOW_MS');
|
|
160
|
+
if (isOutOfRange(rateLimitWindowMs, 1000, 3_600_000)) {
|
|
161
|
+
output.printError('Rate limit window must be an integer from 1000 to 3600000 milliseconds');
|
|
162
|
+
return { success: false, exitCode: 1 };
|
|
163
|
+
}
|
|
164
|
+
const rateLimitPerSession = readIntegerOption(ctx.flags, 'rate-limit-per-session', 'RUFLO_MCP_RATE_LIMIT_PER_SESSION');
|
|
165
|
+
if (isOutOfRange(rateLimitPerSession, 1, 10_000)) {
|
|
166
|
+
output.printError('Session rate limit must be an integer from 1 to 10000 requests');
|
|
167
|
+
return { success: false, exitCode: 1 };
|
|
168
|
+
}
|
|
169
|
+
const rateLimitGlobalRps = readIntegerOption(ctx.flags, 'rate-limit-global-rps', 'RUFLO_MCP_RATE_LIMIT_GLOBAL_RPS');
|
|
170
|
+
if (isOutOfRange(rateLimitGlobalRps, 1, 10_000)) {
|
|
171
|
+
output.printError('Global rate limit must be an integer from 1 to 10000 requests per second');
|
|
172
|
+
return { success: false, exitCode: 1 };
|
|
173
|
+
}
|
|
174
|
+
const rateLimitGlobalBurst = readIntegerOption(ctx.flags, 'rate-limit-global-burst', 'RUFLO_MCP_RATE_LIMIT_GLOBAL_BURST');
|
|
175
|
+
if (isOutOfRange(rateLimitGlobalBurst, 1, 20_000)) {
|
|
176
|
+
output.printError('Global rate limit burst must be an integer from 1 to 20000 requests');
|
|
177
|
+
return { success: false, exitCode: 1 };
|
|
178
|
+
}
|
|
124
179
|
let authToken;
|
|
125
180
|
try {
|
|
126
181
|
authToken = resolveMcpHttpAuthToken({
|
|
@@ -189,6 +244,11 @@ const startCommand = {
|
|
|
189
244
|
port,
|
|
190
245
|
tools: !tools || tools === 'all' ? 'all' : tools.split(','),
|
|
191
246
|
...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
|
|
247
|
+
...(rateLimitPerIp === undefined ? {} : { rateLimitPerIp }),
|
|
248
|
+
...(rateLimitWindowMs === undefined ? {} : { rateLimitWindowMs }),
|
|
249
|
+
...(rateLimitPerSession === undefined ? {} : { rateLimitPerSession }),
|
|
250
|
+
...(rateLimitGlobalRps === undefined ? {} : { rateLimitGlobalRps }),
|
|
251
|
+
...(rateLimitGlobalBurst === undefined ? {} : { rateLimitGlobalBurst }),
|
|
192
252
|
...(authToken ? { authToken } : {}),
|
|
193
253
|
daemonize: daemon,
|
|
194
254
|
};
|
|
@@ -16,6 +16,7 @@ import { existsSync } from 'node:fs';
|
|
|
16
16
|
import { siblingAgentDbPath } from '../memory/memory-bridge.js';
|
|
17
17
|
import { validateIdentifier } from '../mcp-tools/validate-input.js';
|
|
18
18
|
import { memoryKeyError } from '../mcp-tools/memory-tools.js';
|
|
19
|
+
import { memoryPeek } from '../memory/memory-peek.js';
|
|
19
20
|
/**
|
|
20
21
|
* #3228: a miss in one store is not a miss in the memory.
|
|
21
22
|
*
|
|
@@ -1223,6 +1224,54 @@ const statsCommand = {
|
|
|
1223
1224
|
}
|
|
1224
1225
|
}
|
|
1225
1226
|
};
|
|
1227
|
+
// #3508: read-only route/peek — which store files this cwd resolves to,
|
|
1228
|
+
// their row counts and last write, without creating or initializing
|
|
1229
|
+
// anything. Unlike `stats`, this never calls into the MCP tool layer (no
|
|
1230
|
+
// `authorizeMcpTool` receipt, no `ensureInitialized()`): it reads both
|
|
1231
|
+
// files directly, in-process, read-only.
|
|
1232
|
+
const peekCommand = {
|
|
1233
|
+
name: 'peek',
|
|
1234
|
+
description: 'Report which memory store files this cwd resolves to, their row counts and last write — read-only, creates nothing',
|
|
1235
|
+
options: [DB_PATH_OPTION],
|
|
1236
|
+
action: async (ctx) => {
|
|
1237
|
+
try {
|
|
1238
|
+
const result = await memoryPeek({ path: ctx.flags.path });
|
|
1239
|
+
if (ctx.flags.format === 'json') {
|
|
1240
|
+
output.printJson(result);
|
|
1241
|
+
return { success: true, data: result };
|
|
1242
|
+
}
|
|
1243
|
+
output.writeln();
|
|
1244
|
+
output.writeln(output.bold('Memory Peek'));
|
|
1245
|
+
output.writeln();
|
|
1246
|
+
output.writeln(`cwd: ${result.cwd}`);
|
|
1247
|
+
output.writeln(`root: ${result.root.path} (source: ${result.root.source})`);
|
|
1248
|
+
output.writeln();
|
|
1249
|
+
output.printTable({
|
|
1250
|
+
columns: [
|
|
1251
|
+
{ key: 'role', header: 'Role', width: 6 },
|
|
1252
|
+
{ key: 'path', header: 'Path', width: 40 },
|
|
1253
|
+
{ key: 'exists', header: 'Exists', width: 8 },
|
|
1254
|
+
{ key: 'rows', header: 'Rows', width: 8, align: 'right' },
|
|
1255
|
+
{ key: 'lastWrite', header: 'Last Write', width: 24 },
|
|
1256
|
+
],
|
|
1257
|
+
data: result.stores.map((store) => ({
|
|
1258
|
+
role: store.role,
|
|
1259
|
+
path: store.path,
|
|
1260
|
+
exists: store.exists ? 'yes' : 'no',
|
|
1261
|
+
rows: store.encrypted ? 'encrypted' : (store.rows === null ? 'unknown' : String(store.rows)),
|
|
1262
|
+
lastWrite: store.lastWrite ?? 'N/A',
|
|
1263
|
+
})),
|
|
1264
|
+
});
|
|
1265
|
+
output.writeln();
|
|
1266
|
+
output.writeln(`Shared keys (same namespace+key in both stores): ${result.sharedKeys === null ? 'unknown' : result.sharedKeys}`);
|
|
1267
|
+
return { success: true, data: result };
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
output.printError(`Failed to peek: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1271
|
+
return { success: false, exitCode: 1 };
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1226
1275
|
// Configure command
|
|
1227
1276
|
const configureCommand = {
|
|
1228
1277
|
name: 'configure',
|
|
@@ -2038,12 +2087,13 @@ const classifyCommand = {
|
|
|
2038
2087
|
export const memoryCommand = {
|
|
2039
2088
|
name: 'memory',
|
|
2040
2089
|
description: 'Memory management commands',
|
|
2041
|
-
subcommands: [initMemoryCommand, storeCommand, retrieveCommand, searchCommand, listCommand, deleteCommand, purgeCommand, statsCommand, configureCommand, cleanupCommand, compressCommand, exportCommand, importCommand, distillCommand, backupCommand, classifyCommand, selectOperatorCommand],
|
|
2090
|
+
subcommands: [initMemoryCommand, storeCommand, retrieveCommand, searchCommand, listCommand, deleteCommand, purgeCommand, statsCommand, peekCommand, configureCommand, cleanupCommand, compressCommand, exportCommand, importCommand, distillCommand, backupCommand, classifyCommand, selectOperatorCommand],
|
|
2042
2091
|
options: [],
|
|
2043
2092
|
examples: [
|
|
2044
2093
|
{ command: 'claude-flow memory store -k "key" -v "value"', description: 'Store data' },
|
|
2045
2094
|
{ command: 'claude-flow memory search -q "auth patterns"', description: 'Search memory' },
|
|
2046
|
-
{ command: 'claude-flow memory stats', description: 'Show statistics' }
|
|
2095
|
+
{ command: 'claude-flow memory stats', description: 'Show statistics' },
|
|
2096
|
+
{ command: 'claude-flow memory peek', description: 'Read-only: which store files this cwd resolves to, rows, last write' }
|
|
2047
2097
|
],
|
|
2048
2098
|
action: async (ctx) => {
|
|
2049
2099
|
output.writeln();
|
|
@@ -2060,6 +2110,7 @@ export const memoryCommand = {
|
|
|
2060
2110
|
`${output.highlight('list')} - List memory entries`,
|
|
2061
2111
|
`${output.highlight('delete')} - Delete memory entry`,
|
|
2062
2112
|
`${output.highlight('stats')} - Show statistics`,
|
|
2113
|
+
`${output.highlight('peek')} - Read-only: resolved store files, rows, last write (creates nothing)`,
|
|
2063
2114
|
`${output.highlight('configure')} - Configure backend`,
|
|
2064
2115
|
`${output.highlight('cleanup')} - Clean expired entries`,
|
|
2065
2116
|
`${output.highlight('compress')} - Compress database`,
|
package/dist/src/mcp-server.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface MCPServerOptions {
|
|
|
30
30
|
daemonize?: boolean;
|
|
31
31
|
timeout?: number;
|
|
32
32
|
requestTimeoutMs?: number;
|
|
33
|
+
rateLimitPerIp?: number;
|
|
34
|
+
rateLimitWindowMs?: number;
|
|
35
|
+
rateLimitPerSession?: number;
|
|
36
|
+
rateLimitGlobalRps?: number;
|
|
37
|
+
rateLimitGlobalBurst?: number;
|
|
33
38
|
/**
|
|
34
39
|
* Bearer token every HTTP request must present (`Authorization: Bearer <token>`).
|
|
35
40
|
* http transport only. Prefer RUFLO_MCP_HTTP_TOKEN / a token file over this
|
package/dist/src/mcp-server.js
CHANGED
|
@@ -103,6 +103,10 @@ function processStartToken(pid) {
|
|
|
103
103
|
return undefined;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
function definedFields(fields) {
|
|
107
|
+
const entries = Object.entries(fields).filter(([, value]) => value !== undefined);
|
|
108
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
109
|
+
}
|
|
106
110
|
/**
|
|
107
111
|
* Hosts treated as same-machine-only for MCP HTTP tool-call authorization
|
|
108
112
|
* purposes (see startHttpServer's non-loopback authorization gate below).
|
|
@@ -134,6 +138,26 @@ export function isUnauthenticatedHttpAllowed(env = process.env) {
|
|
|
134
138
|
export function shouldRefuseUnauthenticatedHttp(host, env = process.env, authenticated = false) {
|
|
135
139
|
return !isLoopbackHost(host) && !authenticated && !isUnauthenticatedHttpAllowed(env);
|
|
136
140
|
}
|
|
141
|
+
/** The limits @claude-flow/mcp applies when a rate limit option is unset. */
|
|
142
|
+
const DEFAULT_RATE_LIMITS = {
|
|
143
|
+
rateLimitPerIp: 120,
|
|
144
|
+
rateLimitPerSession: 50,
|
|
145
|
+
rateLimitGlobalRps: 100,
|
|
146
|
+
rateLimitGlobalBurst: 200,
|
|
147
|
+
};
|
|
148
|
+
const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
149
|
+
/**
|
|
150
|
+
* Rate limit options that are looser than the @claude-flow/mcp defaults. A
|
|
151
|
+
* shorter window counts as looser.
|
|
152
|
+
*/
|
|
153
|
+
function raisedRateLimits(options) {
|
|
154
|
+
const raised = Object.keys(DEFAULT_RATE_LIMITS)
|
|
155
|
+
.filter((name) => (options[name] ?? 0) > DEFAULT_RATE_LIMITS[name]);
|
|
156
|
+
if ((options.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS) < DEFAULT_RATE_LIMIT_WINDOW_MS) {
|
|
157
|
+
raised.push('rateLimitWindowMs');
|
|
158
|
+
}
|
|
159
|
+
return raised;
|
|
160
|
+
}
|
|
137
161
|
/** Printable ASCII without space (RFC 6750 token-ish), 16-512 chars. */
|
|
138
162
|
const AUTH_TOKEN_PATTERN = /^[\x21-\x7e]{16,512}$/;
|
|
139
163
|
/**
|
|
@@ -822,6 +846,15 @@ export class MCPServerManager extends EventEmitter {
|
|
|
822
846
|
'RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP=1 to acknowledge the risk and proceed ' +
|
|
823
847
|
'(e.g. when a trusted reverse proxy or network boundary already enforces auth).');
|
|
824
848
|
}
|
|
849
|
+
const raised = raisedRateLimits(this.options);
|
|
850
|
+
if (raised.length > 0 && !isLoopbackHost(this.options.host) && !authToken) {
|
|
851
|
+
// RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP=1 lets an open server start, but
|
|
852
|
+
// anyone who can reach it should not also get more than the default
|
|
853
|
+
// request budget.
|
|
854
|
+
throw new Error(`Refusing to raise MCP rate limits (${raised.join(', ')}) above the defaults on non-loopback ` +
|
|
855
|
+
`host "${this.options.host}" without a bearer token. Set RUFLO_MCP_HTTP_TOKEN (or ` +
|
|
856
|
+
'--auth-token-file), or bind to a loopback host (127.0.0.1, ::1, or localhost).');
|
|
857
|
+
}
|
|
825
858
|
// Dynamically import the MCP server package
|
|
826
859
|
// FIX for issue #942: Use proper package import instead of broken relative path
|
|
827
860
|
const { createMCPServer } = await import('@claude-flow/mcp');
|
|
@@ -867,6 +900,12 @@ export class MCPServerManager extends EventEmitter {
|
|
|
867
900
|
enableMetrics: true,
|
|
868
901
|
enableCaching: true,
|
|
869
902
|
requestTimeout: this.options.requestTimeoutMs,
|
|
903
|
+
rateLimit: definedFields({ windowMs: this.options.rateLimitWindowMs, limit: this.options.rateLimitPerIp }),
|
|
904
|
+
sessionRateLimit: definedFields({
|
|
905
|
+
requestsPerSecond: this.options.rateLimitGlobalRps,
|
|
906
|
+
burstSize: this.options.rateLimitGlobalBurst,
|
|
907
|
+
perSessionLimit: this.options.rateLimitPerSession,
|
|
908
|
+
}),
|
|
870
909
|
auth: authToken
|
|
871
910
|
? { enabled: true, method: 'token', tokens: [authToken] }
|
|
872
911
|
: undefined,
|
|
@@ -216,6 +216,7 @@ export const agentTools = [
|
|
|
216
216
|
properties: {
|
|
217
217
|
agentType: { type: 'string', description: 'Type of agent to spawn' },
|
|
218
218
|
agentId: { type: 'string', description: 'Optional custom agent ID' },
|
|
219
|
+
name: { type: 'string', description: 'Optional display name for the agent (shown by agent_list, agent_status and ruflo-console)' },
|
|
219
220
|
// #2085 — accept swarmId so spawned agents register in the
|
|
220
221
|
// swarm.agents array that swarm_status reports. Omit to register
|
|
221
222
|
// with the most-recently-created swarm.
|
|
@@ -241,6 +242,18 @@ export const agentTools = [
|
|
|
241
242
|
}
|
|
242
243
|
const agentId = input.agentId || `agent-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
243
244
|
const agentType = input.agentType;
|
|
245
|
+
// #4300 — older callers (including `agent spawn` before this fix) send
|
|
246
|
+
// the name only as metadata.name.
|
|
247
|
+
const metadata = input.metadata;
|
|
248
|
+
const rawName = input.name ?? metadata?.name;
|
|
249
|
+
let name;
|
|
250
|
+
if (rawName !== undefined && rawName !== '') {
|
|
251
|
+
const vName = validateText(rawName, 'name', 200);
|
|
252
|
+
if (!vName.valid) {
|
|
253
|
+
return { success: false, error: `Input validation failed: ${vName.error}` };
|
|
254
|
+
}
|
|
255
|
+
name = vName.sanitized;
|
|
256
|
+
}
|
|
244
257
|
const config = input.config || {};
|
|
245
258
|
// Add explicit model to config if provided
|
|
246
259
|
if (input.model) {
|
|
@@ -262,6 +275,7 @@ export const agentTools = [
|
|
|
262
275
|
const agent = {
|
|
263
276
|
agentId,
|
|
264
277
|
agentType,
|
|
278
|
+
...(name ? { name } : {}),
|
|
265
279
|
status: 'idle',
|
|
266
280
|
health: 1.0,
|
|
267
281
|
taskCount: 0,
|
|
@@ -347,6 +361,7 @@ export const agentTools = [
|
|
|
347
361
|
success: true,
|
|
348
362
|
agentId,
|
|
349
363
|
agentType: agent.agentType,
|
|
364
|
+
...(agent.name ? { name: agent.name } : {}),
|
|
350
365
|
model: agent.model,
|
|
351
366
|
modelRoutedBy: routingResult.routedBy,
|
|
352
367
|
...(routingResult.modelId ? { modelId: routingResult.modelId } : {}),
|
|
@@ -501,6 +516,7 @@ export const agentTools = [
|
|
|
501
516
|
return {
|
|
502
517
|
agentId: agent.agentId,
|
|
503
518
|
agentType: agent.agentType,
|
|
519
|
+
...(agent.name ? { name: agent.name } : {}),
|
|
504
520
|
status: agent.status,
|
|
505
521
|
health: agent.health,
|
|
506
522
|
taskCount: agent.taskCount,
|
|
@@ -525,6 +541,7 @@ export const agentTools = [
|
|
|
525
541
|
properties: {
|
|
526
542
|
status: { type: 'string', description: 'Filter by status' },
|
|
527
543
|
domain: { type: 'string', description: 'Filter by domain' },
|
|
544
|
+
agentType: { type: 'string', description: 'Filter by agent type' },
|
|
528
545
|
includeTerminated: { type: 'boolean', description: 'Include terminated agents' },
|
|
529
546
|
},
|
|
530
547
|
},
|
|
@@ -539,6 +556,11 @@ export const agentTools = [
|
|
|
539
556
|
if (!v.valid)
|
|
540
557
|
return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
|
|
541
558
|
}
|
|
559
|
+
if (input.agentType) {
|
|
560
|
+
const v = validateIdentifier(input.agentType, 'agentType');
|
|
561
|
+
if (!v.valid)
|
|
562
|
+
return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
|
|
563
|
+
}
|
|
542
564
|
let agents = Object.values(loadAllAgents()); // #1916: includes hive-mind-spawned workers
|
|
543
565
|
// Filter by status
|
|
544
566
|
if (input.status) {
|
|
@@ -551,10 +573,15 @@ export const agentTools = [
|
|
|
551
573
|
if (input.domain) {
|
|
552
574
|
agents = agents.filter(a => a.domain === input.domain);
|
|
553
575
|
}
|
|
576
|
+
// Filter by agent type
|
|
577
|
+
if (input.agentType) {
|
|
578
|
+
agents = agents.filter(a => a.agentType === input.agentType);
|
|
579
|
+
}
|
|
554
580
|
return {
|
|
555
581
|
agents: agents.map(a => ({
|
|
556
582
|
agentId: a.agentId,
|
|
557
583
|
agentType: a.agentType,
|
|
584
|
+
...(a.name ? { name: a.name } : {}),
|
|
558
585
|
status: a.status,
|
|
559
586
|
health: a.health,
|
|
560
587
|
taskCount: a.taskCount,
|
|
@@ -565,6 +592,7 @@ export const agentTools = [
|
|
|
565
592
|
filters: {
|
|
566
593
|
status: input.status,
|
|
567
594
|
domain: input.domain,
|
|
595
|
+
agentType: input.agentType,
|
|
568
596
|
includeTerminated: input.includeTerminated,
|
|
569
597
|
},
|
|
570
598
|
};
|
|
@@ -13,6 +13,7 @@ import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
|
13
13
|
import { join, resolve } from 'path';
|
|
14
14
|
const CLAIMS_DIR = '.claude-flow/claims';
|
|
15
15
|
const CLAIMS_FILE = 'claims.json';
|
|
16
|
+
const DEFAULT_MAX_CLAIMS = 5;
|
|
16
17
|
function getClaimsPath() {
|
|
17
18
|
return resolve(join(CLAIMS_DIR, CLAIMS_FILE));
|
|
18
19
|
}
|
|
@@ -47,7 +48,7 @@ function parseClaimant(str) {
|
|
|
47
48
|
export const claimsTools = [
|
|
48
49
|
{
|
|
49
50
|
name: 'claims_claim',
|
|
50
|
-
description: 'Claim an issue for work (human or agent) Use when
|
|
51
|
+
description: 'Claim an issue for work (human or agent). Use when multiple agents might pick up the same issue — claiming makes ownership visible via claims_list before work starts.',
|
|
51
52
|
category: 'claims',
|
|
52
53
|
inputSchema: {
|
|
53
54
|
type: 'object',
|
|
@@ -64,6 +65,10 @@ export const claimsTools = [
|
|
|
64
65
|
type: 'string',
|
|
65
66
|
description: 'Optional context about the work approach',
|
|
66
67
|
},
|
|
68
|
+
ttlMs: {
|
|
69
|
+
type: 'number',
|
|
70
|
+
description: 'Optional time-to-live in milliseconds; when set, the claim records an expiresAt timestamp. Informational only today — no handler currently enforces it (e.g. claims_steal still requires claims_mark-stealable rather than checking expiry).',
|
|
71
|
+
},
|
|
67
72
|
},
|
|
68
73
|
required: ['issueId', 'claimant'],
|
|
69
74
|
},
|
|
@@ -71,6 +76,7 @@ export const claimsTools = [
|
|
|
71
76
|
const issueId = input.issueId;
|
|
72
77
|
const claimantStr = input.claimant;
|
|
73
78
|
const context = input.context;
|
|
79
|
+
const ttlMs = input.ttlMs;
|
|
74
80
|
{
|
|
75
81
|
const v = validateIdentifier(issueId, 'issueId');
|
|
76
82
|
if (!v.valid)
|
|
@@ -86,6 +92,9 @@ export const claimsTools = [
|
|
|
86
92
|
if (!v.valid)
|
|
87
93
|
return { success: false, error: v.error };
|
|
88
94
|
}
|
|
95
|
+
if (ttlMs !== undefined && (typeof ttlMs !== 'number' || !Number.isFinite(ttlMs) || ttlMs <= 0)) {
|
|
96
|
+
return { success: false, error: 'ttlMs must be a positive number' };
|
|
97
|
+
}
|
|
89
98
|
const claimant = parseClaimant(claimantStr);
|
|
90
99
|
if (!claimant) {
|
|
91
100
|
return { success: false, error: 'Invalid claimant format. Use "human:userId:name" or "agent:agentId:agentType"' };
|
|
@@ -109,6 +118,7 @@ export const claimsTools = [
|
|
|
109
118
|
statusChangedAt: now,
|
|
110
119
|
progress: 0,
|
|
111
120
|
context,
|
|
121
|
+
expiresAt: ttlMs !== undefined ? new Date(Date.now() + ttlMs).toISOString() : undefined,
|
|
112
122
|
};
|
|
113
123
|
store.claims[issueId] = claim;
|
|
114
124
|
saveClaims(store);
|
|
@@ -121,7 +131,7 @@ export const claimsTools = [
|
|
|
121
131
|
},
|
|
122
132
|
{
|
|
123
133
|
name: 'claims_release',
|
|
124
|
-
description: 'Release a claim on an issue Use when
|
|
134
|
+
description: 'Release a claim on an issue. Use when the claimant stops working on it, freeing it for another claims_claim instead of leaving it claimed indefinitely.',
|
|
125
135
|
category: 'claims',
|
|
126
136
|
inputSchema: {
|
|
127
137
|
type: 'object',
|
|
@@ -186,7 +196,7 @@ export const claimsTools = [
|
|
|
186
196
|
},
|
|
187
197
|
{
|
|
188
198
|
name: 'claims_handoff',
|
|
189
|
-
description: 'Request handoff of an issue to another claimant Use when
|
|
199
|
+
description: 'Request handoff of an issue to another claimant. Use when transferring in-progress work to a different agent, instead of releasing and having them claim it fresh.',
|
|
190
200
|
category: 'claims',
|
|
191
201
|
inputSchema: {
|
|
192
202
|
type: 'object',
|
|
@@ -270,7 +280,7 @@ export const claimsTools = [
|
|
|
270
280
|
},
|
|
271
281
|
{
|
|
272
282
|
name: 'claims_accept-handoff',
|
|
273
|
-
description: 'Accept a pending handoff Use when
|
|
283
|
+
description: 'Accept a pending handoff. Use when the receiving claimant confirms a claims_handoff request, completing the ownership transfer.',
|
|
274
284
|
category: 'claims',
|
|
275
285
|
inputSchema: {
|
|
276
286
|
type: 'object',
|
|
@@ -333,7 +343,7 @@ export const claimsTools = [
|
|
|
333
343
|
},
|
|
334
344
|
{
|
|
335
345
|
name: 'claims_status',
|
|
336
|
-
description: 'Update claim status Use when
|
|
346
|
+
description: 'Update claim status. Use when progress changes (active, blocked, review-requested, etc.) so claims_list and claims_load reflect current state.',
|
|
337
347
|
category: 'claims',
|
|
338
348
|
inputSchema: {
|
|
339
349
|
type: 'object',
|
|
@@ -398,7 +408,7 @@ export const claimsTools = [
|
|
|
398
408
|
},
|
|
399
409
|
{
|
|
400
410
|
name: 'claims_list',
|
|
401
|
-
description: 'List all claims or filter by criteria Use when
|
|
411
|
+
description: 'List all claims or filter by criteria. Use when checking what is already claimed before starting new work, instead of guessing from issue comments.',
|
|
402
412
|
category: 'claims',
|
|
403
413
|
inputSchema: {
|
|
404
414
|
type: 'object',
|
|
@@ -453,7 +463,7 @@ export const claimsTools = [
|
|
|
453
463
|
},
|
|
454
464
|
{
|
|
455
465
|
name: 'claims_mark-stealable',
|
|
456
|
-
description: 'Mark an issue as stealable by other agents Use when
|
|
466
|
+
description: 'Mark an issue as stealable by other agents. Use when a claimant is overloaded or stalled and wants another agent to pick it up via claims_steal.',
|
|
457
467
|
category: 'claims',
|
|
458
468
|
inputSchema: {
|
|
459
469
|
type: 'object',
|
|
@@ -521,7 +531,7 @@ export const claimsTools = [
|
|
|
521
531
|
},
|
|
522
532
|
{
|
|
523
533
|
name: 'claims_steal',
|
|
524
|
-
description: 'Steal a stealable issue Use when
|
|
534
|
+
description: 'Steal a stealable issue. Use when picking up work already marked stealable via claims_mark-stealable, rather than claiming an issue someone else still owns.',
|
|
525
535
|
category: 'claims',
|
|
526
536
|
inputSchema: {
|
|
527
537
|
type: 'object',
|
|
@@ -592,7 +602,7 @@ export const claimsTools = [
|
|
|
592
602
|
},
|
|
593
603
|
{
|
|
594
604
|
name: 'claims_stealable',
|
|
595
|
-
description: 'List all stealable issues Use when
|
|
605
|
+
description: 'List all stealable issues. Use when looking for available work that has been freed up via claims_mark-stealable.',
|
|
596
606
|
category: 'claims',
|
|
597
607
|
inputSchema: {
|
|
598
608
|
type: 'object',
|
|
@@ -628,7 +638,7 @@ export const claimsTools = [
|
|
|
628
638
|
},
|
|
629
639
|
{
|
|
630
640
|
name: 'claims_load',
|
|
631
|
-
description: 'Get agent load information Use when
|
|
641
|
+
description: 'Get agent load information. Use when deciding which agent should take new work by comparing claim counts against claims_rebalance targets.',
|
|
632
642
|
category: 'claims',
|
|
633
643
|
inputSchema: {
|
|
634
644
|
type: 'object',
|
|
@@ -689,8 +699,8 @@ export const claimsTools = [
|
|
|
689
699
|
agentId: l.agentId,
|
|
690
700
|
agentType: l.agentType,
|
|
691
701
|
claimCount: l.claims.length,
|
|
692
|
-
maxClaims:
|
|
693
|
-
utilization: l.claims.length /
|
|
702
|
+
maxClaims: DEFAULT_MAX_CLAIMS,
|
|
703
|
+
utilization: l.claims.length / DEFAULT_MAX_CLAIMS,
|
|
694
704
|
blockedCount: l.blockedCount,
|
|
695
705
|
claims: l.claims.map(c => ({
|
|
696
706
|
issueId: c.issueId,
|
|
@@ -711,7 +721,7 @@ export const claimsTools = [
|
|
|
711
721
|
},
|
|
712
722
|
{
|
|
713
723
|
name: 'claims_board',
|
|
714
|
-
description: 'Get a visual board view of all claims Use when
|
|
724
|
+
description: 'Get a visual board view of all claims. Use when scanning overall claim status across issues and agents instead of composing the view from claims_list yourself.',
|
|
715
725
|
category: 'claims',
|
|
716
726
|
inputSchema: {
|
|
717
727
|
type: 'object',
|
|
@@ -760,7 +770,7 @@ export const claimsTools = [
|
|
|
760
770
|
},
|
|
761
771
|
{
|
|
762
772
|
name: 'claims_rebalance',
|
|
763
|
-
description: 'Suggest or apply load rebalancing across agents Use when
|
|
773
|
+
description: 'Suggest or apply load rebalancing across agents. Use when some agents are overloaded and others idle, instead of manually reassigning claims one at a time.',
|
|
764
774
|
category: 'claims',
|
|
765
775
|
inputSchema: {
|
|
766
776
|
type: 'object',
|
|
@@ -794,7 +804,7 @@ export const claimsTools = [
|
|
|
794
804
|
agentLoads.get(key).claims.push(claim);
|
|
795
805
|
}
|
|
796
806
|
const loads = Array.from(agentLoads.values());
|
|
797
|
-
const maxClaims =
|
|
807
|
+
const maxClaims = DEFAULT_MAX_CLAIMS;
|
|
798
808
|
const avgLoad = loads.length > 0
|
|
799
809
|
? loads.reduce((sum, l) => sum + l.claims.length, 0) / loads.length
|
|
800
810
|
: 0;
|