@claude-flow/cli 3.71.4 → 3.72.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 +4 -4
- package/.claude/helpers/hook-handler.cjs +23 -10
- package/.claude/helpers/statusline.cjs +29 -14
- package/README.md +18 -3
- package/bin/cli.js +13 -18
- package/bin/mcp-server.js +13 -18
- package/catalog-manifest.json +2 -2
- package/dist/src/appliance/rvfa-distribution.js +2 -2
- package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
- package/dist/src/benchmarks/gaia-critic.js +1 -7
- package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
- package/dist/src/benchmarks/gaia-decomposer.js +1 -8
- package/dist/src/commands/agent-wasm.js +2 -2
- package/dist/src/commands/analyze.js +1 -1
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/commands/doctor.js +3 -2
- package/dist/src/commands/guidance.js +9 -9
- package/dist/src/commands/hooks.js +13 -8
- package/dist/src/commands/init.js +14 -16
- package/dist/src/commands/neural.js +8 -6
- package/dist/src/commands/route.js +1 -1
- package/dist/src/commands/ruvector/backup.js +7 -5
- package/dist/src/commands/ruvector/benchmark.js +6 -1
- package/dist/src/commands/ruvector/import.js +19 -5
- package/dist/src/commands/ruvector/init.js +1 -1
- package/dist/src/commands/start.js +1 -1
- package/dist/src/commands/status.js +1 -1
- package/dist/src/commands/task.js +18 -1
- package/dist/src/commands/update.js +1 -1
- package/dist/src/funnel/local-signals.js +7 -2
- package/dist/src/mcp-protocol.d.ts +90 -0
- package/dist/src/mcp-protocol.js +179 -0
- package/dist/src/mcp-server.d.ts +1 -0
- package/dist/src/mcp-server.js +20 -22
- package/dist/src/mcp-tools/agent-execute-core.js +9 -1
- package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
- package/dist/src/mcp-tools/agent-tools.js +59 -8
- package/dist/src/mcp-tools/browser-argv.d.ts +9 -0
- package/dist/src/mcp-tools/browser-argv.js +33 -0
- package/dist/src/mcp-tools/browser-session-tools.js +4 -0
- package/dist/src/mcp-tools/browser-tools.js +12 -2
- package/dist/src/mcp-tools/coordination-tools.js +5 -4
- package/dist/src/mcp-tools/embeddings-tools.js +12 -4
- package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
- package/dist/src/mcp-tools/glob-exclusions.js +81 -0
- package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
- package/dist/src/mcp-tools/hooks-tools.js +12 -5
- package/dist/src/mcp-tools/import-limits.d.ts +4 -0
- package/dist/src/mcp-tools/import-limits.js +28 -0
- package/dist/src/mcp-tools/memory-tools.js +21 -42
- package/dist/src/mcp-tools/performance-tools.js +7 -2
- package/dist/src/mcp-tools/record-store.d.ts +8 -0
- package/dist/src/mcp-tools/record-store.js +98 -1
- package/dist/src/mcp-tools/security-tools.d.ts +9 -0
- package/dist/src/mcp-tools/security-tools.js +39 -3
- package/dist/src/mcp-tools/session-tools.js +5 -3
- package/dist/src/mcp-tools/swarm-tools.js +35 -17
- package/dist/src/mcp-tools/task-tools.js +68 -5
- package/dist/src/mcp-tools/terminal-tools.js +9 -0
- package/dist/src/mcp-tools/workflow-tools.js +13 -12
- package/dist/src/mcp-tools/x-federation-join.js +55 -14
- package/dist/src/memory/intelligence.d.ts +11 -0
- package/dist/src/memory/intelligence.js +84 -6
- package/dist/src/memory/memory-initializer.d.ts +6 -2
- package/dist/src/memory/memory-initializer.js +15 -6
- package/dist/src/ruvector/model-router.d.ts +6 -1
- package/dist/src/ruvector/model-router.js +16 -4
- package/dist/src/services/worker-daemon.js +30 -12
- package/dist/src/transfer/storage/gcs.js +3 -2
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -0
|
@@ -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
|
package/dist/src/mcp-server.d.ts
CHANGED
package/dist/src/mcp-server.js
CHANGED
|
@@ -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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
596
|
-
|
|
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');
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Bounded glob matching for caller-provided memory-import exclusions. */
|
|
2
|
+
export declare const MAX_EXCLUDE_PATTERNS = 64;
|
|
3
|
+
export declare const MAX_EXCLUDE_PATTERN_LENGTH = 1024;
|
|
4
|
+
export declare const MAX_EXCLUDE_WILDCARDS = 32;
|
|
5
|
+
export declare function compileExclusionPatterns(input: unknown): Array<(path: string) => boolean>;
|
|
6
|
+
//# sourceMappingURL=glob-exclusions.d.ts.map
|