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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hashline — content-hash anchored line editing (v5.0.2.9)
|
|
3
|
+
*
|
|
4
|
+
* Inspired by oh-my-openagent's "Hashline" design: rather than referring to
|
|
5
|
+
* code by line number (brittle across concurrent edits) or by exact string
|
|
6
|
+
* match (brittle across whitespace drift), each line is tagged with a short
|
|
7
|
+
* content hash. Edits reference the hash and are rejected if the current
|
|
8
|
+
* file contents no longer match — preventing stale-line corruption.
|
|
9
|
+
*
|
|
10
|
+
* Pure functions only — zero side effects, fully testable.
|
|
11
|
+
*
|
|
12
|
+
* Tag format: `<6-char base64url>| <line content>`
|
|
13
|
+
* Empty / whitespace-only lines use `______` (6 underscores).
|
|
14
|
+
*
|
|
15
|
+
* Hash is computed over `line.trim()`, making it insensitive to leading /
|
|
16
|
+
* trailing whitespace — rebust against auto-formatters and indentation drift.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import crypto from "crypto";
|
|
20
|
+
|
|
21
|
+
const HASH_LENGTH = 6;
|
|
22
|
+
const EMPTY_HASH = "______";
|
|
23
|
+
const SEPARATOR = "| ";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compute the stable hash for a single line.
|
|
27
|
+
* Whitespace-insensitive: `.trim()` is applied before hashing.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} line
|
|
30
|
+
* @returns {string} 6-char base64url hash, or "______" for empty/whitespace
|
|
31
|
+
*/
|
|
32
|
+
export function hashLine(line) {
|
|
33
|
+
if (typeof line !== "string") return EMPTY_HASH;
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (trimmed.length === 0) return EMPTY_HASH;
|
|
36
|
+
return crypto
|
|
37
|
+
.createHash("sha256")
|
|
38
|
+
.update(trimmed, "utf8")
|
|
39
|
+
.digest("base64url")
|
|
40
|
+
.slice(0, HASH_LENGTH);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Split content into lines, preserving line-ending style for round-trips.
|
|
45
|
+
* Detects CRLF vs LF; mixed endings fall back to LF.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} content
|
|
48
|
+
* @returns {{ lines: string[], eol: "\r\n" | "\n" }}
|
|
49
|
+
*/
|
|
50
|
+
export function splitLines(content) {
|
|
51
|
+
if (typeof content !== "string") return { lines: [], eol: "\n" };
|
|
52
|
+
const hasCRLF = content.includes("\r\n");
|
|
53
|
+
const hasLF = content.includes("\n");
|
|
54
|
+
// Only treat as CRLF if no bare LFs appear outside CRLF pairs
|
|
55
|
+
if (hasCRLF) {
|
|
56
|
+
// Count bare \n (\n not preceded by \r)
|
|
57
|
+
const bareLFs = (content.match(/(^|[^\r])\n/g) || []).length;
|
|
58
|
+
const crlfs = (content.match(/\r\n/g) || []).length;
|
|
59
|
+
const eol = bareLFs === 0 || bareLFs < crlfs / 2 ? "\r\n" : "\n";
|
|
60
|
+
return { lines: content.split(/\r?\n/), eol };
|
|
61
|
+
}
|
|
62
|
+
return { lines: hasLF ? content.split("\n") : [content], eol: "\n" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Annotate content: prepend each line with `<hash>| `.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} content
|
|
69
|
+
* @returns {string} annotated content
|
|
70
|
+
*/
|
|
71
|
+
export function annotateLines(content) {
|
|
72
|
+
const { lines, eol } = splitLines(content);
|
|
73
|
+
return lines.map((line) => `${hashLine(line)}${SEPARATOR}${line}`).join(eol);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find all lines whose hash matches the anchor.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} content
|
|
80
|
+
* @param {string} anchorHash
|
|
81
|
+
* @returns {Array<{ index: number, lineNumber: number, content: string }>}
|
|
82
|
+
*/
|
|
83
|
+
export function findByHash(content, anchorHash) {
|
|
84
|
+
if (!anchorHash || typeof anchorHash !== "string") return [];
|
|
85
|
+
const { lines } = splitLines(content);
|
|
86
|
+
const matches = [];
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
if (hashLine(lines[i]) === anchorHash) {
|
|
89
|
+
matches.push({
|
|
90
|
+
index: i,
|
|
91
|
+
lineNumber: i + 1, // 1-based for human-friendly display
|
|
92
|
+
content: lines[i],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return matches;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Verify the current content of a line matches both the anchor hash and the
|
|
101
|
+
* expected trimmed content. Used as a second-layer check to defend against
|
|
102
|
+
* hash collisions.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} currentLine
|
|
105
|
+
* @param {string} anchorHash
|
|
106
|
+
* @param {string} expectedLine - Expected content (compared trimmed)
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function verifyLine(currentLine, anchorHash, expectedLine) {
|
|
110
|
+
if (hashLine(currentLine) !== anchorHash) return false;
|
|
111
|
+
if (typeof expectedLine !== "string") return true;
|
|
112
|
+
return currentLine.trim() === expectedLine.trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Replace a single line at the given anchor hash. Returns either the new
|
|
117
|
+
* content or a structured error.
|
|
118
|
+
*
|
|
119
|
+
* Error shapes (not thrown — returned so the agent loop can present them):
|
|
120
|
+
* { error: "hash_mismatch", ... } — anchor doesn't match any line
|
|
121
|
+
* { error: "ambiguous_anchor", ... } — anchor matches multiple lines
|
|
122
|
+
* { error: "content_mismatch", ... } — hash matches but expected_line differs
|
|
123
|
+
*
|
|
124
|
+
* @param {string} content - Full file content
|
|
125
|
+
* @param {object} opts
|
|
126
|
+
* @param {string} opts.anchorHash
|
|
127
|
+
* @param {string} opts.expectedLine
|
|
128
|
+
* @param {string} opts.newLine
|
|
129
|
+
* @param {number} [opts.contextLines=3] - Lines of context for error snippets
|
|
130
|
+
* @returns {{ success: true, content: string, lineNumber: number } | { success: false, error: string, [key: string]: any }}
|
|
131
|
+
*/
|
|
132
|
+
export function replaceByHash(content, opts) {
|
|
133
|
+
const { anchorHash, expectedLine, newLine, contextLines = 3 } = opts;
|
|
134
|
+
const { lines, eol } = splitLines(content);
|
|
135
|
+
const matches = findByHash(content, anchorHash);
|
|
136
|
+
|
|
137
|
+
if (matches.length === 0) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: "hash_mismatch",
|
|
141
|
+
message: `No line matches anchor hash "${anchorHash}"`,
|
|
142
|
+
hint: "Re-read the file with hashed:true to get current hashes",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (matches.length > 1) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: "ambiguous_anchor",
|
|
150
|
+
message: `Anchor hash "${anchorHash}" matches ${matches.length} lines`,
|
|
151
|
+
matches: matches.map((m) => ({
|
|
152
|
+
lineNumber: m.lineNumber,
|
|
153
|
+
content: m.content,
|
|
154
|
+
})),
|
|
155
|
+
hint: "Use edit_file with a unique old_string or refine the anchor",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const match = matches[0];
|
|
160
|
+
if (
|
|
161
|
+
typeof expectedLine === "string" &&
|
|
162
|
+
match.content.trim() !== expectedLine.trim()
|
|
163
|
+
) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: "content_mismatch",
|
|
167
|
+
message: `Line ${match.lineNumber} has hash ${anchorHash} but content differs from expected_line`,
|
|
168
|
+
current: match.content,
|
|
169
|
+
expected: expectedLine,
|
|
170
|
+
hint: "Re-read the file to see current content",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Replace — preserve leading whitespace from original line if new_line has none
|
|
175
|
+
const newLines = [...lines];
|
|
176
|
+
newLines[match.index] = newLine;
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
content: newLines.join(eol),
|
|
180
|
+
lineNumber: match.lineNumber,
|
|
181
|
+
previousContent: match.content,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Produce a small snippet of context around a given line index for error
|
|
187
|
+
* messages. Uses annotated form so the agent can retry with fresh hashes.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} content
|
|
190
|
+
* @param {number} lineIndex - 0-based
|
|
191
|
+
* @param {number} [contextLines=3]
|
|
192
|
+
* @returns {string}
|
|
193
|
+
*/
|
|
194
|
+
export function snippetAround(content, lineIndex, contextLines = 3) {
|
|
195
|
+
const { lines, eol } = splitLines(content);
|
|
196
|
+
const start = Math.max(0, lineIndex - contextLines);
|
|
197
|
+
const end = Math.min(lines.length, lineIndex + contextLines + 1);
|
|
198
|
+
const slice = lines
|
|
199
|
+
.slice(start, end)
|
|
200
|
+
.map((line, i) => `${hashLine(line)}${SEPARATOR}${line}`);
|
|
201
|
+
return slice.join(eol);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const _internals = {
|
|
205
|
+
HASH_LENGTH,
|
|
206
|
+
EMPTY_HASH,
|
|
207
|
+
SEPARATOR,
|
|
208
|
+
};
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated — canonical implementation lives in
|
|
3
|
+
* `../harness/jsonl-session-store.js` as of the CLI Runtime Convergence
|
|
4
|
+
* roadmap. This file is retained as a re-export shim for backwards
|
|
5
|
+
* compatibility and will be removed once all external consumers have
|
|
6
|
+
* migrated.
|
|
7
|
+
*
|
|
8
|
+
* Please import from `packages/cli/src/harness/jsonl-session-store.js`
|
|
9
|
+
* in new code.
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
export {
|
|
2
13
|
appendEvent,
|
|
3
14
|
startSession,
|
package/src/lib/mcp-client.js
CHANGED
|
@@ -1,413 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
ERROR: "error",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* MCP Client — manages connections to MCP servers.
|
|
21
|
-
*/
|
|
22
|
-
export class MCPClient extends EventEmitter {
|
|
23
|
-
constructor() {
|
|
24
|
-
super();
|
|
25
|
-
this.servers = new Map(); // name → { process, state, tools, resources, config }
|
|
26
|
-
this._nextId = 1;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Connect to an MCP server via stdio transport.
|
|
31
|
-
* @param {string} name - Server name
|
|
32
|
-
* @param {object} config - { command, args?, env? }
|
|
33
|
-
*/
|
|
34
|
-
async connect(name, config) {
|
|
35
|
-
if (this.servers.has(name)) {
|
|
36
|
-
throw new Error(`Server "${name}" already connected`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const entry = {
|
|
40
|
-
config,
|
|
41
|
-
state: ServerState.CONNECTING,
|
|
42
|
-
process: null,
|
|
43
|
-
tools: [],
|
|
44
|
-
resources: [],
|
|
45
|
-
prompts: [],
|
|
46
|
-
_pending: new Map(),
|
|
47
|
-
_buffer: "",
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
this.servers.set(name, entry);
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
const proc = spawn(config.command, config.args || [], {
|
|
54
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
55
|
-
env: { ...process.env, ...(config.env || {}) },
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
entry.process = proc;
|
|
59
|
-
|
|
60
|
-
proc.stdout.on("data", (data) => {
|
|
61
|
-
this._handleData(name, data.toString("utf8"));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
proc.stderr.on("data", (data) => {
|
|
65
|
-
this.emit("server-error", { name, error: data.toString("utf8") });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
proc.on("close", (code) => {
|
|
69
|
-
entry.state = ServerState.DISCONNECTED;
|
|
70
|
-
this.emit("server-disconnected", { name, code });
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
proc.on("error", (err) => {
|
|
74
|
-
entry.state = ServerState.ERROR;
|
|
75
|
-
this.emit("server-error", { name, error: err.message });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Initialize MCP protocol
|
|
79
|
-
const initResult = await this._sendRequest(name, "initialize", {
|
|
80
|
-
protocolVersion: "2024-11-05",
|
|
81
|
-
capabilities: { tools: {}, resources: {} },
|
|
82
|
-
clientInfo: { name: "chainlesschain-cli", version: "0.37.9" },
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Send initialized notification
|
|
86
|
-
this._sendNotification(name, "notifications/initialized", {});
|
|
87
|
-
|
|
88
|
-
entry.state = ServerState.CONNECTED;
|
|
89
|
-
entry.serverInfo = initResult?.serverInfo || {};
|
|
90
|
-
entry.capabilities = initResult?.capabilities || {};
|
|
91
|
-
|
|
92
|
-
// Fetch available tools
|
|
93
|
-
try {
|
|
94
|
-
const toolsResult = await this._sendRequest(name, "tools/list", {});
|
|
95
|
-
entry.tools = toolsResult?.tools || [];
|
|
96
|
-
} catch {
|
|
97
|
-
// Server may not support tools
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Fetch available resources
|
|
101
|
-
try {
|
|
102
|
-
const resourcesResult = await this._sendRequest(
|
|
103
|
-
name,
|
|
104
|
-
"resources/list",
|
|
105
|
-
{},
|
|
106
|
-
);
|
|
107
|
-
entry.resources = resourcesResult?.resources || [];
|
|
108
|
-
} catch {
|
|
109
|
-
// Server may not support resources
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
this.emit("server-connected", { name, tools: entry.tools.length });
|
|
113
|
-
return {
|
|
114
|
-
name,
|
|
115
|
-
state: entry.state,
|
|
116
|
-
tools: entry.tools,
|
|
117
|
-
resources: entry.resources,
|
|
118
|
-
serverInfo: entry.serverInfo,
|
|
119
|
-
};
|
|
120
|
-
} catch (err) {
|
|
121
|
-
entry.state = ServerState.ERROR;
|
|
122
|
-
this.servers.delete(name);
|
|
123
|
-
throw err;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Disconnect from an MCP server.
|
|
129
|
-
*/
|
|
130
|
-
async disconnect(name) {
|
|
131
|
-
const entry = this.servers.get(name);
|
|
132
|
-
if (!entry) return false;
|
|
133
|
-
|
|
134
|
-
if (entry.process) {
|
|
135
|
-
entry.process.kill();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
entry.state = ServerState.DISCONNECTED;
|
|
139
|
-
this.servers.delete(name);
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Disconnect from all servers.
|
|
145
|
-
*/
|
|
146
|
-
async disconnectAll() {
|
|
147
|
-
const names = [...this.servers.keys()];
|
|
148
|
-
for (const name of names) {
|
|
149
|
-
await this.disconnect(name);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* List all connected servers.
|
|
155
|
-
*/
|
|
156
|
-
listServers() {
|
|
157
|
-
const result = [];
|
|
158
|
-
for (const [name, entry] of this.servers) {
|
|
159
|
-
result.push({
|
|
160
|
-
name,
|
|
161
|
-
state: entry.state,
|
|
162
|
-
tools: entry.tools.length,
|
|
163
|
-
resources: entry.resources.length,
|
|
164
|
-
serverInfo: entry.serverInfo || {},
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* List tools from a specific server or all servers.
|
|
172
|
-
*/
|
|
173
|
-
listTools(serverName) {
|
|
174
|
-
if (serverName) {
|
|
175
|
-
const entry = this.servers.get(serverName);
|
|
176
|
-
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
177
|
-
return entry.tools.map((t) => ({ ...t, server: serverName }));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const allTools = [];
|
|
181
|
-
for (const [name, entry] of this.servers) {
|
|
182
|
-
for (const tool of entry.tools) {
|
|
183
|
-
allTools.push({ ...tool, server: name });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return allTools;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Call a tool on a specific server.
|
|
191
|
-
* @param {string} serverName - Server name
|
|
192
|
-
* @param {string} toolName - Tool name
|
|
193
|
-
* @param {object} args - Tool arguments
|
|
194
|
-
*/
|
|
195
|
-
async callTool(serverName, toolName, args = {}) {
|
|
196
|
-
const entry = this.servers.get(serverName);
|
|
197
|
-
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
198
|
-
if (entry.state !== ServerState.CONNECTED) {
|
|
199
|
-
throw new Error(`Server "${serverName}" is not connected`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const result = await this._sendRequest(serverName, "tools/call", {
|
|
203
|
-
name: toolName,
|
|
204
|
-
arguments: args,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
return result;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Read a resource from a server.
|
|
212
|
-
*/
|
|
213
|
-
async readResource(serverName, uri) {
|
|
214
|
-
const entry = this.servers.get(serverName);
|
|
215
|
-
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
216
|
-
|
|
217
|
-
const result = await this._sendRequest(serverName, "resources/read", {
|
|
218
|
-
uri,
|
|
219
|
-
});
|
|
220
|
-
return result;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ─── Internal JSON-RPC transport ──────────────────────────────
|
|
224
|
-
|
|
225
|
-
_sendRequest(serverName, method, params) {
|
|
226
|
-
return new Promise((resolve, reject) => {
|
|
227
|
-
const entry = this.servers.get(serverName);
|
|
228
|
-
if (!entry || !entry.process) {
|
|
229
|
-
return reject(new Error("Server not available"));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const id = this._nextId++;
|
|
233
|
-
const message = JSON.stringify({
|
|
234
|
-
jsonrpc: "2.0",
|
|
235
|
-
id,
|
|
236
|
-
method,
|
|
237
|
-
params,
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
entry._pending.set(id, { resolve, reject });
|
|
241
|
-
|
|
242
|
-
// Set timeout
|
|
243
|
-
const timeout = setTimeout(() => {
|
|
244
|
-
entry._pending.delete(id);
|
|
245
|
-
reject(new Error(`Request timeout: ${method}`));
|
|
246
|
-
}, 30000);
|
|
247
|
-
|
|
248
|
-
entry._pending.get(id).timeout = timeout;
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
entry.process.stdin.write(message + "\n");
|
|
252
|
-
} catch (err) {
|
|
253
|
-
clearTimeout(timeout);
|
|
254
|
-
entry._pending.delete(id);
|
|
255
|
-
reject(err);
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
_sendNotification(serverName, method, params) {
|
|
261
|
-
const entry = this.servers.get(serverName);
|
|
262
|
-
if (!entry || !entry.process) return;
|
|
263
|
-
|
|
264
|
-
const message = JSON.stringify({
|
|
265
|
-
jsonrpc: "2.0",
|
|
266
|
-
method,
|
|
267
|
-
params,
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
entry.process.stdin.write(message + "\n");
|
|
272
|
-
} catch {
|
|
273
|
-
// Ignore notification errors
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
_handleData(serverName, data) {
|
|
278
|
-
const entry = this.servers.get(serverName);
|
|
279
|
-
if (!entry) return;
|
|
280
|
-
|
|
281
|
-
entry._buffer += data;
|
|
282
|
-
|
|
283
|
-
// Process complete JSON lines
|
|
284
|
-
const lines = entry._buffer.split("\n");
|
|
285
|
-
entry._buffer = lines.pop() || "";
|
|
286
|
-
|
|
287
|
-
for (const line of lines) {
|
|
288
|
-
const trimmed = line.trim();
|
|
289
|
-
if (!trimmed) continue;
|
|
290
|
-
|
|
291
|
-
try {
|
|
292
|
-
const msg = JSON.parse(trimmed);
|
|
293
|
-
this._handleMessage(serverName, msg);
|
|
294
|
-
} catch {
|
|
295
|
-
// Skip malformed lines
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
_handleMessage(serverName, msg) {
|
|
301
|
-
const entry = this.servers.get(serverName);
|
|
302
|
-
if (!entry) return;
|
|
303
|
-
|
|
304
|
-
// Response to a request
|
|
305
|
-
if (msg.id !== undefined && entry._pending.has(msg.id)) {
|
|
306
|
-
const { resolve, reject, timeout } = entry._pending.get(msg.id);
|
|
307
|
-
clearTimeout(timeout);
|
|
308
|
-
entry._pending.delete(msg.id);
|
|
309
|
-
|
|
310
|
-
if (msg.error) {
|
|
311
|
-
reject(new Error(msg.error.message || "Unknown error"));
|
|
312
|
-
} else {
|
|
313
|
-
resolve(msg.result);
|
|
314
|
-
}
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Server notification
|
|
319
|
-
if (msg.method) {
|
|
320
|
-
this.emit("notification", {
|
|
321
|
-
server: serverName,
|
|
322
|
-
method: msg.method,
|
|
323
|
-
params: msg.params,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* MCP server configuration storage.
|
|
331
|
-
* Persists server configs in the database.
|
|
332
|
-
*/
|
|
333
|
-
export class MCPServerConfig {
|
|
334
|
-
constructor(db) {
|
|
335
|
-
this.db = db;
|
|
336
|
-
this._ensureTable();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
_ensureTable() {
|
|
340
|
-
this.db.exec(`
|
|
341
|
-
CREATE TABLE IF NOT EXISTS mcp_servers (
|
|
342
|
-
name TEXT PRIMARY KEY,
|
|
343
|
-
command TEXT NOT NULL,
|
|
344
|
-
args TEXT DEFAULT '[]',
|
|
345
|
-
env TEXT DEFAULT '{}',
|
|
346
|
-
auto_connect INTEGER DEFAULT 0,
|
|
347
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
348
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
349
|
-
)
|
|
350
|
-
`);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
add(name, config) {
|
|
354
|
-
this.db
|
|
355
|
-
.prepare(
|
|
356
|
-
"INSERT OR REPLACE INTO mcp_servers (name, command, args, env, auto_connect, updated_at) VALUES (?, ?, ?, ?, ?, datetime('now'))",
|
|
357
|
-
)
|
|
358
|
-
.run(
|
|
359
|
-
name,
|
|
360
|
-
config.command,
|
|
361
|
-
JSON.stringify(config.args || []),
|
|
362
|
-
JSON.stringify(config.env || {}),
|
|
363
|
-
config.autoConnect ? 1 : 0,
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
remove(name) {
|
|
368
|
-
const result = this.db
|
|
369
|
-
.prepare("DELETE FROM mcp_servers WHERE name = ?")
|
|
370
|
-
.run(name);
|
|
371
|
-
return result.changes > 0;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
get(name) {
|
|
375
|
-
const row = this.db
|
|
376
|
-
.prepare("SELECT * FROM mcp_servers WHERE name = ?")
|
|
377
|
-
.get(name);
|
|
378
|
-
if (!row) return null;
|
|
379
|
-
return {
|
|
380
|
-
name: row.name,
|
|
381
|
-
command: row.command,
|
|
382
|
-
args: JSON.parse(row.args || "[]"),
|
|
383
|
-
env: JSON.parse(row.env || "{}"),
|
|
384
|
-
autoConnect: row.auto_connect === 1,
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
list() {
|
|
389
|
-
const rows = this.db
|
|
390
|
-
.prepare("SELECT * FROM mcp_servers ORDER BY name")
|
|
391
|
-
.all();
|
|
392
|
-
return rows.map((row) => ({
|
|
393
|
-
name: row.name,
|
|
394
|
-
command: row.command,
|
|
395
|
-
args: JSON.parse(row.args || "[]"),
|
|
396
|
-
env: JSON.parse(row.env || "{}"),
|
|
397
|
-
autoConnect: row.auto_connect === 1,
|
|
398
|
-
}));
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
getAutoConnect() {
|
|
402
|
-
const rows = this.db
|
|
403
|
-
.prepare("SELECT * FROM mcp_servers WHERE auto_connect = ? ORDER BY name")
|
|
404
|
-
.all(1);
|
|
405
|
-
return rows.map((row) => ({
|
|
406
|
-
name: row.name,
|
|
407
|
-
command: row.command,
|
|
408
|
-
args: JSON.parse(row.args || "[]"),
|
|
409
|
-
env: JSON.parse(row.env || "{}"),
|
|
410
|
-
autoConnect: true,
|
|
411
|
-
}));
|
|
412
|
-
}
|
|
413
|
-
}
|
|
2
|
+
* @deprecated — canonical implementation lives in `../harness/mcp-client.js`
|
|
3
|
+
* as of the CLI Runtime Convergence roadmap (Phase 3, 2026-04-09).
|
|
4
|
+
* This file is retained as a re-export shim for backwards compatibility
|
|
5
|
+
* and will be removed once all external consumers have migrated.
|
|
6
|
+
*
|
|
7
|
+
* Please import `MCPClient`, `MCPServerConfig`, and `ServerState` directly
|
|
8
|
+
* from `packages/cli/src/harness/mcp-client.js` in new code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
ServerState,
|
|
13
|
+
MCPClient,
|
|
14
|
+
MCPServerConfig,
|
|
15
|
+
} from "../harness/mcp-client.js";
|