chainlesschain 0.45.70 → 0.45.74
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/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +1 -0
- package/src/assets/web-panel/assets/Analytics-sBrYoc3A.js +3 -0
- package/src/assets/web-panel/assets/AppLayout-BhJ3YFWt.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-Cr2lWhF-.css +1 -0
- package/src/assets/web-panel/assets/Backup-D68fenbD.js +1 -0
- package/src/assets/web-panel/assets/Backup-fZqtfC1m.css +1 -0
- package/src/assets/web-panel/assets/{Chat-DXtvKoM0.js → Chat-DaxTP3x8.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-BJ4ODHOy.js → Cron-CNs03iHJ.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BZd4wDPQ.js → Dashboard-CjlX4CrX.js} +2 -2
- package/src/assets/web-panel/assets/Git-CCMVr3Y8.js +2 -0
- package/src/assets/web-panel/assets/Git-DGcuBXST.css +1 -0
- package/src/assets/web-panel/assets/{Logs-CSeKZEG_.js → Logs-BY6A0UNG.js} +2 -2
- package/src/assets/web-panel/assets/{McpTools-BYQAK11r.js → McpTools-CrBVYlg6.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-gkUAPyuZ.js → Memory-CWx3SpUt.js} +2 -2
- package/src/assets/web-panel/assets/{Notes-bjNrQgAo.js → Notes-1LcGD49x.js} +2 -2
- package/src/assets/web-panel/assets/Organization-DdOOM4ic.css +1 -0
- package/src/assets/web-panel/assets/Organization-Dx2DhbkM.js +4 -0
- package/src/assets/web-panel/assets/P2P-B16fjqfJ.js +2 -0
- package/src/assets/web-panel/assets/P2P-OEzOeMZX.css +1 -0
- package/src/assets/web-panel/assets/Permissions-BQbC9FzG.js +4 -0
- package/src/assets/web-panel/assets/Permissions-C9WlkGl-.css +1 -0
- package/src/assets/web-panel/assets/Projects-CjhZbNYm.js +2 -0
- package/src/assets/web-panel/assets/Projects-DxKelI5h.css +1 -0
- package/src/assets/web-panel/assets/Providers-BEakqcO5.css +1 -0
- package/src/assets/web-panel/assets/Providers-ivOAQtHM.js +2 -0
- package/src/assets/web-panel/assets/RssFeed-BlFC20eg.css +1 -0
- package/src/assets/web-panel/assets/RssFeed-BrsErdrU.js +3 -0
- package/src/assets/web-panel/assets/Security-DnEvJU5h.js +4 -0
- package/src/assets/web-panel/assets/Security-Dwxw7rfP.css +1 -0
- package/src/assets/web-panel/assets/{Services-CS0oMdxh.js → Services-7jQywNbl.js} +2 -2
- package/src/assets/web-panel/assets/Skills-BCvgBkD3.js +1 -0
- package/src/assets/web-panel/assets/{Tasks-qULws8pc.js → Tasks-CmJBC1cf.js} +1 -1
- package/src/assets/web-panel/assets/Templates-DOY_oZnm.css +1 -0
- package/src/assets/web-panel/assets/Templates-RXT8-DNk.js +1 -0
- package/src/assets/web-panel/assets/Wallet-3iYASEx_.js +4 -0
- package/src/assets/web-panel/assets/Wallet-DnIumafl.css +1 -0
- package/src/assets/web-panel/assets/WebAuthn-CNPl2VQR.css +1 -0
- package/src/assets/web-panel/assets/WebAuthn-s3Hzd9db.js +5 -0
- package/src/assets/web-panel/assets/{antd-CJSBocer.js → antd-gZyc63Qr.js} +114 -114
- package/src/assets/web-panel/assets/chat-BmwHBi9M.js +1 -0
- package/src/assets/web-panel/assets/index-DrmEk9S3.js +2 -0
- package/src/assets/web-panel/assets/{markdown-Bo5cVN4u.js → markdown-Bv7nG63L.js} +1 -1
- package/src/assets/web-panel/assets/ws-CU7Gvoom.js +1 -0
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/doctor.js +33 -151
- package/src/commands/mcp.js +1 -1
- package/src/commands/plugin.js +1 -1
- package/src/commands/session.js +106 -7
- package/src/commands/status.js +39 -69
- package/src/gateways/ws/session-protocol.js +1 -1
- package/src/gateways/ws/ws-agent-handler.js +484 -0
- package/src/gateways/ws/ws-server.js +758 -4
- package/src/gateways/ws/ws-session-gateway.js +1432 -1
- package/src/harness/mcp-client.js +417 -0
- package/src/harness/mock-llm-provider.js +167 -0
- package/src/harness/plugin-manager.js +434 -0
- package/src/lib/agent-core.js +25 -1902
- package/src/lib/hashline.js +208 -0
- package/src/lib/jsonl-session-store.js +11 -0
- package/src/lib/mcp-client.js +14 -412
- package/src/lib/plugin-manager.js +29 -428
- package/src/lib/prompt-compressor.js +11 -0
- package/src/lib/session-hooks.js +61 -0
- package/src/lib/skill-loader.js +4 -0
- package/src/lib/skill-mcp.js +190 -0
- package/src/lib/workflow-state-reader.js +94 -0
- package/src/lib/ws-agent-handler.js +8 -472
- package/src/lib/ws-server.js +12 -756
- package/src/lib/ws-session-manager.js +8 -1417
- package/src/repl/agent-repl.js +27 -3
- package/src/runtime/agent-core.js +1760 -0
- package/src/runtime/agent-runtime.js +3 -1
- package/src/runtime/coding-agent-contract-shared.cjs +496 -0
- package/src/runtime/coding-agent-contract.js +49 -229
- package/src/runtime/coding-agent-policy.cjs +54 -5
- package/src/runtime/diagnostics.js +317 -0
- package/src/runtime/index.js +3 -0
- package/src/tools/index.js +3 -0
- package/src/tools/legacy-agent-tools.js +5 -0
- package/src/assets/web-panel/assets/AppLayout-B_tkw3Pn.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +0 -1
- package/src/assets/web-panel/assets/Providers-Brm-S_hS.css +0 -1
- package/src/assets/web-panel/assets/Providers-Dbf57Tbv.js +0 -1
- package/src/assets/web-panel/assets/Skills-B2fgruv8.js +0 -1
- package/src/assets/web-panel/assets/chat-DnH09sSR.js +0 -1
- package/src/assets/web-panel/assets/index-IK-oro0g.js +0 -2
- package/src/assets/web-panel/assets/ws-DjelKkD6.js +0 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight MCP (Model Context Protocol) client.
|
|
3
|
+
* Implements JSON-RPC 2.0 over stdio transport without external SDK dependency.
|
|
4
|
+
*
|
|
5
|
+
* Canonical location (moved from src/lib/mcp-client.js as part of the
|
|
6
|
+
* CLI Runtime Convergence roadmap, Phase 3). src/lib/mcp-client.js is now a
|
|
7
|
+
* thin re-export shim for backwards compatibility.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { EventEmitter } from "events";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MCP Server connection states.
|
|
15
|
+
*/
|
|
16
|
+
export const ServerState = {
|
|
17
|
+
DISCONNECTED: "disconnected",
|
|
18
|
+
CONNECTING: "connecting",
|
|
19
|
+
CONNECTED: "connected",
|
|
20
|
+
ERROR: "error",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* MCP Client — manages connections to MCP servers.
|
|
25
|
+
*/
|
|
26
|
+
export class MCPClient extends EventEmitter {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.servers = new Map(); // name → { process, state, tools, resources, config }
|
|
30
|
+
this._nextId = 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Connect to an MCP server via stdio transport.
|
|
35
|
+
* @param {string} name - Server name
|
|
36
|
+
* @param {object} config - { command, args?, env? }
|
|
37
|
+
*/
|
|
38
|
+
async connect(name, config) {
|
|
39
|
+
if (this.servers.has(name)) {
|
|
40
|
+
throw new Error(`Server "${name}" already connected`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const entry = {
|
|
44
|
+
config,
|
|
45
|
+
state: ServerState.CONNECTING,
|
|
46
|
+
process: null,
|
|
47
|
+
tools: [],
|
|
48
|
+
resources: [],
|
|
49
|
+
prompts: [],
|
|
50
|
+
_pending: new Map(),
|
|
51
|
+
_buffer: "",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.servers.set(name, entry);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const proc = spawn(config.command, config.args || [], {
|
|
58
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
59
|
+
env: { ...process.env, ...(config.env || {}) },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
entry.process = proc;
|
|
63
|
+
|
|
64
|
+
proc.stdout.on("data", (data) => {
|
|
65
|
+
this._handleData(name, data.toString("utf8"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
proc.stderr.on("data", (data) => {
|
|
69
|
+
this.emit("server-error", { name, error: data.toString("utf8") });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
proc.on("close", (code) => {
|
|
73
|
+
entry.state = ServerState.DISCONNECTED;
|
|
74
|
+
this.emit("server-disconnected", { name, code });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
proc.on("error", (err) => {
|
|
78
|
+
entry.state = ServerState.ERROR;
|
|
79
|
+
this.emit("server-error", { name, error: err.message });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Initialize MCP protocol
|
|
83
|
+
const initResult = await this._sendRequest(name, "initialize", {
|
|
84
|
+
protocolVersion: "2024-11-05",
|
|
85
|
+
capabilities: { tools: {}, resources: {} },
|
|
86
|
+
clientInfo: { name: "chainlesschain-cli", version: "0.37.9" },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Send initialized notification
|
|
90
|
+
this._sendNotification(name, "notifications/initialized", {});
|
|
91
|
+
|
|
92
|
+
entry.state = ServerState.CONNECTED;
|
|
93
|
+
entry.serverInfo = initResult?.serverInfo || {};
|
|
94
|
+
entry.capabilities = initResult?.capabilities || {};
|
|
95
|
+
|
|
96
|
+
// Fetch available tools
|
|
97
|
+
try {
|
|
98
|
+
const toolsResult = await this._sendRequest(name, "tools/list", {});
|
|
99
|
+
entry.tools = toolsResult?.tools || [];
|
|
100
|
+
} catch {
|
|
101
|
+
// Server may not support tools
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Fetch available resources
|
|
105
|
+
try {
|
|
106
|
+
const resourcesResult = await this._sendRequest(
|
|
107
|
+
name,
|
|
108
|
+
"resources/list",
|
|
109
|
+
{},
|
|
110
|
+
);
|
|
111
|
+
entry.resources = resourcesResult?.resources || [];
|
|
112
|
+
} catch {
|
|
113
|
+
// Server may not support resources
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.emit("server-connected", { name, tools: entry.tools.length });
|
|
117
|
+
return {
|
|
118
|
+
name,
|
|
119
|
+
state: entry.state,
|
|
120
|
+
tools: entry.tools,
|
|
121
|
+
resources: entry.resources,
|
|
122
|
+
serverInfo: entry.serverInfo,
|
|
123
|
+
};
|
|
124
|
+
} catch (err) {
|
|
125
|
+
entry.state = ServerState.ERROR;
|
|
126
|
+
this.servers.delete(name);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Disconnect from an MCP server.
|
|
133
|
+
*/
|
|
134
|
+
async disconnect(name) {
|
|
135
|
+
const entry = this.servers.get(name);
|
|
136
|
+
if (!entry) return false;
|
|
137
|
+
|
|
138
|
+
if (entry.process) {
|
|
139
|
+
entry.process.kill();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
entry.state = ServerState.DISCONNECTED;
|
|
143
|
+
this.servers.delete(name);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Disconnect from all servers.
|
|
149
|
+
*/
|
|
150
|
+
async disconnectAll() {
|
|
151
|
+
const names = [...this.servers.keys()];
|
|
152
|
+
for (const name of names) {
|
|
153
|
+
await this.disconnect(name);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* List all connected servers.
|
|
159
|
+
*/
|
|
160
|
+
listServers() {
|
|
161
|
+
const result = [];
|
|
162
|
+
for (const [name, entry] of this.servers) {
|
|
163
|
+
result.push({
|
|
164
|
+
name,
|
|
165
|
+
state: entry.state,
|
|
166
|
+
tools: entry.tools.length,
|
|
167
|
+
resources: entry.resources.length,
|
|
168
|
+
serverInfo: entry.serverInfo || {},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* List tools from a specific server or all servers.
|
|
176
|
+
*/
|
|
177
|
+
listTools(serverName) {
|
|
178
|
+
if (serverName) {
|
|
179
|
+
const entry = this.servers.get(serverName);
|
|
180
|
+
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
181
|
+
return entry.tools.map((t) => ({ ...t, server: serverName }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const allTools = [];
|
|
185
|
+
for (const [name, entry] of this.servers) {
|
|
186
|
+
for (const tool of entry.tools) {
|
|
187
|
+
allTools.push({ ...tool, server: name });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return allTools;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Call a tool on a specific server.
|
|
195
|
+
* @param {string} serverName - Server name
|
|
196
|
+
* @param {string} toolName - Tool name
|
|
197
|
+
* @param {object} args - Tool arguments
|
|
198
|
+
*/
|
|
199
|
+
async callTool(serverName, toolName, args = {}) {
|
|
200
|
+
const entry = this.servers.get(serverName);
|
|
201
|
+
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
202
|
+
if (entry.state !== ServerState.CONNECTED) {
|
|
203
|
+
throw new Error(`Server "${serverName}" is not connected`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await this._sendRequest(serverName, "tools/call", {
|
|
207
|
+
name: toolName,
|
|
208
|
+
arguments: args,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Read a resource from a server.
|
|
216
|
+
*/
|
|
217
|
+
async readResource(serverName, uri) {
|
|
218
|
+
const entry = this.servers.get(serverName);
|
|
219
|
+
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
220
|
+
|
|
221
|
+
const result = await this._sendRequest(serverName, "resources/read", {
|
|
222
|
+
uri,
|
|
223
|
+
});
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Internal JSON-RPC transport ──────────────────────────────
|
|
228
|
+
|
|
229
|
+
_sendRequest(serverName, method, params) {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
const entry = this.servers.get(serverName);
|
|
232
|
+
if (!entry || !entry.process) {
|
|
233
|
+
return reject(new Error("Server not available"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const id = this._nextId++;
|
|
237
|
+
const message = JSON.stringify({
|
|
238
|
+
jsonrpc: "2.0",
|
|
239
|
+
id,
|
|
240
|
+
method,
|
|
241
|
+
params,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
entry._pending.set(id, { resolve, reject });
|
|
245
|
+
|
|
246
|
+
// Set timeout
|
|
247
|
+
const timeout = setTimeout(() => {
|
|
248
|
+
entry._pending.delete(id);
|
|
249
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
250
|
+
}, 30000);
|
|
251
|
+
|
|
252
|
+
entry._pending.get(id).timeout = timeout;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
entry.process.stdin.write(message + "\n");
|
|
256
|
+
} catch (err) {
|
|
257
|
+
clearTimeout(timeout);
|
|
258
|
+
entry._pending.delete(id);
|
|
259
|
+
reject(err);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_sendNotification(serverName, method, params) {
|
|
265
|
+
const entry = this.servers.get(serverName);
|
|
266
|
+
if (!entry || !entry.process) return;
|
|
267
|
+
|
|
268
|
+
const message = JSON.stringify({
|
|
269
|
+
jsonrpc: "2.0",
|
|
270
|
+
method,
|
|
271
|
+
params,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
entry.process.stdin.write(message + "\n");
|
|
276
|
+
} catch {
|
|
277
|
+
// Ignore notification errors
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_handleData(serverName, data) {
|
|
282
|
+
const entry = this.servers.get(serverName);
|
|
283
|
+
if (!entry) return;
|
|
284
|
+
|
|
285
|
+
entry._buffer += data;
|
|
286
|
+
|
|
287
|
+
// Process complete JSON lines
|
|
288
|
+
const lines = entry._buffer.split("\n");
|
|
289
|
+
entry._buffer = lines.pop() || "";
|
|
290
|
+
|
|
291
|
+
for (const line of lines) {
|
|
292
|
+
const trimmed = line.trim();
|
|
293
|
+
if (!trimmed) continue;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const msg = JSON.parse(trimmed);
|
|
297
|
+
this._handleMessage(serverName, msg);
|
|
298
|
+
} catch {
|
|
299
|
+
// Skip malformed lines
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
_handleMessage(serverName, msg) {
|
|
305
|
+
const entry = this.servers.get(serverName);
|
|
306
|
+
if (!entry) return;
|
|
307
|
+
|
|
308
|
+
// Response to a request
|
|
309
|
+
if (msg.id !== undefined && entry._pending.has(msg.id)) {
|
|
310
|
+
const { resolve, reject, timeout } = entry._pending.get(msg.id);
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
entry._pending.delete(msg.id);
|
|
313
|
+
|
|
314
|
+
if (msg.error) {
|
|
315
|
+
reject(new Error(msg.error.message || "Unknown error"));
|
|
316
|
+
} else {
|
|
317
|
+
resolve(msg.result);
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Server notification
|
|
323
|
+
if (msg.method) {
|
|
324
|
+
this.emit("notification", {
|
|
325
|
+
server: serverName,
|
|
326
|
+
method: msg.method,
|
|
327
|
+
params: msg.params,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* MCP server configuration storage.
|
|
335
|
+
* Persists server configs in the database.
|
|
336
|
+
*/
|
|
337
|
+
export class MCPServerConfig {
|
|
338
|
+
constructor(db) {
|
|
339
|
+
this.db = db;
|
|
340
|
+
this._ensureTable();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_ensureTable() {
|
|
344
|
+
this.db.exec(`
|
|
345
|
+
CREATE TABLE IF NOT EXISTS mcp_servers (
|
|
346
|
+
name TEXT PRIMARY KEY,
|
|
347
|
+
command TEXT NOT NULL,
|
|
348
|
+
args TEXT DEFAULT '[]',
|
|
349
|
+
env TEXT DEFAULT '{}',
|
|
350
|
+
auto_connect INTEGER DEFAULT 0,
|
|
351
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
352
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
353
|
+
)
|
|
354
|
+
`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
add(name, config) {
|
|
358
|
+
this.db
|
|
359
|
+
.prepare(
|
|
360
|
+
"INSERT OR REPLACE INTO mcp_servers (name, command, args, env, auto_connect, updated_at) VALUES (?, ?, ?, ?, ?, datetime('now'))",
|
|
361
|
+
)
|
|
362
|
+
.run(
|
|
363
|
+
name,
|
|
364
|
+
config.command,
|
|
365
|
+
JSON.stringify(config.args || []),
|
|
366
|
+
JSON.stringify(config.env || {}),
|
|
367
|
+
config.autoConnect ? 1 : 0,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
remove(name) {
|
|
372
|
+
const result = this.db
|
|
373
|
+
.prepare("DELETE FROM mcp_servers WHERE name = ?")
|
|
374
|
+
.run(name);
|
|
375
|
+
return result.changes > 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
get(name) {
|
|
379
|
+
const row = this.db
|
|
380
|
+
.prepare("SELECT * FROM mcp_servers WHERE name = ?")
|
|
381
|
+
.get(name);
|
|
382
|
+
if (!row) return null;
|
|
383
|
+
return {
|
|
384
|
+
name: row.name,
|
|
385
|
+
command: row.command,
|
|
386
|
+
args: JSON.parse(row.args || "[]"),
|
|
387
|
+
env: JSON.parse(row.env || "{}"),
|
|
388
|
+
autoConnect: row.auto_connect === 1,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
list() {
|
|
393
|
+
const rows = this.db
|
|
394
|
+
.prepare("SELECT * FROM mcp_servers ORDER BY name")
|
|
395
|
+
.all();
|
|
396
|
+
return rows.map((row) => ({
|
|
397
|
+
name: row.name,
|
|
398
|
+
command: row.command,
|
|
399
|
+
args: JSON.parse(row.args || "[]"),
|
|
400
|
+
env: JSON.parse(row.env || "{}"),
|
|
401
|
+
autoConnect: row.auto_connect === 1,
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
getAutoConnect() {
|
|
406
|
+
const rows = this.db
|
|
407
|
+
.prepare("SELECT * FROM mcp_servers WHERE auto_connect = ? ORDER BY name")
|
|
408
|
+
.all(1);
|
|
409
|
+
return rows.map((row) => ({
|
|
410
|
+
name: row.name,
|
|
411
|
+
command: row.command,
|
|
412
|
+
args: JSON.parse(row.args || "[]"),
|
|
413
|
+
env: JSON.parse(row.env || "{}"),
|
|
414
|
+
autoConnect: true,
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock LLM Provider — Phase 7 Parity Harness foundation.
|
|
3
|
+
*
|
|
4
|
+
* Produces a deterministic `chatWithTools`-shaped function from a scripted
|
|
5
|
+
* sequence of responses. Each script entry describes what the mock returns
|
|
6
|
+
* for the next LLM call, in order. This is the foundation for golden-
|
|
7
|
+
* transcript parity tests in `packages/cli/__tests__/integration/parity-*`.
|
|
8
|
+
*
|
|
9
|
+
* Design goals:
|
|
10
|
+
* - Zero network, zero provider SDK — pure in-memory dispatch
|
|
11
|
+
* - Same return shape as real `chatWithTools`: `{ message: { role, content, tool_calls? } }`
|
|
12
|
+
* - Supports tool_calls to drive multi-turn agent loops
|
|
13
|
+
* - Runs out of steps cleanly (throws a descriptive error) so tests can't
|
|
14
|
+
* silently loop forever
|
|
15
|
+
* - Optional match predicates per step let tests assert that the loop
|
|
16
|
+
* is feeding back the expected messages before the mock replies
|
|
17
|
+
*
|
|
18
|
+
* Example:
|
|
19
|
+
*
|
|
20
|
+
* const mock = createMockLLMProvider([
|
|
21
|
+
* {
|
|
22
|
+
* // First LLM call — ask for a tool invocation
|
|
23
|
+
* response: {
|
|
24
|
+
* message: {
|
|
25
|
+
* role: "assistant",
|
|
26
|
+
* content: "",
|
|
27
|
+
* tool_calls: [
|
|
28
|
+
* {
|
|
29
|
+
* id: "call_1",
|
|
30
|
+
* type: "function",
|
|
31
|
+
* function: {
|
|
32
|
+
* name: "read_file",
|
|
33
|
+
* arguments: JSON.stringify({ path: "README.md" }),
|
|
34
|
+
* },
|
|
35
|
+
* },
|
|
36
|
+
* ],
|
|
37
|
+
* },
|
|
38
|
+
* },
|
|
39
|
+
* },
|
|
40
|
+
* {
|
|
41
|
+
* // Second LLM call — produce the final assistant text
|
|
42
|
+
* expect: (messages) =>
|
|
43
|
+
* messages.some((m) => m.role === "tool" && m.name === "read_file"),
|
|
44
|
+
* response: {
|
|
45
|
+
* message: { role: "assistant", content: "Done." },
|
|
46
|
+
* },
|
|
47
|
+
* },
|
|
48
|
+
* ]);
|
|
49
|
+
*
|
|
50
|
+
* // Drop-in replacement for chatWithTools:
|
|
51
|
+
* await agentLoop(messages, { ...opts, chatFn: mock.chatFn });
|
|
52
|
+
*
|
|
53
|
+
* mock.assertDrained(); // verify the loop consumed every scripted response
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} MockScriptStep
|
|
58
|
+
* @property {(messages: Array<object>, options: object) => boolean} [expect]
|
|
59
|
+
* Optional predicate run against the messages passed to the mock. If it
|
|
60
|
+
* returns false, the mock throws — this lets tests assert that the loop
|
|
61
|
+
* is in the expected state before it receives the next scripted reply.
|
|
62
|
+
* @property {object} response
|
|
63
|
+
* The value the mock returns. Must match the `chatWithTools` shape:
|
|
64
|
+
* `{ message: { role, content, tool_calls? } }`.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a mock LLM provider backed by a scripted sequence.
|
|
69
|
+
*
|
|
70
|
+
* @param {Array<MockScriptStep>} script
|
|
71
|
+
* @returns {{ chatFn: Function, calls: Array<{messages, options}>, assertDrained: Function, remaining: () => number }}
|
|
72
|
+
*/
|
|
73
|
+
export function createMockLLMProvider(script) {
|
|
74
|
+
if (!Array.isArray(script)) {
|
|
75
|
+
throw new TypeError("Mock LLM script must be an array");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const calls = [];
|
|
79
|
+
let cursor = 0;
|
|
80
|
+
|
|
81
|
+
const chatFn = async function mockChatWithTools(messages, options) {
|
|
82
|
+
if (cursor >= script.length) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Mock LLM script exhausted after ${cursor} call(s). The agent loop made ` +
|
|
85
|
+
`an unexpected extra LLM call. Add another script step or tighten the ` +
|
|
86
|
+
`loop's stop condition.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const step = script[cursor];
|
|
91
|
+
cursor += 1;
|
|
92
|
+
|
|
93
|
+
// Snapshot-before-mutation so test assertions see exactly what the mock
|
|
94
|
+
// was called with (agentLoop mutates its messages array in place).
|
|
95
|
+
const snapshot = messages.map((m) => ({ ...m }));
|
|
96
|
+
calls.push({ messages: snapshot, options });
|
|
97
|
+
|
|
98
|
+
if (typeof step.expect === "function") {
|
|
99
|
+
const ok = step.expect(snapshot, options);
|
|
100
|
+
if (!ok) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Mock LLM script step ${cursor - 1} expectation failed. Messages ` +
|
|
103
|
+
`passed to the mock did not match the expected predicate. Received ` +
|
|
104
|
+
`${snapshot.length} messages; last role = "${snapshot[snapshot.length - 1]?.role}".`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!step.response || !step.response.message) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Mock LLM script step ${cursor - 1} is missing response.message. ` +
|
|
112
|
+
`Every step must return a chatWithTools-shaped { message: {...} }.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Deep clone the response so tests can safely reuse script objects across
|
|
117
|
+
// multiple runs without the loop mutating their contents.
|
|
118
|
+
return {
|
|
119
|
+
message: JSON.parse(JSON.stringify(step.response.message)),
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
chatFn,
|
|
125
|
+
calls,
|
|
126
|
+
remaining: () => script.length - cursor,
|
|
127
|
+
assertDrained() {
|
|
128
|
+
if (cursor !== script.length) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Mock LLM script not fully consumed: ${cursor}/${script.length} steps called. ` +
|
|
131
|
+
`The agent loop stopped before reaching the end of the script.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Convenience builder for a single tool_call assistant message.
|
|
140
|
+
* @param {string} toolName
|
|
141
|
+
* @param {object} args
|
|
142
|
+
* @param {string} [callId]
|
|
143
|
+
*/
|
|
144
|
+
export function mockToolCallMessage(toolName, args, callId = "call_1") {
|
|
145
|
+
return {
|
|
146
|
+
role: "assistant",
|
|
147
|
+
content: "",
|
|
148
|
+
tool_calls: [
|
|
149
|
+
{
|
|
150
|
+
id: callId,
|
|
151
|
+
type: "function",
|
|
152
|
+
function: {
|
|
153
|
+
name: toolName,
|
|
154
|
+
arguments: JSON.stringify(args),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convenience builder for a final assistant text message.
|
|
163
|
+
* @param {string} text
|
|
164
|
+
*/
|
|
165
|
+
export function mockTextMessage(text) {
|
|
166
|
+
return { role: "assistant", content: text };
|
|
167
|
+
}
|