@claude-flow/cli 3.71.4 → 3.72.1

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 (124) hide show
  1. package/.claude/helpers/helpers.manifest.json +4 -4
  2. package/.claude/helpers/hook-handler.cjs +23 -10
  3. package/.claude/helpers/statusline.cjs +29 -14
  4. package/.claude/skills/fast-code-edits/SKILL.md +45 -0
  5. package/.claude/skills/graph-mincut/SKILL.md +32 -0
  6. package/.claude/skills/injection-defense/SKILL.md +36 -0
  7. package/.claude/skills/lora-learning/SKILL.md +30 -0
  8. package/.claude/skills/sublinear-solver/SKILL.md +34 -0
  9. package/.claude/skills/vector-quantization/SKILL.md +36 -0
  10. package/README.md +18 -3
  11. package/bin/cli.js +13 -18
  12. package/bin/mcp-server.js +13 -18
  13. package/catalog-manifest.json +4 -4
  14. package/dist/src/appliance/ruvllm-bridge.d.ts +6 -2
  15. package/dist/src/appliance/ruvllm-bridge.js +13 -8
  16. package/dist/src/appliance/rvfa-distribution.js +2 -2
  17. package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
  18. package/dist/src/benchmarks/gaia-critic.js +1 -7
  19. package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
  20. package/dist/src/benchmarks/gaia-decomposer.js +1 -8
  21. package/dist/src/commands/agent-wasm.js +2 -2
  22. package/dist/src/commands/analyze.js +1 -1
  23. package/dist/src/commands/daemon.js +1 -1
  24. package/dist/src/commands/doctor.js +3 -2
  25. package/dist/src/commands/guidance.js +9 -9
  26. package/dist/src/commands/hooks.js +13 -8
  27. package/dist/src/commands/init.js +16 -16
  28. package/dist/src/commands/neural.js +87 -77
  29. package/dist/src/commands/route.js +1 -1
  30. package/dist/src/commands/ruvector/backup.js +7 -5
  31. package/dist/src/commands/ruvector/benchmark.js +6 -1
  32. package/dist/src/commands/ruvector/import.js +19 -5
  33. package/dist/src/commands/ruvector/init.js +1 -1
  34. package/dist/src/commands/start.js +1 -1
  35. package/dist/src/commands/status.js +1 -1
  36. package/dist/src/commands/task.js +18 -1
  37. package/dist/src/commands/update.js +1 -1
  38. package/dist/src/funnel/local-signals.js +7 -2
  39. package/dist/src/init/executor.js +13 -0
  40. package/dist/src/init/types.d.ts +6 -0
  41. package/dist/src/init/types.js +3 -0
  42. package/dist/src/mcp-client.js +2 -0
  43. package/dist/src/mcp-protocol.d.ts +90 -0
  44. package/dist/src/mcp-protocol.js +179 -0
  45. package/dist/src/mcp-server.d.ts +1 -0
  46. package/dist/src/mcp-server.js +20 -22
  47. package/dist/src/mcp-tools/agent-execute-core.js +9 -1
  48. package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
  49. package/dist/src/mcp-tools/agent-tools.js +59 -8
  50. package/dist/src/mcp-tools/agentdb-tools.js +34 -2
  51. package/dist/src/mcp-tools/browser-argv.d.ts +9 -0
  52. package/dist/src/mcp-tools/browser-argv.js +33 -0
  53. package/dist/src/mcp-tools/browser-session-tools.js +4 -0
  54. package/dist/src/mcp-tools/browser-tools.js +12 -2
  55. package/dist/src/mcp-tools/coordination-tools.js +5 -4
  56. package/dist/src/mcp-tools/embeddings-tools.js +12 -4
  57. package/dist/src/mcp-tools/fast-apply-tools.d.ts +4 -0
  58. package/dist/src/mcp-tools/fast-apply-tools.js +85 -0
  59. package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
  60. package/dist/src/mcp-tools/glob-exclusions.js +81 -0
  61. package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
  62. package/dist/src/mcp-tools/hooks-tools.js +14 -38
  63. package/dist/src/mcp-tools/import-limits.d.ts +4 -0
  64. package/dist/src/mcp-tools/import-limits.js +28 -0
  65. package/dist/src/mcp-tools/memory-tools.js +21 -42
  66. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  67. package/dist/src/mcp-tools/performance-tools.js +7 -2
  68. package/dist/src/mcp-tools/realpath-containment.d.ts +3 -0
  69. package/dist/src/mcp-tools/realpath-containment.js +39 -0
  70. package/dist/src/mcp-tools/record-store.d.ts +8 -0
  71. package/dist/src/mcp-tools/record-store.js +98 -1
  72. package/dist/src/mcp-tools/security-tools.d.ts +9 -0
  73. package/dist/src/mcp-tools/security-tools.js +39 -3
  74. package/dist/src/mcp-tools/session-tools.js +5 -3
  75. package/dist/src/mcp-tools/swarm-tools.js +35 -17
  76. package/dist/src/mcp-tools/task-tools.js +68 -5
  77. package/dist/src/mcp-tools/terminal-tools.js +9 -0
  78. package/dist/src/mcp-tools/workflow-tools.js +13 -12
  79. package/dist/src/mcp-tools/x-federation-join.js +55 -14
  80. package/dist/src/memory/intelligence.d.ts +11 -0
  81. package/dist/src/memory/intelligence.js +84 -6
  82. package/dist/src/memory/memory-initializer.d.ts +6 -2
  83. package/dist/src/memory/memory-initializer.js +15 -6
  84. package/dist/src/mods/command-registry/schema.d.ts +12 -12
  85. package/dist/src/ruvector/ast-analyzer.d.ts +1 -2
  86. package/dist/src/ruvector/ast-analyzer.js +4 -12
  87. package/dist/src/ruvector/coverage-router.d.ts +1 -2
  88. package/dist/src/ruvector/coverage-router.js +4 -12
  89. package/dist/src/ruvector/diff-classifier.d.ts +1 -2
  90. package/dist/src/ruvector/diff-classifier.js +3 -12
  91. package/dist/src/ruvector/fast-apply.d.ts +46 -0
  92. package/dist/src/ruvector/fast-apply.js +73 -0
  93. package/dist/src/ruvector/graph-analyzer.d.ts +11 -1
  94. package/dist/src/ruvector/graph-analyzer.js +22 -36
  95. package/dist/src/ruvector/mincut-bridge.d.ts +70 -0
  96. package/dist/src/ruvector/mincut-bridge.js +172 -0
  97. package/dist/src/ruvector/model-router.d.ts +6 -1
  98. package/dist/src/ruvector/model-router.js +16 -4
  99. package/dist/src/services/distill-oracle.d.ts +1 -1
  100. package/dist/src/services/distill-oracle.js +2 -2
  101. package/dist/src/services/worker-daemon.js +30 -12
  102. package/dist/src/transfer/storage/gcs.js +3 -2
  103. package/node_modules/@claude-flow/codex/.agents/skills/fast-code-edits/SKILL.md +42 -0
  104. package/node_modules/@claude-flow/codex/.agents/skills/graph-mincut/SKILL.md +42 -0
  105. package/node_modules/@claude-flow/codex/.agents/skills/injection-defense/SKILL.md +45 -0
  106. package/node_modules/@claude-flow/codex/.agents/skills/lora-learning/SKILL.md +39 -0
  107. package/node_modules/@claude-flow/codex/.agents/skills/sublinear-solver/SKILL.md +38 -0
  108. package/node_modules/@claude-flow/codex/.agents/skills/vector-quantization/SKILL.md +49 -0
  109. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
  110. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +6 -0
  111. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
  112. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  113. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  114. package/node_modules/@claude-flow/codex/dist/templates/index.d.ts.map +1 -1
  115. package/node_modules/@claude-flow/codex/dist/templates/index.js +37 -0
  116. package/node_modules/@claude-flow/codex/dist/templates/index.js.map +1 -1
  117. package/node_modules/@claude-flow/codex/dist/types.d.ts +1 -1
  118. package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
  119. package/node_modules/@claude-flow/codex/package.json +1 -1
  120. package/package.json +11 -9
  121. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +2 -1
  122. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
  123. package/plugins/ruflo-metaharness/scripts/_harness.mjs +1 -1
  124. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
@@ -0,0 +1,179 @@
1
+ /**
2
+ * MCP protocol-version handling for the stdio servers (ADR-524).
3
+ *
4
+ * Ruflo ships three stdio entry points (`bin/cli.js` auto-detect, `bin/mcp-server.js`,
5
+ * and `MCPServerManager` in mcp-server.ts). All three answered `initialize` with a
6
+ * hardcoded `2024-11-05` and knew nothing of the 2026-07-28 revision, which drops the
7
+ * handshake and carries the version on every request. This module is the one place
8
+ * that decides which revision a message speaks, so the three handlers cannot drift.
9
+ *
10
+ * Dual-era behaviour (spec: basic/versioning):
11
+ * - `initialize` selects legacy semantics; the requested legacy version is echoed.
12
+ * - a request carrying `_meta["io.modelcontextprotocol/protocolVersion"]` is served
13
+ * statelessly: `resultType` and `serverInfo` on every result, cache hints on lists,
14
+ * `-32602` for an unknown resource, `-32022` for an unsupported version.
15
+ * - `server/discover` is always answered, so a dual-era client's stdio probe succeeds.
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+ export const MODERN_PROTOCOL_VERSIONS = ['2026-07-28'];
19
+ // 2025-03-26 is deliberately absent: it is the one revision that allowed JSON-RPC batching, which these
20
+ // line-at-a-time handlers do not accept. A client asking for it is offered 2025-11-25 instead.
21
+ export const LEGACY_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2024-11-05'];
22
+ export const SUPPORTED_PROTOCOL_VERSIONS = [
23
+ ...MODERN_PROTOCOL_VERSIONS,
24
+ ...LEGACY_PROTOCOL_VERSIONS,
25
+ ];
26
+ /** Offered to a legacy client whose requested version we do not know. */
27
+ export const LATEST_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
28
+ export const META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
29
+ export const META_CLIENT_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities';
30
+ export const META_SERVER_INFO = 'io.modelcontextprotocol/serverInfo';
31
+ export const ERROR_INVALID_PARAMS = -32602;
32
+ export const ERROR_UNSUPPORTED_PROTOCOL_VERSION = -32022;
33
+ /** 2025-11-25 and earlier; replaced by -32602 in 2026-07-28. */
34
+ export const ERROR_LEGACY_RESOURCE_NOT_FOUND = -32002;
35
+ /** Tool catalogue is fixed for the life of a stdio process; five minutes is a conservative hint. */
36
+ export const TOOLS_LIST_TTL_MS = 300_000;
37
+ export const DISCOVER_TTL_MS = 3_600_000;
38
+ const LIST_METHODS = new Set(['tools/list', 'prompts/list', 'resources/list', 'resources/templates/list']);
39
+ // W3C Trace Context: version-traceid-parentid-flags, lowercase hex, ids not all zero, version != ff.
40
+ const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
41
+ let cachedVersion;
42
+ /** The CLI package version, read once from package.json (dist/src/ → ../../). */
43
+ export function rufloPackageVersion() {
44
+ if (cachedVersion)
45
+ return cachedVersion;
46
+ for (const rel of ['../../package.json', '../package.json']) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(new URL(rel, import.meta.url), 'utf8'));
49
+ if (pkg.name === '@claude-flow/cli' && typeof pkg.version === 'string') {
50
+ cachedVersion = pkg.version;
51
+ return cachedVersion;
52
+ }
53
+ }
54
+ catch {
55
+ // try the next candidate
56
+ }
57
+ }
58
+ cachedVersion = '0.0.0';
59
+ return cachedVersion;
60
+ }
61
+ export function defaultServerIdentity() {
62
+ return { name: 'ruflo', version: rufloPackageVersion() };
63
+ }
64
+ /** Pick the legacy version to answer `initialize` with (spec: echo if supported, else our latest). */
65
+ export function negotiateLegacyVersion(requested) {
66
+ return typeof requested === 'string' && LEGACY_PROTOCOL_VERSIONS.includes(requested)
67
+ ? requested
68
+ : LATEST_LEGACY_PROTOCOL_VERSION;
69
+ }
70
+ export function isValidTraceparent(value) {
71
+ if (typeof value !== 'string')
72
+ return false;
73
+ const m = TRACEPARENT.exec(value);
74
+ if (!m)
75
+ return false;
76
+ const [, version, traceId, parentId] = m;
77
+ return version !== 'ff' && !/^0+$/.test(traceId) && !/^0+$/.test(parentId);
78
+ }
79
+ function metaOf(params) {
80
+ if (!params || typeof params !== 'object')
81
+ return undefined;
82
+ const meta = params._meta;
83
+ return meta && typeof meta === 'object' && !Array.isArray(meta) ? meta : undefined;
84
+ }
85
+ function error(id, code, message, data) {
86
+ return { jsonrpc: '2.0', id: id ?? null, error: data === undefined ? { code, message } : { code, message, data } };
87
+ }
88
+ /**
89
+ * Decide how to serve one inbound message before the handler's own switch runs.
90
+ * Returns a complete response for `initialize`, `server/discover` and protocol
91
+ * errors; otherwise returns the context the handler passes to `finalizeResponse`.
92
+ */
93
+ export function protocolPreflight(message, capabilities, identity = defaultServerIdentity()) {
94
+ const meta = metaOf(message.params);
95
+ const traceparent = isValidTraceparent(meta?.traceparent) ? meta.traceparent : undefined;
96
+ const tracestate = traceparent && typeof meta?.tracestate === 'string' && meta.tracestate.length <= 512
97
+ ? meta.tracestate
98
+ : undefined;
99
+ const declared = meta?.[META_PROTOCOL_VERSION];
100
+ const isNotification = message.id === undefined || message.id === null;
101
+ if (message.method === 'initialize') {
102
+ const requested = message.params?.protocolVersion;
103
+ return {
104
+ response: {
105
+ jsonrpc: '2.0',
106
+ id: message.id,
107
+ result: { protocolVersion: negotiateLegacyVersion(requested), serverInfo: identity, capabilities },
108
+ },
109
+ };
110
+ }
111
+ if (declared !== undefined && !isNotification) {
112
+ if (typeof declared !== 'string' || !SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
113
+ return {
114
+ response: error(message.id, ERROR_UNSUPPORTED_PROTOCOL_VERSION, 'Unsupported protocol version', {
115
+ supported: [...SUPPORTED_PROTOCOL_VERSIONS],
116
+ requested: declared,
117
+ }),
118
+ };
119
+ }
120
+ const modern = MODERN_PROTOCOL_VERSIONS.includes(declared);
121
+ if (modern) {
122
+ const caps = meta?.[META_CLIENT_CAPABILITIES];
123
+ if (message.method !== 'server/discover' && (!caps || typeof caps !== 'object' || Array.isArray(caps))) {
124
+ return { response: error(message.id, ERROR_INVALID_PARAMS, `Missing required _meta field: ${META_CLIENT_CAPABILITIES}`) };
125
+ }
126
+ }
127
+ if (message.method === 'server/discover')
128
+ return { response: discoverResponse(message.id, capabilities, identity) };
129
+ return { context: { modern, version: declared, traceparent, tracestate } };
130
+ }
131
+ if (message.method === 'server/discover')
132
+ return { response: discoverResponse(message.id, capabilities, identity) };
133
+ return { context: { modern: false, traceparent, tracestate } };
134
+ }
135
+ function discoverResponse(id, capabilities, identity) {
136
+ return {
137
+ jsonrpc: '2.0',
138
+ id,
139
+ result: {
140
+ resultType: 'complete',
141
+ supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
142
+ capabilities,
143
+ _meta: { [META_SERVER_INFO]: identity },
144
+ ttlMs: DISCOVER_TTL_MS,
145
+ cacheScope: 'private',
146
+ },
147
+ };
148
+ }
149
+ /** The resource-not-found code the request's revision expects. */
150
+ export function resourceNotFoundCode(context) {
151
+ return context.modern ? ERROR_INVALID_PARAMS : ERROR_LEGACY_RESOURCE_NOT_FOUND;
152
+ }
153
+ /** Stable, name-ordered copy so `tools/list` is byte-identical across calls (prompt-cache friendly). */
154
+ export function sortToolsDeterministically(tools) {
155
+ return [...tools].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
156
+ }
157
+ /** Tool-call context fields to forward, so handlers and logs can join the caller's trace. */
158
+ export function traceContextFields(context) {
159
+ const out = {};
160
+ if (context.traceparent)
161
+ out.traceparent = context.traceparent;
162
+ if (context.tracestate)
163
+ out.tracestate = context.tracestate;
164
+ return out;
165
+ }
166
+ /** Add the 2026-07-28 result fields to a handler's response. Legacy responses pass through untouched. */
167
+ export function finalizeResponse(response, method, context, identity = defaultServerIdentity()) {
168
+ if (!response || !context.modern || !response.result)
169
+ return response;
170
+ const result = { resultType: 'complete', ...response.result };
171
+ const priorMeta = result._meta && typeof result._meta === 'object' ? result._meta : {};
172
+ result._meta = { ...priorMeta, [META_SERVER_INFO]: identity };
173
+ if (method && LIST_METHODS.has(method)) {
174
+ result.ttlMs = TOOLS_LIST_TTL_MS;
175
+ result.cacheScope = 'private';
176
+ }
177
+ return { ...response, result };
178
+ }
179
+ //# sourceMappingURL=mcp-protocol.js.map
@@ -182,6 +182,7 @@ export declare class MCPServerManager extends EventEmitter {
182
182
  * Handle incoming MCP message
183
183
  */
184
184
  private handleMCPMessage;
185
+ private dispatchMCPMessage;
185
186
  /**
186
187
  * Start HTTP server in-process
187
188
  */
@@ -26,6 +26,7 @@ import * as os from 'os';
26
26
  import { fileURLToPath } from 'url';
27
27
  import { dirname } from 'path';
28
28
  import { trackRequest } from './mcp-tools/request-tracker.js';
29
+ import { defaultServerIdentity, finalizeResponse, protocolPreflight, resourceNotFoundCode, sortToolsDeterministically, traceContextFields, } from './mcp-protocol.js';
29
30
  import { isPolicyEnforcementEnabled, loadMcpPolicy, evaluateToolCall, } from './mcp-tools/policy-enforcer.js';
30
31
  /** A failed task can be valid data; only an explicit error denotes tool failure. */
31
32
  function hasToolError(result) {
@@ -275,6 +276,11 @@ export function assessMcpSchemaOverhead(tools, contextWindowTokens) {
275
276
  : 'normal',
276
277
  };
277
278
  }
279
+ /** Capabilities every ruflo stdio server advertises (initialize and server/discover). */
280
+ const MCP_STDIO_CAPABILITIES = {
281
+ tools: { listChanged: true },
282
+ resources: {},
283
+ };
278
284
  /**
279
285
  * MCP Server Manager
280
286
  *
@@ -616,7 +622,7 @@ export class MCPServerManager extends EventEmitter {
616
622
  });
617
623
  // Import the tool registry
618
624
  const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
619
- const VERSION = '3.0.0';
625
+ const VERSION = defaultServerIdentity().version;
620
626
  const sessionId = `mcp-${Date.now()}-${randomUUID().slice(0, 8)}`;
621
627
  // Log to stderr to not corrupt stdout
622
628
  console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Starting in stdio mode`);
@@ -663,10 +669,7 @@ export class MCPServerManager extends EventEmitter {
663
669
  serverInfo: {
664
670
  name: 'ruflo',
665
671
  version: VERSION,
666
- capabilities: {
667
- tools: { listChanged: true },
668
- resources: {},
669
- },
672
+ capabilities: MCP_STDIO_CAPABILITIES,
670
673
  },
671
674
  },
672
675
  });
@@ -722,7 +725,6 @@ export class MCPServerManager extends EventEmitter {
722
725
  * Handle incoming MCP message
723
726
  */
724
727
  async handleMCPMessage(message, sessionId) {
725
- const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
726
728
  if (!message.method) {
727
729
  return {
728
730
  jsonrpc: '2.0',
@@ -730,24 +732,20 @@ export class MCPServerManager extends EventEmitter {
730
732
  error: { code: -32600, message: 'Invalid Request: missing method' },
731
733
  };
732
734
  }
735
+ // ADR-524: one decision point for which MCP revision this message speaks.
736
+ const preflight = protocolPreflight(message, MCP_STDIO_CAPABILITIES);
737
+ if (preflight.response)
738
+ return preflight.response;
739
+ const response = await this.dispatchMCPMessage(message, sessionId, preflight.context);
740
+ return finalizeResponse(response, message.method, preflight.context);
741
+ }
742
+ async dispatchMCPMessage(message, sessionId, protocol) {
743
+ const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
733
744
  const params = (message.params || {});
734
745
  try {
735
746
  switch (message.method) {
736
- case 'initialize':
737
- return {
738
- jsonrpc: '2.0',
739
- id: message.id,
740
- result: {
741
- protocolVersion: '2024-11-05',
742
- serverInfo: { name: 'ruflo', version: '3.0.0' },
743
- capabilities: {
744
- tools: { listChanged: true },
745
- resources: {},
746
- },
747
- },
748
- };
749
747
  case 'tools/list':
750
- const tools = filterAdvertisedMcpTools(listMCPTools(), this.options.tools);
748
+ const tools = sortToolsDeterministically(filterAdvertisedMcpTools(listMCPTools(), this.options.tools));
751
749
  return {
752
750
  jsonrpc: '2.0',
753
751
  id: message.id,
@@ -781,7 +779,7 @@ export class MCPServerManager extends EventEmitter {
781
779
  }
782
780
  }
783
781
  try {
784
- const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio' });
782
+ const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio', ...traceContextFields(protocol) });
785
783
  const isError = hasToolError(result);
786
784
  trackRequest(toolName, !isError);
787
785
  return {
@@ -808,7 +806,7 @@ export class MCPServerManager extends EventEmitter {
808
806
  case 'resources/templates/list':
809
807
  return { jsonrpc: '2.0', id: message.id, result: { resourceTemplates: [] } };
810
808
  case 'resources/read':
811
- return { jsonrpc: '2.0', id: message.id, error: { code: -32002, message: 'Resource not found' } };
809
+ return { jsonrpc: '2.0', id: message.id, error: { code: resourceNotFoundCode(protocol), message: 'Resource not found' } };
812
810
  case 'notifications/initialized':
813
811
  // Client notification - no response needed
814
812
  console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Client initialized`);
@@ -147,10 +147,18 @@ export async function callAnthropicMessages(input) {
147
147
  // checked in that order — matching callOllamaCompat's own precedence).
148
148
  const resolvedBaseUrl = process.env.OLLAMA_BASE_URL || persistedOllama?.baseUrl;
149
149
  const isSelfHosted = !!resolvedBaseUrl && !/^https:\/\/ollama\.com\/?$/i.test(resolvedBaseUrl);
150
+ // The persisted baseUrl comes from project config, which a cloned repo
151
+ // can commit. Never hand the environment's OLLAMA_API_KEY to an endpoint
152
+ // the project chose: that endpoint gets only the key saved beside it (or
153
+ // none). The env key goes to OLLAMA_BASE_URL, or to Ollama Cloud itself.
154
+ const baseFromProject = !process.env.OLLAMA_BASE_URL && !!persistedOllama?.baseUrl && isSelfHosted;
155
+ const apiKey = baseFromProject
156
+ ? persistedOllama?.apiKey || 'local'
157
+ : ollamaKey || persistedOllama?.apiKey || 'local';
150
158
  if (ollamaKey || persistedOllama?.apiKey || isSelfHosted) {
151
159
  return callOllamaCompat({
152
160
  ...input,
153
- apiKey: ollamaKey || persistedOllama?.apiKey || 'local',
161
+ apiKey,
154
162
  baseUrl: resolvedBaseUrl,
155
163
  model: input.model,
156
164
  // Local default for tier aliases / Anthropic ids (see
@@ -5,5 +5,18 @@
5
5
  * Includes model routing integration for intelligent model selection.
6
6
  */
7
7
  import { type MCPTool } from './types.js';
8
+ /** Live canonical registry members, excluding stale or duplicate swarm IDs. */
9
+ export declare function getLiveAgentIds(agentIds: string[]): string[];
10
+ export declare const AGENT_STATUSES: readonly ["idle", "busy", "terminated"];
11
+ type AgentLifecycleStatus = (typeof AGENT_STATUSES)[number];
12
+ /** Canonical lifecycle status (trimmed, any case, known aliases), or null. */
13
+ export declare function agentStatusOf(value: unknown): AgentLifecycleStatus | null;
14
+ /**
15
+ * Statuses `agent_list` should keep. `null` means no status filter:
16
+ * omitted, empty, whitespace, or `all` (any case, including inside a comma list).
17
+ * The caller still hides terminated agents unless includeTerminated is set.
18
+ */
19
+ export declare function agentListStatusFilter(status: unknown): string[] | null;
8
20
  export declare const agentTools: MCPTool[];
21
+ export {};
9
22
  //# sourceMappingURL=agent-tools.d.ts.map
@@ -36,6 +36,11 @@ function ensureAgentDir() {
36
36
  function loadAgentStore() {
37
37
  return readRecordStore(getAgentPath(), 'agents', () => ({ agents: {}, version: '3.0.0' }));
38
38
  }
39
+ /** Live canonical registry members, excluding stale or duplicate swarm IDs. */
40
+ export function getLiveAgentIds(agentIds) {
41
+ const { agents } = loadAgentStore();
42
+ return [...new Set(agentIds)].filter(id => agents[id] && agents[id].status !== 'terminated');
43
+ }
39
44
  function saveAgentStore(store) {
40
45
  ensureAgentDir();
41
46
  writeFileSync(getAgentPath(), JSON.stringify(store, null, 2), 'utf-8');
@@ -67,6 +72,40 @@ function loadHiveAgents() {
67
72
  function loadAllAgents() {
68
73
  return Object.assign(nullPrototypeRecords(undefined), loadHiveAgents(), loadAgentStore().agents);
69
74
  }
75
+ export const AGENT_STATUSES = ['idle', 'busy', 'terminated'];
76
+ // Spellings callers write for the three stored states. v2 `agent/list` used
77
+ // "active" for a working agent; hive liveness folds running/working into busy.
78
+ const AGENT_STATUS_ALIASES = {
79
+ active: 'busy', running: 'busy', working: 'busy',
80
+ ready: 'idle', waiting: 'idle',
81
+ stopped: 'terminated', inactive: 'terminated', offline: 'terminated',
82
+ };
83
+ /** Canonical lifecycle status (trimmed, any case, known aliases), or null. */
84
+ export function agentStatusOf(value) {
85
+ if (typeof value !== 'string')
86
+ return null;
87
+ const key = value.trim().toLowerCase();
88
+ if (AGENT_STATUSES.includes(key))
89
+ return key;
90
+ return Object.hasOwn(AGENT_STATUS_ALIASES, key) ? AGENT_STATUS_ALIASES[key] : null;
91
+ }
92
+ /**
93
+ * Statuses `agent_list` should keep. `null` means no status filter:
94
+ * omitted, empty, whitespace, or `all` (any case, including inside a comma list).
95
+ * The caller still hides terminated agents unless includeTerminated is set.
96
+ */
97
+ export function agentListStatusFilter(status) {
98
+ if (typeof status !== 'string' || status.trim() === '')
99
+ return null;
100
+ const canonical = status.split(',').map(part => part.trim()).filter(Boolean).map(part => {
101
+ if (part.toLowerCase() === 'all')
102
+ return 'all';
103
+ return agentStatusOf(part) ?? part.toLowerCase();
104
+ });
105
+ if (canonical.length === 0 || canonical.includes('all'))
106
+ return null;
107
+ return canonical;
108
+ }
70
109
  // Default model mappings for agent types (can be overridden)
71
110
  const AGENT_TYPE_MODEL_DEFAULTS = {
72
111
  // Complex agents → opus
@@ -568,17 +607,24 @@ export const agentTools = [
568
607
  inputSchema: {
569
608
  type: 'object',
570
609
  properties: {
571
- status: { type: 'string', description: 'Filter by status' },
610
+ status: { type: 'string', description: 'Filter by status (idle, busy, terminated; case-insensitive, aliases such as active/running/stopped). "all" or empty applies no status filter. Comma-separated values match any of them.' },
572
611
  domain: { type: 'string', description: 'Filter by domain' },
573
612
  agentType: { type: 'string', description: 'Filter by agent type' },
574
613
  includeTerminated: { type: 'boolean', description: 'Include terminated agents' },
575
614
  },
576
615
  },
577
616
  handler: async (input) => {
578
- if (input.status) {
579
- const v = validateIdentifier(input.status, 'status');
580
- if (!v.valid)
581
- return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
617
+ // Validate each status token. Commas separate values (`active,idle`);
618
+ // the whole string is not one identifier.
619
+ if (typeof input.status === 'string' && input.status.trim() !== '') {
620
+ for (const part of input.status.split(',')) {
621
+ const token = part.trim();
622
+ if (!token || token.toLowerCase() === 'all')
623
+ continue;
624
+ const v = validateIdentifier(token, 'status');
625
+ if (!v.valid)
626
+ return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
627
+ }
582
628
  }
583
629
  if (input.domain) {
584
630
  const v = validateIdentifier(input.domain, 'domain');
@@ -591,9 +637,14 @@ export const agentTools = [
591
637
  return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
592
638
  }
593
639
  let agents = Object.values(loadAllAgents()); // #1916: includes hive-mind-spawned workers
594
- // Filter by status
595
- if (input.status) {
596
- agents = agents.filter(a => a.status === input.status);
640
+ // Filter by status. `all` and empty mean no status filter (#4732),
641
+ // matching task_list: comma-separated, case-insensitive, known aliases.
642
+ const statuses = agentListStatusFilter(input.status);
643
+ if (statuses) {
644
+ agents = agents.filter(a => {
645
+ const stored = agentStatusOf(a.status) ?? String(a.status).trim().toLowerCase();
646
+ return statuses.includes(stored);
647
+ });
597
648
  }
598
649
  else if (!input.includeTerminated) {
599
650
  agents = agents.filter(a => a.status !== 'terminated');
@@ -1502,6 +1502,8 @@ export const agentdbGraphPathfinder = {
1502
1502
  }
1503
1503
  const edges = rawEdges;
1504
1504
  let paths = [];
1505
+ let mincutInfo;
1506
+ let approximation;
1505
1507
  // Check millisecond budget before heavy computation
1506
1508
  if (Date.now() - t0 > budget.maxMillis) {
1507
1509
  return { success: true, paths: [], count: 0, message: `complexityBudget.maxMillis (${budget.maxMillis}ms) exceeded before solver dispatch`, seedNodeId, algorithm, elapsedMs: Date.now() - t0 };
@@ -1557,10 +1559,38 @@ export const agentdbGraphPathfinder = {
1557
1559
  paths = witnessChain.slice(0, topK);
1558
1560
  break;
1559
1561
  }
1562
+ case 'dynamic-mincut': {
1563
+ // Exact minimum cut of the seed's depth-hop neighbourhood: the
1564
+ // weakest boundary around the seed, and the nodes on its side of it.
1565
+ // Wasm solver via ruvector (@ruvector/mincut-wasm), JS Stoer-Wagner
1566
+ // fallback for small neighbourhoods.
1567
+ const edgeTuples = edges.map(r => [String(r[0]), String(r[1]), Number(r[2]) || 1.0]);
1568
+ const { seedLocalMinCut } = await import('../ruvector/mincut-bridge.js');
1569
+ const { fallbackMinCut } = await import('../ruvector/graph-analyzer.js');
1570
+ const cut = await seedLocalMinCut(seedNodeId, edgeTuples, depth, budget.maxNodesVisited, fallbackMinCut);
1571
+ if (!cut) {
1572
+ mincutInfo = { backend: 'none', message: 'seed neighbourhood too large for the JS fallback and the wasm solver is unavailable (install ruvector)' };
1573
+ break;
1574
+ }
1575
+ paths = cut.seedSide
1576
+ .map(r => ({ nodeId: r.nodeId, score: 1 / (1 + r.depth), depth: r.depth }))
1577
+ .filter(r => r.score >= threshold)
1578
+ .slice(0, topK);
1579
+ mincutInfo = {
1580
+ backend: cut.backend,
1581
+ cutValue: cut.cutValue,
1582
+ seedSideSize: cut.seedSide.length + 1,
1583
+ otherSideSize: cut.otherSide.length,
1584
+ subgraphNodes: cut.subgraphNodes,
1585
+ cutEdges: cut.cutEdges.slice(0, 50),
1586
+ };
1587
+ break;
1588
+ }
1560
1589
  case 'connected-component-churn':
1561
- case 'dynamic-mincut':
1562
1590
  case 'spectral-sparsify': {
1563
- // Simplified implementations: return k-hop neighbors with basic score
1591
+ // Not implemented natively yet: approximated by k-hop neighbours with
1592
+ // a rank-decay score. The response says so via `implementation`.
1593
+ approximation = 'k-hop';
1564
1594
  const edgeTuples = edges.map(r => [r[0], r[1], Number(r[2]) || 1.0]);
1565
1595
  const khopResult = await agentdbGraphQuery.handler({
1566
1596
  nodeId: seedNodeId, mode: 'k-hop', depth, complexityBudget: budget,
@@ -1582,6 +1612,8 @@ export const agentdbGraphPathfinder = {
1582
1612
  count: paths.length,
1583
1613
  elapsedMs,
1584
1614
  budgetUsed: { millis: elapsedMs, nodes: rawEdges.length },
1615
+ ...(mincutInfo && { mincut: mincutInfo }),
1616
+ ...(approximation && { implementation: `${approximation} approximation` }),
1585
1617
  ...(depthWarning && { warning: depthWarning }),
1586
1618
  };
1587
1619
  }
@@ -15,4 +15,13 @@ export declare function launchArgsError(value: unknown): string | null;
15
15
  * pass — for example a selector or typed text of "--executable-path".
16
16
  */
17
17
  export declare function commandArgvError(args: readonly string[]): string | null;
18
+ /**
19
+ * Returns an error when a navigation target is not a web page. The browser
20
+ * tools return page text to the caller, so a `file:` URL (or `view-source:`,
21
+ * `chrome:`, `data:` and the like) would read local files or browser state
22
+ * that `http_fetch` never serves. Only http(s) URLs, `about:blank`, and
23
+ * scheme-less targets such as `example.com/path` or `localhost:3000` (which
24
+ * agent-browser opens over http(s)) are accepted.
25
+ */
26
+ export declare function browserUrlError(value: unknown): string | null;
18
27
  //# sourceMappingURL=browser-argv.d.ts.map
@@ -42,6 +42,11 @@ export function launchArgsError(value) {
42
42
  * pass — for example a selector or typed text of "--executable-path".
43
43
  */
44
44
  export function commandArgvError(args) {
45
+ if (args[0] === 'open') {
46
+ const urlError = browserUrlError(args[1]);
47
+ if (urlError)
48
+ return urlError;
49
+ }
45
50
  for (const arg of args) {
46
51
  if (typeof arg !== 'string')
47
52
  return 'browser command arguments must be strings';
@@ -56,4 +61,32 @@ export function commandArgvError(args) {
56
61
  }
57
62
  return null;
58
63
  }
64
+ /**
65
+ * Returns an error when a navigation target is not a web page. The browser
66
+ * tools return page text to the caller, so a `file:` URL (or `view-source:`,
67
+ * `chrome:`, `data:` and the like) would read local files or browser state
68
+ * that `http_fetch` never serves. Only http(s) URLs, `about:blank`, and
69
+ * scheme-less targets such as `example.com/path` or `localhost:3000` (which
70
+ * agent-browser opens over http(s)) are accepted.
71
+ */
72
+ export function browserUrlError(value) {
73
+ if (typeof value !== 'string' || !value.trim())
74
+ return 'url must be a non-empty string';
75
+ // Chromium drops tabs and newlines anywhere in a URL and trims leading
76
+ // control characters and spaces, so check what it would actually parse.
77
+ const url = value.replace(/[\t\n\r]/g, '').replace(/^[\u0000- ]+/, '');
78
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(url);
79
+ if (!scheme)
80
+ return null;
81
+ const name = scheme[1].toLowerCase();
82
+ if (name === 'http' || name === 'https')
83
+ return null;
84
+ if (url.toLowerCase() === 'about:blank')
85
+ return null;
86
+ // `host:port` with no scheme, e.g. `localhost:3000/app`. The host must be
87
+ // `localhost` or contain a dot, so `file:1/...` is not mistaken for one.
88
+ if ((name === 'localhost' || name.includes('.')) && /^[a-z0-9.-]+:\d{1,5}(?:[/?#]|$)/i.test(url))
89
+ return null;
90
+ return `url scheme not allowed: ${JSON.stringify(name.slice(0, 40))} (use http or https)`;
91
+ }
59
92
  //# sourceMappingURL=browser-argv.js.map
@@ -22,6 +22,7 @@
22
22
  * rather than a process crash.
23
23
  */
24
24
  import { validateIdentifier, validateText } from './validate-input.js';
25
+ import { browserUrlError } from './browser-argv.js';
25
26
  // Pin matches the version in this package's own dependency tree so the npx
26
27
  // fallback never downloads a second ruvector. Subcommand surface verified
27
28
  // against ruvector@0.2.27 --help: `rvf create/compact/status/derive/segments`
@@ -144,6 +145,9 @@ export const browserSessionTools = [
144
145
  const vUrl = validateText(input.url, 'url');
145
146
  if (!vUrl.valid)
146
147
  return fail(vUrl.error || 'invalid url');
148
+ const urlError = browserUrlError(input.url);
149
+ if (urlError)
150
+ return fail(urlError);
147
151
  const vTask = validateText(input.task, 'task');
148
152
  if (!vTask.valid)
149
153
  return fail(vTask.error || 'invalid task');
@@ -8,6 +8,8 @@ import { readFileSync, existsSync } from 'node:fs';
8
8
  import { validateIdentifier, validateText } from './validate-input.js';
9
9
  import { resolveAgentBrowserLaunch, resolveNpxLaunch } from '../runtime/browser-command.js';
10
10
  import { commandArgvError, launchArgsError } from './browser-argv.js';
11
+ import { containedWritePath } from './path-containment.js';
12
+ const SCREENSHOT_EXTENSIONS = ['.png', '.jpg', '.jpeg'];
11
13
  // Session registry for multi-session support
12
14
  const browserSessions = new Map();
13
15
  /** Preserve agent-browser's JSON error on non-zero exit without echoing other process output. */
@@ -324,8 +326,16 @@ export const browserTools = [
324
326
  }
325
327
  const { session, path, fullPage } = input;
326
328
  const args = ['screenshot'];
327
- if (path)
328
- args.push(path);
329
+ if (path) {
330
+ // The browser writes the image wherever it is told and replaces what
331
+ // is there, so a caller-named path must stay inside the project.
332
+ const target = containedWritePath(path, SCREENSHOT_EXTENSIONS);
333
+ if (!target) {
334
+ const error = `path must be a new or regular file inside the project ending in ${SCREENSHOT_EXTENSIONS.join(', ')}`;
335
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error }) }], isError: true };
336
+ }
337
+ args.push(target);
338
+ }
329
339
  if (fullPage)
330
340
  args.push('--full');
331
341
  return execBrowserCommand(args, session);
@@ -346,11 +346,12 @@ export const coordinationTools = [
346
346
  }
347
347
  if (action === 'heartbeat') {
348
348
  const nodeId = input.nodeId;
349
- if (store.nodes[nodeId]) {
350
- store.nodes[nodeId].lastHeartbeat = new Date().toISOString();
351
- store.nodes[nodeId].status = 'active';
352
- saveCoordStore(store);
349
+ if (!store.nodes[nodeId]) {
350
+ return { success: false, error: 'Node not found' };
353
351
  }
352
+ store.nodes[nodeId].lastHeartbeat = new Date().toISOString();
353
+ store.nodes[nodeId].status = 'active';
354
+ saveCoordStore(store);
354
355
  return {
355
356
  success: true,
356
357
  action: 'heartbeat',
@@ -50,11 +50,14 @@ async function getRealEmbeddingFunction() {
50
50
  }
51
51
  return realEmbeddingFn;
52
52
  }
53
- async function generateRealEmbedding(text, dimension) {
53
+ async function generateRealEmbedding(text, dimension, normalize = true) {
54
54
  const realFn = await getRealEmbeddingFunction();
55
55
  if (realFn) {
56
56
  try {
57
- const result = await realFn(text);
57
+ // Default callers stay a single-argument call so existing mocks and
58
+ // the unit-norm path are unchanged. Only an explicit opt-out asks the
59
+ // generator for the pre-L2 vector.
60
+ const result = normalize ? await realFn(text) : await realFn(text, { normalize: false });
58
61
  return {
59
62
  embedding: result.embedding,
60
63
  backend: result.backend ?? 'onnx',
@@ -76,7 +79,10 @@ async function generateRealEmbedding(text, dimension) {
76
79
  const seed = hash + i * 1337;
77
80
  embedding.push(Math.sin(seed) * Math.cos(seed * 0.5));
78
81
  }
79
- // L2 normalize
82
+ // L2 normalize unless the caller opted out (#4724).
83
+ if (!normalize) {
84
+ return { embedding, backend: 'mock', model: 'hash-fallback' };
85
+ }
80
86
  const norm = Math.sqrt(embedding.reduce((sum, x) => sum + x * x, 0));
81
87
  return {
82
88
  embedding: embedding.map(x => x / norm),
@@ -256,8 +262,10 @@ export const embeddingsTools = [
256
262
  return { success: false, error: v.error };
257
263
  }
258
264
  const useHyperbolic = input.hyperbolic === true && config.hyperbolic.enabled;
265
+ // Default true: omitted and explicit true keep the historical unit vector.
266
+ const shouldNormalize = input.normalize !== false;
259
267
  // Generate real ONNX embedding
260
- const generated = await generateRealEmbedding(text, config.dimension);
268
+ const generated = await generateRealEmbedding(text, config.dimension, shouldNormalize);
261
269
  const embedding = generated.embedding;
262
270
  let result;
263
271
  let geometry;