chainlesschain 0.45.70 → 0.45.75

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-2RCrdXxl.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-D9pBLPC3.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-B2nB8o_F.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-DanoHPSI.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-CLlblJcG.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-DWBA4-cl.js +1 -0
  43. package/src/assets/web-panel/assets/index-CyGtHm63.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
@@ -1,760 +1,16 @@
1
1
  /**
2
- * ChainlessChain WebSocket Server
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
- * Exposes CLI commands over WebSocket for remote access by IDE plugins,
5
- * web frontends, automation scripts, etc. Commands are executed by spawning
6
- * child processes — all 60+ CLI commands are available immediately.
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
- import { EventEmitter } from "node:events";
10
- import { spawn } from "node:child_process";
11
- import { fileURLToPath } from "node:url";
12
- import { dirname, join } from "node:path";
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";