claude-flow 3.57.1 → 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.
Files changed (77) hide show
  1. package/.claude/helpers/intelligence.cjs +3 -5
  2. package/.claude/proven-config.json +1 -1
  3. package/.claude-plugin/marketplace.json +11 -0
  4. package/README.md +1 -0
  5. package/node_modules/@claude-flow/codex/README.md +87 -9
  6. package/node_modules/@claude-flow/codex/dist/activity/cli.d.ts +3 -0
  7. package/node_modules/@claude-flow/codex/dist/activity/cli.d.ts.map +1 -0
  8. package/node_modules/@claude-flow/codex/dist/activity/cli.js +115 -0
  9. package/node_modules/@claude-flow/codex/dist/activity/cli.js.map +1 -0
  10. package/node_modules/@claude-flow/codex/dist/activity/files.d.ts +14 -0
  11. package/node_modules/@claude-flow/codex/dist/activity/files.d.ts.map +1 -0
  12. package/node_modules/@claude-flow/codex/dist/activity/files.js +142 -0
  13. package/node_modules/@claude-flow/codex/dist/activity/files.js.map +1 -0
  14. package/node_modules/@claude-flow/codex/dist/activity/parser.d.ts +21 -0
  15. package/node_modules/@claude-flow/codex/dist/activity/parser.d.ts.map +1 -0
  16. package/node_modules/@claude-flow/codex/dist/activity/parser.js +179 -0
  17. package/node_modules/@claude-flow/codex/dist/activity/parser.js.map +1 -0
  18. package/node_modules/@claude-flow/codex/dist/activity/reader.d.ts +20 -0
  19. package/node_modules/@claude-flow/codex/dist/activity/reader.d.ts.map +1 -0
  20. package/node_modules/@claude-flow/codex/dist/activity/reader.js +106 -0
  21. package/node_modules/@claude-flow/codex/dist/activity/reader.js.map +1 -0
  22. package/node_modules/@claude-flow/codex/dist/activity/sanitize.d.ts +4 -0
  23. package/node_modules/@claude-flow/codex/dist/activity/sanitize.d.ts.map +1 -0
  24. package/node_modules/@claude-flow/codex/dist/activity/sanitize.js +27 -0
  25. package/node_modules/@claude-flow/codex/dist/activity/sanitize.js.map +1 -0
  26. package/node_modules/@claude-flow/codex/dist/activity/types.d.ts +31 -0
  27. package/node_modules/@claude-flow/codex/dist/activity/types.d.ts.map +1 -0
  28. package/node_modules/@claude-flow/codex/dist/activity/types.js +5 -0
  29. package/node_modules/@claude-flow/codex/dist/activity/types.js.map +1 -0
  30. package/node_modules/@claude-flow/codex/dist/activity/viewer.d.ts +21 -0
  31. package/node_modules/@claude-flow/codex/dist/activity/viewer.d.ts.map +1 -0
  32. package/node_modules/@claude-flow/codex/dist/activity/viewer.js +168 -0
  33. package/node_modules/@claude-flow/codex/dist/activity/viewer.js.map +1 -0
  34. package/node_modules/@claude-flow/codex/dist/cli.js +3 -0
  35. package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
  36. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +3 -3
  37. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
  38. package/node_modules/@claude-flow/mcp/dist/server.d.ts.map +1 -1
  39. package/node_modules/@claude-flow/mcp/dist/server.js +5 -3
  40. package/node_modules/@claude-flow/mcp/dist/server.js.map +1 -1
  41. package/node_modules/@claude-flow/mcp/dist/types.d.ts +11 -0
  42. package/node_modules/@claude-flow/mcp/dist/types.d.ts.map +1 -1
  43. package/node_modules/@claude-flow/mcp/dist/types.js.map +1 -1
  44. package/package.json +3 -2
  45. package/v3/@claude-flow/cli/README.md +1 -0
  46. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  47. package/v3/@claude-flow/cli/dist/src/commands/agent.js +4 -1
  48. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +20 -1
  49. package/v3/@claude-flow/cli/dist/src/commands/mcp.js +66 -6
  50. package/v3/@claude-flow/cli/dist/src/commands/memory.js +78 -3
  51. package/v3/@claude-flow/cli/dist/src/commands/workflow.js +55 -15
  52. package/v3/@claude-flow/cli/dist/src/init/executor.js +9 -5
  53. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +1 -1
  54. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +4 -3
  55. package/v3/@claude-flow/cli/dist/src/mcp-server.d.ts +5 -0
  56. package/v3/@claude-flow/cli/dist/src/mcp-server.js +39 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +28 -0
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/claims-tools.js +25 -15
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +9 -3
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +58 -9
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +2 -1
  62. package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.d.ts +16 -4
  63. package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +19 -7
  64. package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +4 -0
  65. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +55 -9
  66. package/v3/@claude-flow/cli/dist/src/memory/memory-peek.d.ts +28 -0
  67. package/v3/@claude-flow/cli/dist/src/memory/memory-peek.js +222 -0
  68. package/v3/@claude-flow/cli/dist/src/memory/model-cache-dir.d.ts +11 -0
  69. package/v3/@claude-flow/cli/dist/src/memory/model-cache-dir.js +17 -0
  70. package/v3/@claude-flow/cli/dist/src/ruvector/task-embedder.js +38 -4
  71. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +6 -0
  72. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +109 -81
  73. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.d.ts +2 -0
  74. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +4 -4
  75. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +54 -3
  76. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +4 -0
  77. package/v3/@claude-flow/cli/package.json +5 -4
@@ -2333,6 +2333,19 @@ const workerListCommand = {
2333
2333
  }
2334
2334
  }
2335
2335
  };
2336
+ // Render the status hooks_worker-dispatch actually returned. Only a status that
2337
+ // means work finished may read as "completed" (#4196).
2338
+ function dispatchStatusLabel(status) {
2339
+ switch (status) {
2340
+ case 'dispatched': return output.highlight('dispatched (background)');
2341
+ case 'queued': return output.highlight('queued (daemon)');
2342
+ case 'no-daemon': return output.warning('no-daemon (not run)');
2343
+ case 'mcp-only': return output.warning('mcp-only (not queued)');
2344
+ case 'synthetic-completed': return output.warning('recorded (no work executed)');
2345
+ case 'completed': return output.success('completed');
2346
+ default: return output.dim(String(status));
2347
+ }
2348
+ }
2336
2349
  const workerDispatchCommand = {
2337
2350
  name: 'dispatch',
2338
2351
  description: 'Dispatch a background worker for analysis/optimization',
@@ -2386,9 +2399,15 @@ const workerDispatchCommand = {
2386
2399
  { field: 'Description', value: result.config.description },
2387
2400
  { field: 'Est. Duration', value: result.config.estimatedDuration },
2388
2401
  { field: 'Capabilities', value: result.config.capabilities.join(', ') },
2389
- { field: 'Status', value: result.status === 'dispatched' ? output.highlight('dispatched (background)') : output.success('completed') },
2402
+ { field: 'Status', value: dispatchStatusLabel(result.status) },
2390
2403
  ],
2391
2404
  });
2405
+ // The tool says why it could not run the worker (e.g. no daemon); show it
2406
+ // rather than letting a recorded-only dispatch read as finished work (#4196).
2407
+ if (result.note) {
2408
+ output.writeln();
2409
+ output.printWarning(result.note);
2410
+ }
2392
2411
  if (background) {
2393
2412
  output.writeln();
2394
2413
  output.writeln(output.dim(`Check status: claude-flow hooks worker status --id ${result.workerId}`));
@@ -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 timeoutInput = ctx.flags.requestTimeoutMs
116
- ?? ctx.flags['request-timeout-ms']
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
  *
@@ -659,7 +660,9 @@ const searchCommand = {
659
660
  });
660
661
  return {
661
662
  results: r.results.map(e => ({
662
- id: e.id,
663
+ // #2271: searchEntries() ids are truncated to 12 chars and collide
664
+ // for entries written within ~2.8h; SmartRetrieval dedupes on id.
665
+ id: JSON.stringify([e.namespace, e.key]),
663
666
  key: e.key,
664
667
  content: e.content,
665
668
  score: e.score,
@@ -1221,6 +1224,54 @@ const statsCommand = {
1221
1224
  }
1222
1225
  }
1223
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
+ };
1224
1275
  // Configure command
1225
1276
  const configureCommand = {
1226
1277
  name: 'configure',
@@ -1592,6 +1643,13 @@ const exportCommand = {
1592
1643
  namespace: ctx.flags.namespace,
1593
1644
  includeVectors: ctx.flags.includeVectors ?? true,
1594
1645
  });
1646
+ // memory_export reports failure as { error } rather than throwing; without
1647
+ // this check the command printed "Exported to undefined" and then crashed
1648
+ // reading result.exported.entries (#4204).
1649
+ if (!result || result.error || !result.exported) {
1650
+ output.printError(`Export error: ${result?.error ?? 'memory_export returned no export summary'}`);
1651
+ return { success: false, exitCode: 1 };
1652
+ }
1595
1653
  output.printSuccess(`Exported to ${result.outputPath}`);
1596
1654
  output.printList([
1597
1655
  `Entries: ${result.exported.entries}`,
@@ -1657,6 +1715,21 @@ const importCommand = {
1657
1715
  namespace: ctx.flags.namespace,
1658
1716
  dbPath: resolveDbPath(ctx.flags.path),
1659
1717
  });
1718
+ // memory_import reports a bad file as { error } rather than throwing, and
1719
+ // a partial write as { success: false, error, imported }. Without this the
1720
+ // command printed "Imported from undefined" and crashed on result.imported,
1721
+ // or reported success for an import that dropped entries.
1722
+ const failure = result?.error;
1723
+ if (!result || failure || result.success === false || !result.imported) {
1724
+ output.printError(`Import error: ${failure ?? 'memory_import returned no import summary'}`);
1725
+ if (result?.imported) {
1726
+ output.printList([
1727
+ `Entries imported: ${result.imported.entries}`,
1728
+ `Failed: ${result.failed ?? 'unknown'}`,
1729
+ ]);
1730
+ }
1731
+ return { success: false, exitCode: 1 };
1732
+ }
1660
1733
  output.printSuccess(`Imported from ${result.inputPath}`);
1661
1734
  output.printList([
1662
1735
  `Entries: ${result.imported.entries}`,
@@ -2014,12 +2087,13 @@ const classifyCommand = {
2014
2087
  export const memoryCommand = {
2015
2088
  name: 'memory',
2016
2089
  description: 'Memory management commands',
2017
- 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],
2018
2091
  options: [],
2019
2092
  examples: [
2020
2093
  { command: 'claude-flow memory store -k "key" -v "value"', description: 'Store data' },
2021
2094
  { command: 'claude-flow memory search -q "auth patterns"', description: 'Search memory' },
2022
- { 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' }
2023
2097
  ],
2024
2098
  action: async (ctx) => {
2025
2099
  output.writeln();
@@ -2036,6 +2110,7 @@ export const memoryCommand = {
2036
2110
  `${output.highlight('list')} - List memory entries`,
2037
2111
  `${output.highlight('delete')} - Delete memory entry`,
2038
2112
  `${output.highlight('stats')} - Show statistics`,
2113
+ `${output.highlight('peek')} - Read-only: resolved store files, rows, last write (creates nothing)`,
2039
2114
  `${output.highlight('configure')} - Configure backend`,
2040
2115
  `${output.highlight('cleanup')} - Clean expired entries`,
2041
2116
  `${output.highlight('compress')} - Compress database`,
@@ -111,6 +111,12 @@ const runCommand = {
111
111
  dryRun,
112
112
  },
113
113
  });
114
+ const runError = toolError(result);
115
+ if (runError || !result.metrics) {
116
+ spinner.fail('Workflow failed');
117
+ output.printError(`Workflow error: ${runError ?? 'workflow_run returned no workflow summary'}`);
118
+ return { success: false, exitCode: 1 };
119
+ }
114
120
  if (dryRun) {
115
121
  spinner.succeed('Workflow validated successfully');
116
122
  }
@@ -277,6 +283,11 @@ const listCommand = {
277
283
  status: status || 'all',
278
284
  limit,
279
285
  });
286
+ const listError = toolError(result);
287
+ if (listError || !Array.isArray(result.workflows)) {
288
+ output.printError(`Failed to list workflows: ${listError ?? 'workflow_list returned no workflows'}`);
289
+ return { success: false, exitCode: 1 };
290
+ }
280
291
  if (ctx.flags.format === 'json') {
281
292
  output.printJson(result);
282
293
  return { success: true, data: result };
@@ -290,13 +301,19 @@ const listCommand = {
290
301
  }
291
302
  output.printTable({
292
303
  columns: [
293
- { key: 'id', header: 'ID', width: 15 },
294
- { key: 'template', header: 'Template', width: 15 },
304
+ { key: 'id', header: 'ID', width: 30 },
305
+ { key: 'name', header: 'Name', width: 24 },
295
306
  { key: 'status', header: 'Status', width: 12, format: formatStageStatus },
296
- { key: 'progress', header: 'Progress', width: 10, align: 'right', format: (v) => `${v}%` },
297
- { key: 'startedAt', header: 'Started', width: 20, format: (v) => new Date(String(v)).toLocaleString() }
307
+ { key: 'steps', header: 'Steps', width: 6, align: 'right' },
308
+ { key: 'createdAt', header: 'Created', width: 20, format: (v) => new Date(String(v)).toLocaleString() }
298
309
  ],
299
- data: result.workflows
310
+ data: result.workflows.map(w => ({
311
+ id: w.workflowId,
312
+ name: w.name,
313
+ status: w.status,
314
+ steps: w.stepCount,
315
+ createdAt: w.createdAt,
316
+ }))
300
317
  });
301
318
  output.writeln();
302
319
  output.printInfo(`Total: ${result.total} workflows`);
@@ -335,30 +352,37 @@ const statusCommand = {
335
352
  try {
336
353
  const result = await callMCPTool('workflow_status', {
337
354
  workflowId,
355
+ verbose: true,
338
356
  });
357
+ const statusError = toolError(result);
358
+ if (statusError) {
359
+ output.printError(`Failed to get status: ${statusError}`);
360
+ return { success: false, exitCode: 1 };
361
+ }
339
362
  if (ctx.flags.format === 'json') {
340
363
  output.printJson(result);
341
364
  return { success: true, data: result };
342
365
  }
366
+ const started = result.startedAt ? Date.parse(result.startedAt) : NaN;
367
+ const ended = result.completedAt ? Date.parse(result.completedAt) : Date.now();
343
368
  output.writeln();
344
369
  output.printBox([
345
- `ID: ${result.id}`,
346
- `Template: ${result.template}`,
370
+ `ID: ${result.workflowId}`,
371
+ `Name: ${result.name}`,
347
372
  `Status: ${formatStageStatus(result.status)}`,
348
- `Progress: ${result.progress}%`,
349
- `Duration: ${(result.metrics.duration / 1000).toFixed(1)}s`,
350
- `Tokens: ${result.metrics.tokensUsed.toLocaleString()}`,
351
- `Agents: ${result.metrics.agentsSpawned}`
373
+ `Progress: ${Math.round(result.progress)}% (${result.completedSteps}/${result.totalSteps} steps)`,
374
+ ...(Number.isNaN(started) ? [] : [`Duration: ${((ended - started) / 1000).toFixed(1)}s`]),
375
+ ...(result.error ? [`Error: ${result.error}`] : []),
352
376
  ].join('\n'), 'Workflow Status');
353
377
  output.writeln();
354
- output.writeln(output.bold('Stage Progress'));
378
+ output.writeln(output.bold('Step Progress'));
355
379
  output.printTable({
356
380
  columns: [
357
- { key: 'name', header: 'Stage', width: 20 },
381
+ { key: 'name', header: 'Step', width: 24 },
382
+ { key: 'type', header: 'Type', width: 10 },
358
383
  { key: 'status', header: 'Status', width: 12, format: formatStageStatus },
359
- { key: 'agents', header: 'Agents', width: 25, format: (v) => Array.isArray(v) ? v.length.toString() : '0' }
360
384
  ],
361
- data: result.stages
385
+ data: result.steps ?? []
362
386
  });
363
387
  return { success: true, data: result };
364
388
  }
@@ -408,6 +432,11 @@ const stopCommand = {
408
432
  workflowId,
409
433
  graceful: !force,
410
434
  });
435
+ const stopError = toolError(result);
436
+ if (stopError || result.stopped !== true) {
437
+ output.printError(`Failed to stop workflow: ${stopError ?? 'the workflow was not stopped'}`);
438
+ return { success: false, exitCode: 1 };
439
+ }
411
440
  output.printSuccess(`Workflow ${workflowId} stopped`);
412
441
  return { success: true, data: result };
413
442
  }
@@ -556,6 +585,17 @@ export const workflowCommand = {
556
585
  }
557
586
  };
558
587
  // Helper functions
588
+ /**
589
+ * The workflow MCP tools report failure as `{ error }` (or `{ success: false, error }`)
590
+ * instead of throwing, so a command that reads the result without checking it prints
591
+ * a false success or crashes on a field the error result does not have.
592
+ */
593
+ function toolError(result) {
594
+ const r = result;
595
+ if (!r)
596
+ return 'the workflow tool returned no result';
597
+ return typeof r.error === 'string' && r.error.length > 0 ? r.error : undefined;
598
+ }
559
599
  function formatStageStatus(status) {
560
600
  const statusStr = String(status);
561
601
  switch (statusStr) {
@@ -18,7 +18,7 @@ import { generatePreCommitHook, generatePostCommitHook, generateSessionManager,
18
18
  import { getInstalledCliVersion, HELPERS_STAMP_FILE } from './helper-refresh.js';
19
19
  import { generateClaudeMd } from './claudemd-generator.js';
20
20
  import { recordMemoryPackagePath } from './memory-package-resolver.js';
21
- import { ensureCommonJsCompanions } from './helper-companions.js';
21
+ import { COMMONJS_COMPANION_HELPERS, ensureCommonJsCompanions } from './helper-companions.js';
22
22
  import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
23
23
  import { AUTO_MEMORY_HOOK_TIMEOUT_S, helperHookCommand, helperScopeFor, helperStatusLineCommand, pinToUserHelpers } from './helper-commands.js';
24
24
  /**
@@ -548,10 +548,14 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
548
548
  catch { }
549
549
  }
550
550
  }
551
- // #3555: the refreshed hook-handler requires the .cjs companions; older
552
- // installs only have session.js / memory.js, which can't load in ESM.
553
- for (const name of await ensureCommonJsCompanions(path.join(targetDir, '.claude', 'helpers'))) {
554
- result.created.push(`.claude/helpers/${name}`);
551
+ // #4198: refresh existing companions too, so upgraded hook handlers never
552
+ // keep obsolete session/memory logic. Generate from the CLI trust root.
553
+ const helpersDir = path.join(targetDir, '.claude', 'helpers');
554
+ const existingCompanions = new Set(COMMONJS_COMPANION_HELPERS.filter((name) => fs.existsSync(path.join(helpersDir, name))));
555
+ for (const name of await ensureCommonJsCompanions(helpersDir, true)) {
556
+ const changes = existingCompanions.has(name)
557
+ ? result.updated : result.created;
558
+ changes.push(`.claude/helpers/${name}`);
555
559
  }
556
560
  // Stamp the installed version so the startup auto-refresh treats these as
557
561
  // current (no redundant re-copy on the next command).
@@ -1,3 +1,3 @@
1
1
  export declare const COMMONJS_COMPANION_HELPERS: readonly ["session.cjs", "memory.cjs"];
2
- export declare function ensureCommonJsCompanions(helpersDir: string): Promise<string[]>;
2
+ export declare function ensureCommonJsCompanions(helpersDir: string, force?: boolean): Promise<string[]>;
3
3
  //# sourceMappingURL=helper-companions.d.ts.map
@@ -7,13 +7,14 @@
7
7
  * `.js` copies, which the refreshed hook-handler no longer requires, so the
8
8
  * refresh writes any missing `.cjs` companion from the CLI's own compiled
9
9
  * generators (the trust root; no external file to verify). Stale `.js` copies
10
- * are left in place and ignored. Existing `.cjs` files are never overwritten.
10
+ * are left in place and ignored. Existing `.cjs` files are preserved during
11
+ * startup refresh; an explicit init upgrade replaces them from the generators.
11
12
  */
12
13
  import * as fs from 'fs';
13
14
  import * as path from 'path';
14
15
  export const COMMONJS_COMPANION_HELPERS = ['session.cjs', 'memory.cjs'];
15
- export async function ensureCommonJsCompanions(helpersDir) {
16
- const missing = COMMONJS_COMPANION_HELPERS.filter((name) => !fs.existsSync(path.join(helpersDir, name)));
16
+ export async function ensureCommonJsCompanions(helpersDir, force = false) {
17
+ const missing = COMMONJS_COMPANION_HELPERS.filter((name) => force || !fs.existsSync(path.join(helpersDir, name)));
17
18
  if (missing.length === 0)
18
19
  return [];
19
20
  const gen = await import('./helpers-generator.js');
@@ -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
@@ -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
  };