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
package/src/lib/ws-server.js
CHANGED
|
@@ -1,760 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @deprecated — canonical implementation lives in
|
|
3
|
+
* `../gateways/ws/ws-server.js` as of the CLI Runtime Convergence
|
|
4
|
+
* roadmap (Phase 6a, 2026-04-09). This file is retained as a
|
|
5
|
+
* re-export shim for backwards compatibility and will be removed
|
|
6
|
+
* once all external consumers have migrated.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
8
|
+
* Please import `ChainlessChainWSServer` and `tokenizeCommand`
|
|
9
|
+
* directly from `packages/cli/src/gateways/ws/ws-server.js`
|
|
10
|
+
* in new code.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import { WebSocketServer } from "ws";
|
|
14
|
-
import { createTaskRecord } from "../runtime/contracts/task-record.js";
|
|
15
|
-
import {
|
|
16
|
-
RUNTIME_EVENTS,
|
|
17
|
-
createRuntimeEvent,
|
|
18
|
-
} from "../runtime/runtime-events.js";
|
|
19
|
-
import { createWsMessageDispatcher } from "../gateways/ws/message-dispatcher.js";
|
|
20
|
-
import {
|
|
21
|
-
handleTaskDetail,
|
|
22
|
-
handleTaskHistory,
|
|
23
|
-
} from "../gateways/ws/task-protocol.js";
|
|
24
|
-
import {
|
|
25
|
-
handleSessionCreate,
|
|
26
|
-
handleSessionResume,
|
|
27
|
-
handleSessionMessage,
|
|
28
|
-
handleSessionPolicyUpdate,
|
|
29
|
-
handleSessionList,
|
|
30
|
-
handleSessionClose,
|
|
31
|
-
handleSessionInterrupt,
|
|
32
|
-
handleSessionAnswer,
|
|
33
|
-
handleHostToolResult,
|
|
34
|
-
handleSubAgentList,
|
|
35
|
-
handleSubAgentGet,
|
|
36
|
-
handleReviewEnter,
|
|
37
|
-
handleReviewSubmit,
|
|
38
|
-
handleReviewResolve,
|
|
39
|
-
handleReviewStatus,
|
|
40
|
-
handlePatchPropose,
|
|
41
|
-
handlePatchApply,
|
|
42
|
-
handlePatchReject,
|
|
43
|
-
handlePatchSummary,
|
|
44
|
-
handleTaskGraphCreate,
|
|
45
|
-
handleTaskGraphAddNode,
|
|
46
|
-
handleTaskGraphUpdateNode,
|
|
47
|
-
handleTaskGraphAdvance,
|
|
48
|
-
handleTaskGraphState,
|
|
49
|
-
} from "../gateways/ws/session-protocol.js";
|
|
50
|
-
import {
|
|
51
|
-
handleSlashCommand,
|
|
52
|
-
handleOrchestrate,
|
|
53
|
-
} from "../gateways/ws/action-protocol.js";
|
|
54
|
-
import {
|
|
55
|
-
handleWorktreeDiff,
|
|
56
|
-
handleWorktreeMerge,
|
|
57
|
-
handleWorktreeMergePreview,
|
|
58
|
-
handleWorktreeAutomationApply,
|
|
59
|
-
handleWorktreeList,
|
|
60
|
-
handleCompressionStats,
|
|
61
|
-
} from "../gateways/ws/worktree-protocol.js";
|
|
62
|
-
|
|
63
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
64
|
-
const __dirname = dirname(__filename);
|
|
65
|
-
|
|
66
|
-
/** Absolute path to the CLI entry point */
|
|
67
|
-
const BIN_PATH = join(__dirname, "..", "..", "bin", "chainlesschain.js");
|
|
68
|
-
|
|
69
|
-
/** Commands that must not be executed via WebSocket */
|
|
70
|
-
const BLOCKED_COMMANDS = new Set(["serve", "chat", "agent", "setup"]);
|
|
71
|
-
|
|
72
|
-
/** Heartbeat interval (ms) */
|
|
73
|
-
const HEARTBEAT_INTERVAL = 30_000;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Tokenize a command string into an array of arguments.
|
|
77
|
-
* Handles double-quoted and single-quoted strings. Does NOT invoke a shell.
|
|
78
|
-
*/
|
|
79
|
-
export function tokenizeCommand(input) {
|
|
80
|
-
const args = [];
|
|
81
|
-
let current = "";
|
|
82
|
-
let inDouble = false;
|
|
83
|
-
let inSingle = false;
|
|
84
|
-
let escape = false;
|
|
85
|
-
|
|
86
|
-
for (const ch of input) {
|
|
87
|
-
if (escape) {
|
|
88
|
-
current += ch;
|
|
89
|
-
escape = false;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (ch === "\\" && inDouble) {
|
|
93
|
-
escape = true;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (ch === '"' && !inSingle) {
|
|
97
|
-
inDouble = !inDouble;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (ch === "'" && !inDouble) {
|
|
101
|
-
inSingle = !inSingle;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if ((ch === " " || ch === "\t") && !inDouble && !inSingle) {
|
|
105
|
-
if (current.length > 0) {
|
|
106
|
-
args.push(current);
|
|
107
|
-
current = "";
|
|
108
|
-
}
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
current += ch;
|
|
112
|
-
}
|
|
113
|
-
if (current.length > 0) {
|
|
114
|
-
args.push(current);
|
|
115
|
-
}
|
|
116
|
-
return args;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export class ChainlessChainWSServer extends EventEmitter {
|
|
120
|
-
/**
|
|
121
|
-
* @param {object} options
|
|
122
|
-
* @param {number} [options.port=18800]
|
|
123
|
-
* @param {string} [options.host="127.0.0.1"]
|
|
124
|
-
* @param {string} [options.token] - If set, clients must authenticate first
|
|
125
|
-
* @param {number} [options.maxConnections=10]
|
|
126
|
-
* @param {number} [options.timeout=30000] - Command execution timeout (ms)
|
|
127
|
-
*/
|
|
128
|
-
constructor(options = {}) {
|
|
129
|
-
super();
|
|
130
|
-
this.port = options.port || 18800;
|
|
131
|
-
this.host = options.host || "127.0.0.1";
|
|
132
|
-
this.token = options.token || null;
|
|
133
|
-
this.maxConnections = options.maxConnections || 10;
|
|
134
|
-
this.timeout = options.timeout || 30000;
|
|
135
|
-
|
|
136
|
-
/** @type {WebSocketServer|null} */
|
|
137
|
-
this.wss = null;
|
|
138
|
-
|
|
139
|
-
/** Connected clients: clientId → { ws, authenticated, connectedAt } */
|
|
140
|
-
this.clients = new Map();
|
|
141
|
-
|
|
142
|
-
/** Running child processes: requestId → ChildProcess */
|
|
143
|
-
this.processes = new Map();
|
|
144
|
-
|
|
145
|
-
/** Session manager for stateful agent/chat sessions */
|
|
146
|
-
this.sessionManager = options.sessionManager || null;
|
|
147
|
-
|
|
148
|
-
/** Session handlers: sessionId → WSAgentHandler | WSChatHandler */
|
|
149
|
-
this.sessionHandlers = new Map();
|
|
150
|
-
this._dispatcher = createWsMessageDispatcher(this);
|
|
151
|
-
|
|
152
|
-
this._heartbeatTimer = null;
|
|
153
|
-
this._clientCounter = 0;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Start the WebSocket server */
|
|
157
|
-
start() {
|
|
158
|
-
return new Promise((resolve, reject) => {
|
|
159
|
-
this.wss = new WebSocketServer({
|
|
160
|
-
port: this.port,
|
|
161
|
-
host: this.host,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
this.wss.on("listening", () => {
|
|
165
|
-
this._startHeartbeat();
|
|
166
|
-
this.emit("listening", { port: this.port, host: this.host });
|
|
167
|
-
resolve();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
this.wss.on("error", (err) => {
|
|
171
|
-
this.emit("error", err);
|
|
172
|
-
reject(err);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
this.wss.on("connection", (ws, req) => this._handleConnection(ws, req));
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Stop the server and clean up */
|
|
180
|
-
async stop() {
|
|
181
|
-
if (this._heartbeatTimer) {
|
|
182
|
-
clearInterval(this._heartbeatTimer);
|
|
183
|
-
this._heartbeatTimer = null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Close all session handlers
|
|
187
|
-
for (const [sessionId, handler] of this.sessionHandlers) {
|
|
188
|
-
if (handler && handler.destroy) {
|
|
189
|
-
handler.destroy();
|
|
190
|
-
}
|
|
191
|
-
if (this.sessionManager) {
|
|
192
|
-
try {
|
|
193
|
-
this.sessionManager.closeSession(sessionId);
|
|
194
|
-
} catch (_err) {
|
|
195
|
-
// Non-critical
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
this.sessionHandlers.clear();
|
|
200
|
-
|
|
201
|
-
// Kill all running child processes
|
|
202
|
-
for (const [id, child] of this.processes) {
|
|
203
|
-
try {
|
|
204
|
-
child.kill("SIGTERM");
|
|
205
|
-
} catch (_err) {
|
|
206
|
-
// Process may have already exited
|
|
207
|
-
}
|
|
208
|
-
this.processes.delete(id);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Close all client connections
|
|
212
|
-
for (const [, client] of this.clients) {
|
|
213
|
-
try {
|
|
214
|
-
client.ws.close(1001, "Server shutting down");
|
|
215
|
-
} catch (_err) {
|
|
216
|
-
// Connection may already be closed
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
this.clients.clear();
|
|
220
|
-
|
|
221
|
-
// Close the server
|
|
222
|
-
if (this.wss) {
|
|
223
|
-
await new Promise((resolve) => {
|
|
224
|
-
this.wss.close(() => resolve());
|
|
225
|
-
});
|
|
226
|
-
this.wss = null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
this.emit("stopped");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** @private */
|
|
233
|
-
_handleConnection(ws, req) {
|
|
234
|
-
if (this.clients.size >= this.maxConnections) {
|
|
235
|
-
ws.close(1013, "Max connections reached");
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const clientId = `client-${++this._clientCounter}`;
|
|
240
|
-
const clientIp =
|
|
241
|
-
req.socket.remoteAddress || req.headers["x-forwarded-for"] || "unknown";
|
|
242
|
-
|
|
243
|
-
this.clients.set(clientId, {
|
|
244
|
-
ws,
|
|
245
|
-
authenticated: !this.token, // If no token required, auto-authenticated
|
|
246
|
-
connectedAt: Date.now(),
|
|
247
|
-
ip: clientIp,
|
|
248
|
-
alive: true,
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
this.emit("connection", { clientId, ip: clientIp });
|
|
252
|
-
|
|
253
|
-
ws.on("message", (data) => {
|
|
254
|
-
try {
|
|
255
|
-
const message = JSON.parse(data.toString("utf8"));
|
|
256
|
-
this._handleMessage(clientId, ws, message);
|
|
257
|
-
} catch (_err) {
|
|
258
|
-
this._send(ws, {
|
|
259
|
-
type: "error",
|
|
260
|
-
code: "INVALID_JSON",
|
|
261
|
-
message: "Failed to parse message as JSON",
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
ws.on("close", () => {
|
|
267
|
-
this.clients.delete(clientId);
|
|
268
|
-
this.emit("disconnection", { clientId });
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
ws.on("pong", () => {
|
|
272
|
-
const client = this.clients.get(clientId);
|
|
273
|
-
if (client) client.alive = true;
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** @private */
|
|
278
|
-
async _handleMessage(clientId, ws, message) {
|
|
279
|
-
return this._dispatcher.dispatch(clientId, ws, message);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Handle an orchestrate message — runs ChainlessChain orchestration with
|
|
284
|
-
* real-time progress pushed back over this WebSocket connection.
|
|
285
|
-
*
|
|
286
|
-
* Message format:
|
|
287
|
-
* { type: "orchestrate", id: "req-1", task: "Fix bug X",
|
|
288
|
-
* cwd: "/path", agents: 3, ci: "npm test", notify: false }
|
|
289
|
-
*
|
|
290
|
-
* Events emitted back:
|
|
291
|
-
* { type: "orchestrate:event", event: "start|agent:output|ci:pass|ci:fail|task:status", ... }
|
|
292
|
-
* { type: "orchestrate:done", id, taskId, status }
|
|
293
|
-
* { type: "error", code: "ORCHESTRATE_FAILED", ... }
|
|
294
|
-
*/
|
|
295
|
-
async _handleOrchestrate(id, ws, message) {
|
|
296
|
-
return handleOrchestrate(this, id, ws, message);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** @private – list background tasks */
|
|
300
|
-
async _handleTasksList(id, ws) {
|
|
301
|
-
try {
|
|
302
|
-
await this._ensureTaskManager();
|
|
303
|
-
const tasks = this._taskManager.list();
|
|
304
|
-
this._send(ws, { id, type: "tasks-list", tasks });
|
|
305
|
-
} catch (err) {
|
|
306
|
-
this._send(ws, { id, type: "tasks-list", tasks: [] });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** @private — subscribe to task completion events and broadcast to all clients */
|
|
311
|
-
_subscribeTaskNotifications() {
|
|
312
|
-
if (!this._taskManager || this._taskNotificationsSubscribed) return;
|
|
313
|
-
this._taskNotificationsSubscribed = true;
|
|
314
|
-
|
|
315
|
-
this._taskManager.on("task:complete", (task) => {
|
|
316
|
-
const record = createTaskRecord(task, {
|
|
317
|
-
source: "background-task-manager",
|
|
318
|
-
});
|
|
319
|
-
this.emit(
|
|
320
|
-
RUNTIME_EVENTS.TASK_NOTIFICATION,
|
|
321
|
-
createRuntimeEvent(
|
|
322
|
-
RUNTIME_EVENTS.TASK_NOTIFICATION,
|
|
323
|
-
{ task: record },
|
|
324
|
-
{ kind: "server" },
|
|
325
|
-
),
|
|
326
|
-
);
|
|
327
|
-
this._broadcast({
|
|
328
|
-
type: "task:notification",
|
|
329
|
-
task: record,
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** @private – stop a background task */
|
|
335
|
-
async _handleTasksStop(id, ws, message) {
|
|
336
|
-
try {
|
|
337
|
-
await this._ensureTaskManager();
|
|
338
|
-
|
|
339
|
-
if (this._taskManager && message.taskId) {
|
|
340
|
-
this._taskManager.stop(message.taskId);
|
|
341
|
-
this._send(ws, { id, type: "tasks-stopped", taskId: message.taskId });
|
|
342
|
-
} else {
|
|
343
|
-
this._send(ws, {
|
|
344
|
-
id,
|
|
345
|
-
type: "error",
|
|
346
|
-
code: "NO_TASK",
|
|
347
|
-
message: "taskId required or no task manager",
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
} catch (err) {
|
|
351
|
-
this._send(ws, {
|
|
352
|
-
id,
|
|
353
|
-
type: "error",
|
|
354
|
-
code: "TASKS_STOP_FAILED",
|
|
355
|
-
message: err.message,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/** @private */
|
|
361
|
-
async _handleTaskDetail(id, ws, message) {
|
|
362
|
-
return handleTaskDetail(this, id, ws, message);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** @private */
|
|
366
|
-
async _handleTaskHistory(id, ws, message) {
|
|
367
|
-
return handleTaskHistory(this, id, ws, message);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** @private — diff preview for agent worktree branch */
|
|
371
|
-
async _handleWorktreeDiff(id, ws, message) {
|
|
372
|
-
return handleWorktreeDiff(this, id, ws, message);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** @private — one-click merge of agent worktree branch */
|
|
376
|
-
async _handleWorktreeMerge(id, ws, message) {
|
|
377
|
-
return handleWorktreeMerge(this, id, ws, message);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** @private - dry-run merge preview for an agent worktree branch */
|
|
381
|
-
async _handleWorktreeMergePreview(id, ws, message) {
|
|
382
|
-
return handleWorktreeMergePreview(this, id, ws, message);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/** @private - apply a safe automation candidate inside an agent worktree */
|
|
386
|
-
async _handleWorktreeAutomationApply(id, ws, message) {
|
|
387
|
-
return handleWorktreeAutomationApply(this, id, ws, message);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/** @private - list agent worktrees */
|
|
391
|
-
async _handleWorktreeList(id, ws) {
|
|
392
|
-
return handleWorktreeList(this, id, ws);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** @private */
|
|
396
|
-
async _handleCompressionStats(id, ws, message) {
|
|
397
|
-
return handleCompressionStats(this, id, ws, message);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/** @private */
|
|
401
|
-
_handleAuth(clientId, ws, message) {
|
|
402
|
-
const { id, token } = message;
|
|
403
|
-
const success = token === this.token;
|
|
404
|
-
const client = this.clients.get(clientId);
|
|
405
|
-
|
|
406
|
-
if (success && client) {
|
|
407
|
-
client.authenticated = true;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
this._send(ws, {
|
|
411
|
-
id,
|
|
412
|
-
type: "auth-result",
|
|
413
|
-
success,
|
|
414
|
-
...(success ? {} : { message: "Invalid token" }),
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
if (!success) {
|
|
418
|
-
// Disconnect after failed auth
|
|
419
|
-
setTimeout(() => ws.close(4001, "Authentication failed"), 100);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/** @private */
|
|
424
|
-
_executeCommand(id, ws, command, stream) {
|
|
425
|
-
if (!command || typeof command !== "string") {
|
|
426
|
-
this._send(ws, {
|
|
427
|
-
id,
|
|
428
|
-
type: "error",
|
|
429
|
-
code: "INVALID_COMMAND",
|
|
430
|
-
message: "Command must be a non-empty string",
|
|
431
|
-
});
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const args = tokenizeCommand(command.trim());
|
|
436
|
-
if (args.length === 0) {
|
|
437
|
-
this._send(ws, {
|
|
438
|
-
id,
|
|
439
|
-
type: "error",
|
|
440
|
-
code: "INVALID_COMMAND",
|
|
441
|
-
message: "Empty command",
|
|
442
|
-
});
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Block dangerous/interactive commands
|
|
447
|
-
const baseCmd = args[0];
|
|
448
|
-
if (BLOCKED_COMMANDS.has(baseCmd)) {
|
|
449
|
-
this._send(ws, {
|
|
450
|
-
id,
|
|
451
|
-
type: "error",
|
|
452
|
-
code: "COMMAND_BLOCKED",
|
|
453
|
-
message: `Command "${baseCmd}" cannot be executed via WebSocket (interactive or recursive)`,
|
|
454
|
-
});
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const child = spawn(process.execPath, [BIN_PATH, ...args], {
|
|
459
|
-
env: {
|
|
460
|
-
...process.env,
|
|
461
|
-
FORCE_COLOR: "0",
|
|
462
|
-
NO_SPINNER: "1",
|
|
463
|
-
},
|
|
464
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
465
|
-
windowsHide: true,
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
this.processes.set(id, child);
|
|
469
|
-
this.emit("command:start", { id, command, stream });
|
|
470
|
-
|
|
471
|
-
// Timeout handling
|
|
472
|
-
const timer = setTimeout(() => {
|
|
473
|
-
if (this.processes.has(id)) {
|
|
474
|
-
try {
|
|
475
|
-
child.kill("SIGTERM");
|
|
476
|
-
} catch (_err) {
|
|
477
|
-
// Process may have already exited
|
|
478
|
-
}
|
|
479
|
-
this.processes.delete(id);
|
|
480
|
-
this._send(ws, {
|
|
481
|
-
id,
|
|
482
|
-
type: "error",
|
|
483
|
-
code: "COMMAND_TIMEOUT",
|
|
484
|
-
message: `Command timed out after ${this.timeout}ms`,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}, this.timeout);
|
|
488
|
-
|
|
489
|
-
if (stream) {
|
|
490
|
-
// Stream mode: send chunks as they arrive
|
|
491
|
-
child.stdout.on("data", (data) => {
|
|
492
|
-
this._send(ws, {
|
|
493
|
-
id,
|
|
494
|
-
type: "stream-data",
|
|
495
|
-
channel: "stdout",
|
|
496
|
-
data: data.toString("utf8"),
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
child.stderr.on("data", (data) => {
|
|
501
|
-
this._send(ws, {
|
|
502
|
-
id,
|
|
503
|
-
type: "stream-data",
|
|
504
|
-
channel: "stderr",
|
|
505
|
-
data: data.toString("utf8"),
|
|
506
|
-
});
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
child.on("close", (exitCode) => {
|
|
510
|
-
clearTimeout(timer);
|
|
511
|
-
this.processes.delete(id);
|
|
512
|
-
this._send(ws, {
|
|
513
|
-
id,
|
|
514
|
-
type: "stream-end",
|
|
515
|
-
exitCode: exitCode ?? 1,
|
|
516
|
-
});
|
|
517
|
-
this.emit("command:end", { id, exitCode });
|
|
518
|
-
});
|
|
519
|
-
} else {
|
|
520
|
-
// Buffered mode: collect all output then send result
|
|
521
|
-
const stdoutChunks = [];
|
|
522
|
-
const stderrChunks = [];
|
|
523
|
-
|
|
524
|
-
child.stdout.on("data", (data) => stdoutChunks.push(data));
|
|
525
|
-
child.stderr.on("data", (data) => stderrChunks.push(data));
|
|
526
|
-
|
|
527
|
-
child.on("close", (exitCode) => {
|
|
528
|
-
clearTimeout(timer);
|
|
529
|
-
this.processes.delete(id);
|
|
530
|
-
|
|
531
|
-
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
532
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
533
|
-
|
|
534
|
-
this._send(ws, {
|
|
535
|
-
id,
|
|
536
|
-
type: "result",
|
|
537
|
-
success: exitCode === 0,
|
|
538
|
-
exitCode: exitCode ?? 1,
|
|
539
|
-
stdout,
|
|
540
|
-
stderr,
|
|
541
|
-
});
|
|
542
|
-
this.emit("command:end", { id, exitCode });
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
child.on("error", (err) => {
|
|
547
|
-
clearTimeout(timer);
|
|
548
|
-
this.processes.delete(id);
|
|
549
|
-
this._send(ws, {
|
|
550
|
-
id,
|
|
551
|
-
type: "error",
|
|
552
|
-
code: "SPAWN_ERROR",
|
|
553
|
-
message: err.message,
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/** @private */
|
|
559
|
-
_cancelRequest(id, ws) {
|
|
560
|
-
const child = this.processes.get(id);
|
|
561
|
-
if (child) {
|
|
562
|
-
try {
|
|
563
|
-
child.kill("SIGTERM");
|
|
564
|
-
} catch (_err) {
|
|
565
|
-
// Process may have already exited
|
|
566
|
-
}
|
|
567
|
-
this.processes.delete(id);
|
|
568
|
-
this._send(ws, {
|
|
569
|
-
id,
|
|
570
|
-
type: "result",
|
|
571
|
-
success: false,
|
|
572
|
-
exitCode: -1,
|
|
573
|
-
stdout: "",
|
|
574
|
-
stderr: "Cancelled by client",
|
|
575
|
-
});
|
|
576
|
-
} else {
|
|
577
|
-
this._send(ws, {
|
|
578
|
-
id,
|
|
579
|
-
type: "error",
|
|
580
|
-
code: "NOT_FOUND",
|
|
581
|
-
message: `No running command with id "${id}"`,
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// ─── Session handlers ─────────────────────────────────────────────
|
|
587
|
-
|
|
588
|
-
/** @private */
|
|
589
|
-
async _handleSessionCreate(id, ws, message) {
|
|
590
|
-
return handleSessionCreate(this, id, ws, message);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/** @private */
|
|
594
|
-
async _handleSessionResume(id, ws, message) {
|
|
595
|
-
return handleSessionResume(this, id, ws, message);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/** @private */
|
|
599
|
-
_handleSessionMessage(id, ws, message) {
|
|
600
|
-
return handleSessionMessage(this, id, ws, message);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/** @private */
|
|
604
|
-
_handleSessionPolicyUpdate(id, ws, message) {
|
|
605
|
-
return handleSessionPolicyUpdate(this, id, ws, message);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/** @private */
|
|
609
|
-
_handleSessionList(id, ws) {
|
|
610
|
-
return handleSessionList(this, id, ws);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/** @private */
|
|
614
|
-
_handleSessionClose(id, ws, message) {
|
|
615
|
-
return handleSessionClose(this, id, ws, message);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/** @private */
|
|
619
|
-
_handleSessionInterrupt(id, ws, message) {
|
|
620
|
-
return handleSessionInterrupt(this, id, ws, message);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/** @private */
|
|
624
|
-
_handleSlashCommand(id, ws, message) {
|
|
625
|
-
return handleSlashCommand(this, id, ws, message);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/** @private */
|
|
629
|
-
_handleSessionAnswer(id, ws, message) {
|
|
630
|
-
return handleSessionAnswer(this, id, ws, message);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
_handleHostToolResult(id, ws, message) {
|
|
634
|
-
return handleHostToolResult(this, id, ws, message);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/** @private */
|
|
638
|
-
_handleSubAgentList(id, ws, message) {
|
|
639
|
-
return handleSubAgentList(this, id, ws, message);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/** @private */
|
|
643
|
-
_handleSubAgentGet(id, ws, message) {
|
|
644
|
-
return handleSubAgentGet(this, id, ws, message);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/** @private */
|
|
648
|
-
_handleReviewEnter(id, ws, message) {
|
|
649
|
-
return handleReviewEnter(this, id, ws, message);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/** @private */
|
|
653
|
-
_handleReviewSubmit(id, ws, message) {
|
|
654
|
-
return handleReviewSubmit(this, id, ws, message);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/** @private */
|
|
658
|
-
_handleReviewResolve(id, ws, message) {
|
|
659
|
-
return handleReviewResolve(this, id, ws, message);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/** @private */
|
|
663
|
-
_handleReviewStatus(id, ws, message) {
|
|
664
|
-
return handleReviewStatus(this, id, ws, message);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/** @private */
|
|
668
|
-
_handlePatchPropose(id, ws, message) {
|
|
669
|
-
return handlePatchPropose(this, id, ws, message);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/** @private */
|
|
673
|
-
_handlePatchApply(id, ws, message) {
|
|
674
|
-
return handlePatchApply(this, id, ws, message);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/** @private */
|
|
678
|
-
_handlePatchReject(id, ws, message) {
|
|
679
|
-
return handlePatchReject(this, id, ws, message);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/** @private */
|
|
683
|
-
_handlePatchSummary(id, ws, message) {
|
|
684
|
-
return handlePatchSummary(this, id, ws, message);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/** @private */
|
|
688
|
-
_handleTaskGraphCreate(id, ws, message) {
|
|
689
|
-
return handleTaskGraphCreate(this, id, ws, message);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/** @private */
|
|
693
|
-
_handleTaskGraphAddNode(id, ws, message) {
|
|
694
|
-
return handleTaskGraphAddNode(this, id, ws, message);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/** @private */
|
|
698
|
-
_handleTaskGraphUpdateNode(id, ws, message) {
|
|
699
|
-
return handleTaskGraphUpdateNode(this, id, ws, message);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/** @private */
|
|
703
|
-
_handleTaskGraphAdvance(id, ws, message) {
|
|
704
|
-
return handleTaskGraphAdvance(this, id, ws, message);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/** @private */
|
|
708
|
-
_handleTaskGraphState(id, ws, message) {
|
|
709
|
-
return handleTaskGraphState(this, id, ws, message);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/** @private — ping/pong heartbeat to detect dead connections */
|
|
713
|
-
async _ensureTaskManager() {
|
|
714
|
-
if (this._taskManager) return this._taskManager;
|
|
715
|
-
const { BackgroundTaskManager } =
|
|
716
|
-
await import("./background-task-manager.js");
|
|
717
|
-
this._taskManager = new BackgroundTaskManager({ recoverOnStart: true });
|
|
718
|
-
this._subscribeTaskNotifications();
|
|
719
|
-
return this._taskManager;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
_startHeartbeat() {
|
|
723
|
-
this._heartbeatTimer = setInterval(() => {
|
|
724
|
-
for (const [clientId, client] of this.clients) {
|
|
725
|
-
if (!client.alive) {
|
|
726
|
-
client.ws.terminate();
|
|
727
|
-
this.clients.delete(clientId);
|
|
728
|
-
this.emit("disconnection", { clientId, reason: "heartbeat timeout" });
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
client.alive = false;
|
|
732
|
-
try {
|
|
733
|
-
client.ws.ping();
|
|
734
|
-
} catch (_err) {
|
|
735
|
-
// Connection may be closing
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}, HEARTBEAT_INTERVAL);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/** @private — safe JSON send */
|
|
742
|
-
_send(ws, data) {
|
|
743
|
-
if (ws.readyState === ws.OPEN) {
|
|
744
|
-
try {
|
|
745
|
-
ws.send(JSON.stringify(data));
|
|
746
|
-
} catch (_err) {
|
|
747
|
-
// Connection may have just closed
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/** @private — broadcast a message to all connected, authenticated clients */
|
|
753
|
-
_broadcast(data) {
|
|
754
|
-
for (const [, client] of this.clients) {
|
|
755
|
-
if (client.authenticated || !this.token) {
|
|
756
|
-
this._send(client.ws, data);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
13
|
+
export {
|
|
14
|
+
ChainlessChainWSServer,
|
|
15
|
+
tokenizeCommand,
|
|
16
|
+
} from "../gateways/ws/ws-server.js";
|