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.
Files changed (89) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +1 -0
  4. package/src/assets/web-panel/assets/Analytics-sBrYoc3A.js +3 -0
  5. package/src/assets/web-panel/assets/AppLayout-BhJ3YFWt.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-Cr2lWhF-.css +1 -0
  7. package/src/assets/web-panel/assets/Backup-D68fenbD.js +1 -0
  8. package/src/assets/web-panel/assets/Backup-fZqtfC1m.css +1 -0
  9. package/src/assets/web-panel/assets/{Chat-DXtvKoM0.js → Chat-DaxTP3x8.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-BJ4ODHOy.js → Cron-CNs03iHJ.js} +2 -2
  11. package/src/assets/web-panel/assets/{Dashboard-BZd4wDPQ.js → Dashboard-CjlX4CrX.js} +2 -2
  12. package/src/assets/web-panel/assets/Git-CCMVr3Y8.js +2 -0
  13. package/src/assets/web-panel/assets/Git-DGcuBXST.css +1 -0
  14. package/src/assets/web-panel/assets/{Logs-CSeKZEG_.js → Logs-BY6A0UNG.js} +2 -2
  15. package/src/assets/web-panel/assets/{McpTools-BYQAK11r.js → McpTools-CrBVYlg6.js} +2 -2
  16. package/src/assets/web-panel/assets/{Memory-gkUAPyuZ.js → Memory-CWx3SpUt.js} +2 -2
  17. package/src/assets/web-panel/assets/{Notes-bjNrQgAo.js → Notes-1LcGD49x.js} +2 -2
  18. package/src/assets/web-panel/assets/Organization-DdOOM4ic.css +1 -0
  19. package/src/assets/web-panel/assets/Organization-Dx2DhbkM.js +4 -0
  20. package/src/assets/web-panel/assets/P2P-B16fjqfJ.js +2 -0
  21. package/src/assets/web-panel/assets/P2P-OEzOeMZX.css +1 -0
  22. package/src/assets/web-panel/assets/Permissions-BQbC9FzG.js +4 -0
  23. package/src/assets/web-panel/assets/Permissions-C9WlkGl-.css +1 -0
  24. package/src/assets/web-panel/assets/Projects-CjhZbNYm.js +2 -0
  25. package/src/assets/web-panel/assets/Projects-DxKelI5h.css +1 -0
  26. package/src/assets/web-panel/assets/Providers-BEakqcO5.css +1 -0
  27. package/src/assets/web-panel/assets/Providers-ivOAQtHM.js +2 -0
  28. package/src/assets/web-panel/assets/RssFeed-BlFC20eg.css +1 -0
  29. package/src/assets/web-panel/assets/RssFeed-BrsErdrU.js +3 -0
  30. package/src/assets/web-panel/assets/Security-DnEvJU5h.js +4 -0
  31. package/src/assets/web-panel/assets/Security-Dwxw7rfP.css +1 -0
  32. package/src/assets/web-panel/assets/{Services-CS0oMdxh.js → Services-7jQywNbl.js} +2 -2
  33. package/src/assets/web-panel/assets/Skills-BCvgBkD3.js +1 -0
  34. package/src/assets/web-panel/assets/{Tasks-qULws8pc.js → Tasks-CmJBC1cf.js} +1 -1
  35. package/src/assets/web-panel/assets/Templates-DOY_oZnm.css +1 -0
  36. package/src/assets/web-panel/assets/Templates-RXT8-DNk.js +1 -0
  37. package/src/assets/web-panel/assets/Wallet-3iYASEx_.js +4 -0
  38. package/src/assets/web-panel/assets/Wallet-DnIumafl.css +1 -0
  39. package/src/assets/web-panel/assets/WebAuthn-CNPl2VQR.css +1 -0
  40. package/src/assets/web-panel/assets/WebAuthn-s3Hzd9db.js +5 -0
  41. package/src/assets/web-panel/assets/{antd-CJSBocer.js → antd-gZyc63Qr.js} +114 -114
  42. package/src/assets/web-panel/assets/chat-BmwHBi9M.js +1 -0
  43. package/src/assets/web-panel/assets/index-DrmEk9S3.js +2 -0
  44. package/src/assets/web-panel/assets/{markdown-Bo5cVN4u.js → markdown-Bv7nG63L.js} +1 -1
  45. package/src/assets/web-panel/assets/ws-CU7Gvoom.js +1 -0
  46. package/src/assets/web-panel/index.html +2 -2
  47. package/src/commands/doctor.js +33 -151
  48. package/src/commands/mcp.js +1 -1
  49. package/src/commands/plugin.js +1 -1
  50. package/src/commands/session.js +106 -7
  51. package/src/commands/status.js +39 -69
  52. package/src/gateways/ws/session-protocol.js +1 -1
  53. package/src/gateways/ws/ws-agent-handler.js +484 -0
  54. package/src/gateways/ws/ws-server.js +758 -4
  55. package/src/gateways/ws/ws-session-gateway.js +1432 -1
  56. package/src/harness/mcp-client.js +417 -0
  57. package/src/harness/mock-llm-provider.js +167 -0
  58. package/src/harness/plugin-manager.js +434 -0
  59. package/src/lib/agent-core.js +25 -1902
  60. package/src/lib/hashline.js +208 -0
  61. package/src/lib/jsonl-session-store.js +11 -0
  62. package/src/lib/mcp-client.js +14 -412
  63. package/src/lib/plugin-manager.js +29 -428
  64. package/src/lib/prompt-compressor.js +11 -0
  65. package/src/lib/session-hooks.js +61 -0
  66. package/src/lib/skill-loader.js +4 -0
  67. package/src/lib/skill-mcp.js +190 -0
  68. package/src/lib/workflow-state-reader.js +94 -0
  69. package/src/lib/ws-agent-handler.js +8 -472
  70. package/src/lib/ws-server.js +12 -756
  71. package/src/lib/ws-session-manager.js +8 -1417
  72. package/src/repl/agent-repl.js +27 -3
  73. package/src/runtime/agent-core.js +1760 -0
  74. package/src/runtime/agent-runtime.js +3 -1
  75. package/src/runtime/coding-agent-contract-shared.cjs +496 -0
  76. package/src/runtime/coding-agent-contract.js +49 -229
  77. package/src/runtime/coding-agent-policy.cjs +54 -5
  78. package/src/runtime/diagnostics.js +317 -0
  79. package/src/runtime/index.js +3 -0
  80. package/src/tools/index.js +3 -0
  81. package/src/tools/legacy-agent-tools.js +5 -0
  82. package/src/assets/web-panel/assets/AppLayout-B_tkw3Pn.js +0 -1
  83. package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +0 -1
  84. package/src/assets/web-panel/assets/Providers-Brm-S_hS.css +0 -1
  85. package/src/assets/web-panel/assets/Providers-Dbf57Tbv.js +0 -1
  86. package/src/assets/web-panel/assets/Skills-B2fgruv8.js +0 -1
  87. package/src/assets/web-panel/assets/chat-DnH09sSR.js +0 -1
  88. package/src/assets/web-panel/assets/index-IK-oro0g.js +0 -2
  89. 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,
@@ -1,413 +1,15 @@
1
1
  /**
2
- * Lightweight MCP (Model Context Protocol) client.
3
- * Implements JSON-RPC 2.0 over stdio transport without external SDK dependency.
4
- */
5
-
6
- import { spawn } from "child_process";
7
- import { EventEmitter } from "events";
8
-
9
- /**
10
- * MCP Server connection states.
11
- */
12
- export const ServerState = {
13
- DISCONNECTED: "disconnected",
14
- CONNECTING: "connecting",
15
- CONNECTED: "connected",
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";