cli-wechat-bridge 1.0.5

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 (54) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +637 -0
  3. package/bin/_run-entry.mjs +35 -0
  4. package/bin/wechat-bridge-claude.mjs +5 -0
  5. package/bin/wechat-bridge-codex.mjs +5 -0
  6. package/bin/wechat-bridge-opencode.mjs +5 -0
  7. package/bin/wechat-bridge-shell.mjs +5 -0
  8. package/bin/wechat-bridge.mjs +5 -0
  9. package/bin/wechat-check-update.mjs +5 -0
  10. package/bin/wechat-claude-start.mjs +5 -0
  11. package/bin/wechat-claude.mjs +5 -0
  12. package/bin/wechat-codex-start.mjs +5 -0
  13. package/bin/wechat-codex.mjs +5 -0
  14. package/bin/wechat-daemon.mjs +5 -0
  15. package/bin/wechat-opencode-start.mjs +5 -0
  16. package/bin/wechat-opencode.mjs +5 -0
  17. package/bin/wechat-setup.mjs +5 -0
  18. package/dist/bridge/bridge-adapter-common.js +95 -0
  19. package/dist/bridge/bridge-adapters.claude.js +829 -0
  20. package/dist/bridge/bridge-adapters.codex.js +2228 -0
  21. package/dist/bridge/bridge-adapters.core.js +717 -0
  22. package/dist/bridge/bridge-adapters.js +26 -0
  23. package/dist/bridge/bridge-adapters.opencode.js +2129 -0
  24. package/dist/bridge/bridge-adapters.shared.js +1005 -0
  25. package/dist/bridge/bridge-adapters.shell.js +363 -0
  26. package/dist/bridge/bridge-controller.js +48 -0
  27. package/dist/bridge/bridge-final-reply.js +46 -0
  28. package/dist/bridge/bridge-process-reaper.js +348 -0
  29. package/dist/bridge/bridge-state.js +362 -0
  30. package/dist/bridge/bridge-types.js +1 -0
  31. package/dist/bridge/bridge-utils.js +1240 -0
  32. package/dist/bridge/claude-hook.js +82 -0
  33. package/dist/bridge/claude-hooks.js +267 -0
  34. package/dist/bridge/wechat-bridge.js +1026 -0
  35. package/dist/commands/check-update.js +30 -0
  36. package/dist/companion/codex-panel-link.js +72 -0
  37. package/dist/companion/codex-panel.js +179 -0
  38. package/dist/companion/codex-remote-client.js +124 -0
  39. package/dist/companion/local-companion-link.js +240 -0
  40. package/dist/companion/local-companion-start.js +420 -0
  41. package/dist/companion/local-companion.js +424 -0
  42. package/dist/daemon/daemon-link.js +175 -0
  43. package/dist/daemon/wechat-daemon.js +1202 -0
  44. package/dist/media/media-types.js +1 -0
  45. package/dist/runtime/create-runtime-host.js +12 -0
  46. package/dist/runtime/legacy-adapter-runtime.js +46 -0
  47. package/dist/runtime/runtime-types.js +5 -0
  48. package/dist/utils/version-checker.js +161 -0
  49. package/dist/wechat/channel-config.js +196 -0
  50. package/dist/wechat/setup.js +283 -0
  51. package/dist/wechat/standalone-bot.js +355 -0
  52. package/dist/wechat/wechat-channel.js +492 -0
  53. package/dist/wechat/wechat-transport.js +1213 -0
  54. package/package.json +101 -0
@@ -0,0 +1,717 @@
1
+ import net from "node:net";
2
+ import { spawn as spawnPty } from "node-pty";
3
+ import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, sendLocalCompanionMessage, updateLocalCompanionOccupancy, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
4
+ import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
5
+ import * as shared from "./bridge-adapters.shared.js";
6
+ import { LOCAL_CLIENT_PROTOCOL_VERSION } from "../runtime/runtime-types.js";
7
+ const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, } = shared;
8
+ export class LocalCompanionProxyAdapter {
9
+ options;
10
+ state;
11
+ eventSink = () => undefined;
12
+ server = null;
13
+ socket = null;
14
+ detachMessageListener = null;
15
+ requestCounter = 0;
16
+ endpoint = null;
17
+ pendingRequests = new Map();
18
+ shuttingDown = false;
19
+ expectedCloseReason = null;
20
+ reconnectShutdownTimer = null;
21
+ constructor(options) {
22
+ this.options = options;
23
+ this.state = {
24
+ kind: options.kind,
25
+ status: "stopped",
26
+ cwd: options.cwd,
27
+ command: options.command,
28
+ profile: options.profile,
29
+ sharedSessionId: options.initialSharedSessionId ?? options.initialSharedThreadId,
30
+ sharedThreadId: options.kind === "codex" || options.kind === "opencode"
31
+ ? options.initialSharedSessionId ?? options.initialSharedThreadId
32
+ : undefined,
33
+ activeRuntimeSessionId: options.kind === "claude" || options.kind === "opencode"
34
+ ? options.initialSharedSessionId ?? options.initialSharedThreadId
35
+ : undefined,
36
+ resumeConversationId: options.kind === "claude" ? options.initialResumeConversationId : undefined,
37
+ transcriptPath: options.kind === "claude" ? options.initialTranscriptPath : undefined,
38
+ };
39
+ }
40
+ setEventSink(sink) {
41
+ this.eventSink = sink;
42
+ }
43
+ async start() {
44
+ if (this.server) {
45
+ return;
46
+ }
47
+ this.shuttingDown = false;
48
+ this.expectedCloseReason = null;
49
+ this.clearReconnectShutdownTimer();
50
+ this.setStatus("starting", formatLocalCompanionStartupMessage({
51
+ kind: this.options.kind,
52
+ launchMode: this.options.companionLaunchMode,
53
+ }));
54
+ await new Promise((resolve, reject) => {
55
+ const server = net.createServer((socket) => {
56
+ this.handlePanelSocket(socket);
57
+ });
58
+ this.server = server;
59
+ server.on("error", (error) => {
60
+ reject(error);
61
+ });
62
+ server.listen(0, CODEX_APP_SERVER_HOST, () => {
63
+ const address = server.address();
64
+ if (!address || typeof address === "string") {
65
+ reject(new Error(`Failed to allocate a local ${this.options.kind} companion port.`));
66
+ return;
67
+ }
68
+ this.endpoint = {
69
+ protocolVersion: LOCAL_CLIENT_PROTOCOL_VERSION,
70
+ runtimeKind: "legacy_adapter",
71
+ instanceId: `${process.pid}-${Date.now().toString(36)}`,
72
+ kind: this.options.kind,
73
+ port: address.port,
74
+ token: buildLocalCompanionToken(),
75
+ cwd: this.options.cwd,
76
+ command: this.options.command,
77
+ profile: this.options.profile,
78
+ sharedSessionId: getSharedSessionIdFromAdapterState(this.state),
79
+ resumeConversationId: this.state.resumeConversationId,
80
+ transcriptPath: this.state.transcriptPath,
81
+ startedAt: nowIso(),
82
+ };
83
+ const endpoint = this.endpoint;
84
+ writeLocalCompanionEndpoint(endpoint);
85
+ resolve();
86
+ });
87
+ });
88
+ }
89
+ async sendInput(text) {
90
+ await this.sendRequest({
91
+ command: "send_input",
92
+ text,
93
+ });
94
+ }
95
+ async listResumeSessions(limit = 10) {
96
+ const result = await this.sendRequest({
97
+ command: "list_resume_sessions",
98
+ limit,
99
+ });
100
+ return Array.isArray(result) ? result : [];
101
+ }
102
+ async resumeSession(sessionId) {
103
+ await this.sendRequest({
104
+ command: "resume_session",
105
+ sessionId,
106
+ });
107
+ }
108
+ async createSession() {
109
+ await this.sendRequest({
110
+ command: "create_session",
111
+ });
112
+ }
113
+ async interrupt() {
114
+ const result = await this.sendRequest({
115
+ command: "interrupt",
116
+ });
117
+ return Boolean(result);
118
+ }
119
+ async reset() {
120
+ await this.sendRequest({
121
+ command: "reset",
122
+ });
123
+ }
124
+ async resolveApproval(action) {
125
+ const result = await this.sendRequest({
126
+ command: "resolve_approval",
127
+ action,
128
+ });
129
+ return Boolean(result);
130
+ }
131
+ async submitUserInput(_answers) {
132
+ return false;
133
+ }
134
+ async dispose() {
135
+ this.shuttingDown = true;
136
+ this.expectedCloseReason = null;
137
+ this.clearReconnectShutdownTimer();
138
+ this.rejectPendingRequests(`${this.options.kind} companion proxy is shutting down.`);
139
+ clearLocalCompanionEndpoint(this.options.cwd, this.endpoint?.instanceId, {
140
+ adapter: this.options.kind,
141
+ });
142
+ if (this.socket) {
143
+ try {
144
+ sendLocalCompanionMessage(this.socket, {
145
+ type: "request",
146
+ id: `${++this.requestCounter}`,
147
+ payload: { command: "dispose" },
148
+ });
149
+ }
150
+ catch {
151
+ // Best effort.
152
+ }
153
+ this.detachPanelSocket();
154
+ }
155
+ if (!this.server) {
156
+ this.state.status = "stopped";
157
+ return;
158
+ }
159
+ const server = this.server;
160
+ this.server = null;
161
+ await new Promise((resolve) => {
162
+ server.close(() => resolve());
163
+ });
164
+ this.state.status = "stopped";
165
+ }
166
+ getState() {
167
+ return JSON.parse(JSON.stringify(this.state));
168
+ }
169
+ handlePanelSocket(socket) {
170
+ if (!this.endpoint) {
171
+ socket.destroy();
172
+ return;
173
+ }
174
+ if (this.socket) {
175
+ socket.end();
176
+ socket.destroy();
177
+ return;
178
+ }
179
+ let authenticated = false;
180
+ socket.setNoDelay(true);
181
+ const detachListener = attachLocalCompanionMessageListener(socket, (message) => {
182
+ if (!authenticated) {
183
+ if (message.type !== "hello" ||
184
+ message.token !== this.endpoint?.token) {
185
+ socket.destroy();
186
+ return;
187
+ }
188
+ authenticated = true;
189
+ this.clearReconnectShutdownTimer();
190
+ this.expectedCloseReason = null;
191
+ this.socket = socket;
192
+ this.detachMessageListener = detachListener;
193
+ const companionConnectedAt = nowIso();
194
+ if (this.endpoint) {
195
+ this.endpoint.companionPid = message.companionPid;
196
+ this.endpoint.companionConnectedAt = companionConnectedAt;
197
+ }
198
+ updateLocalCompanionOccupancy(this.options.cwd, {
199
+ companionPid: message.companionPid,
200
+ companionConnectedAt,
201
+ }, this.endpoint?.instanceId, { adapter: this.options.kind });
202
+ sendLocalCompanionMessage(socket, { type: "hello_ack" });
203
+ return;
204
+ }
205
+ this.handlePanelMessage(message);
206
+ });
207
+ socket.once("close", () => {
208
+ if (this.socket === socket) {
209
+ const expectedCloseReason = this.expectedCloseReason;
210
+ this.expectedCloseReason = null;
211
+ clearLocalCompanionOccupancy(this.options.cwd, this.endpoint?.instanceId, {
212
+ adapter: this.options.kind,
213
+ });
214
+ this.detachPanelSocket();
215
+ if (!this.shuttingDown) {
216
+ this.handleCompanionDisconnect(expectedCloseReason);
217
+ }
218
+ }
219
+ });
220
+ socket.once("error", () => {
221
+ socket.destroy();
222
+ });
223
+ }
224
+ handlePanelMessage(message) {
225
+ switch (message.type) {
226
+ case "closing":
227
+ this.expectedCloseReason = message.reason;
228
+ return;
229
+ case "event":
230
+ this.eventSink(message.event);
231
+ return;
232
+ case "state":
233
+ if (this.endpoint) {
234
+ Object.assign(this.endpoint, buildCompanionHealthPatch(message.state, nowIso()));
235
+ const nextSessionId = getSharedSessionIdFromAdapterState(message.state);
236
+ if (this.endpoint.sharedSessionId !== nextSessionId ||
237
+ this.endpoint.resumeConversationId !== message.state.resumeConversationId ||
238
+ this.endpoint.transcriptPath !== message.state.transcriptPath) {
239
+ this.endpoint.sharedSessionId = nextSessionId;
240
+ this.endpoint.sharedThreadId =
241
+ this.options.kind === "codex" || this.options.kind === "opencode" ? nextSessionId : undefined;
242
+ this.endpoint.resumeConversationId = message.state.resumeConversationId;
243
+ this.endpoint.transcriptPath = message.state.transcriptPath;
244
+ }
245
+ writeLocalCompanionEndpoint(this.endpoint);
246
+ }
247
+ this.state.pid = undefined;
248
+ this.state.startedAt = undefined;
249
+ this.state.lastInputAt = undefined;
250
+ this.state.lastOutputAt = undefined;
251
+ this.state.pendingApproval = null;
252
+ this.state.sharedSessionId = undefined;
253
+ this.state.sharedThreadId = undefined;
254
+ this.state.activeRuntimeSessionId = undefined;
255
+ this.state.resumeConversationId = undefined;
256
+ this.state.transcriptPath = undefined;
257
+ this.state.lastSessionSwitchAt = undefined;
258
+ this.state.lastSessionSwitchSource = undefined;
259
+ this.state.lastSessionSwitchReason = undefined;
260
+ this.state.lastThreadSwitchAt = undefined;
261
+ this.state.lastThreadSwitchSource = undefined;
262
+ this.state.lastThreadSwitchReason = undefined;
263
+ this.state.activeTurnId = undefined;
264
+ this.state.activeTurnOrigin = undefined;
265
+ this.state.pendingApprovalOrigin = undefined;
266
+ Object.assign(this.state, message.state);
267
+ this.eventSink({
268
+ type: "status",
269
+ status: this.state.status,
270
+ timestamp: nowIso(),
271
+ });
272
+ return;
273
+ case "response": {
274
+ const pending = this.pendingRequests.get(message.id);
275
+ if (!pending) {
276
+ return;
277
+ }
278
+ this.pendingRequests.delete(message.id);
279
+ if (!message.ok) {
280
+ pending.reject(new Error(message.error ?? `Unknown ${this.options.kind} companion error.`));
281
+ return;
282
+ }
283
+ pending.resolve(message.result);
284
+ return;
285
+ }
286
+ }
287
+ }
288
+ detachPanelSocket() {
289
+ this.detachMessageListener?.();
290
+ this.detachMessageListener = null;
291
+ if (this.socket) {
292
+ this.socket.removeAllListeners();
293
+ this.socket.destroy();
294
+ this.socket = null;
295
+ }
296
+ this.state.pid = undefined;
297
+ this.state.startedAt = undefined;
298
+ this.state.lastInputAt = undefined;
299
+ this.state.lastOutputAt = undefined;
300
+ this.state.pendingApproval = null;
301
+ this.state.pendingApprovalOrigin = undefined;
302
+ this.state.activeTurnId = undefined;
303
+ this.state.activeTurnOrigin = undefined;
304
+ }
305
+ handleCompanionDisconnect(expectedCloseReason) {
306
+ const disposition = getCompanionDisconnectDisposition({
307
+ kind: this.options.kind,
308
+ lifecycle: this.options.lifecycle,
309
+ expectedClose: isExpectedLocalCompanionClose(expectedCloseReason),
310
+ reconnectGraceMs: LOCAL_COMPANION_RECONNECT_GRACE_MS,
311
+ launchMode: this.options.companionLaunchMode,
312
+ });
313
+ if (disposition.action === "shutdown") {
314
+ this.clearReconnectShutdownTimer();
315
+ this.setStatus("stopped", disposition.message);
316
+ this.eventSink({
317
+ type: "shutdown_requested",
318
+ reason: disposition.shutdownReason,
319
+ message: disposition.message,
320
+ timestamp: nowIso(),
321
+ });
322
+ return;
323
+ }
324
+ if (disposition.action === "wait_for_reconnect") {
325
+ this.setStatus("starting", disposition.message);
326
+ this.armReconnectShutdownTimer();
327
+ return;
328
+ }
329
+ this.clearReconnectShutdownTimer();
330
+ this.setStatus("starting", disposition.message);
331
+ }
332
+ armReconnectShutdownTimer() {
333
+ this.clearReconnectShutdownTimer();
334
+ this.reconnectShutdownTimer = setTimeout(() => {
335
+ this.reconnectShutdownTimer = null;
336
+ if (this.shuttingDown || this.socket) {
337
+ return;
338
+ }
339
+ const message = buildCompanionReconnectTimeoutMessage({
340
+ kind: this.options.kind,
341
+ reconnectGraceMs: LOCAL_COMPANION_RECONNECT_GRACE_MS,
342
+ });
343
+ this.setStatus("stopped", message);
344
+ this.eventSink({
345
+ type: "shutdown_requested",
346
+ reason: "companion_reconnect_timeout",
347
+ message,
348
+ timestamp: nowIso(),
349
+ });
350
+ }, LOCAL_COMPANION_RECONNECT_GRACE_MS);
351
+ this.reconnectShutdownTimer.unref?.();
352
+ }
353
+ clearReconnectShutdownTimer() {
354
+ if (!this.reconnectShutdownTimer) {
355
+ return;
356
+ }
357
+ clearTimeout(this.reconnectShutdownTimer);
358
+ this.reconnectShutdownTimer = null;
359
+ }
360
+ setStatus(status, message) {
361
+ this.state.status = status;
362
+ this.eventSink({
363
+ type: "status",
364
+ status,
365
+ message,
366
+ timestamp: nowIso(),
367
+ });
368
+ }
369
+ rejectPendingRequests(message) {
370
+ for (const pending of this.pendingRequests.values()) {
371
+ pending.reject(new Error(message));
372
+ }
373
+ this.pendingRequests.clear();
374
+ }
375
+ async sendRequest(payload) {
376
+ const socket = this.socket;
377
+ if (!socket) {
378
+ throw new Error(formatCompanionNotConnectedMessage({
379
+ kind: this.options.kind,
380
+ launchMode: this.options.companionLaunchMode,
381
+ }));
382
+ }
383
+ if (!this.state.pid && payload.command !== "dispose") {
384
+ throw new Error(`${this.options.kind} companion is connected but not ready yet. Wait for it to finish starting.`);
385
+ }
386
+ const id = `${++this.requestCounter}`;
387
+ const response = new Promise((resolve, reject) => {
388
+ this.pendingRequests.set(id, { resolve, reject });
389
+ });
390
+ sendLocalCompanionMessage(socket, {
391
+ type: "request",
392
+ id,
393
+ payload,
394
+ });
395
+ return await response;
396
+ }
397
+ }
398
+ export function shouldStopBridgeAfterCompanionDisconnect(lifecycle) {
399
+ return lifecycle === "companion_bound";
400
+ }
401
+ export function buildCompanionHealthPatch(state, timestamp) {
402
+ return {
403
+ companionStatus: state.status,
404
+ companionLastStateAt: timestamp,
405
+ companionWorkerPid: state.pid,
406
+ };
407
+ }
408
+ export function formatLocalCompanionStartupMessage(params) {
409
+ if (params.launchMode === "daemon_auto") {
410
+ return `Waiting for daemon-managed ${params.kind} companion connection. The daemon will open or reuse the visible CLI automatically.`;
411
+ }
412
+ return `Waiting for manual ${params.kind} companion connection. Run "${getLocalCompanionCommandName(params.kind)}" in a second terminal for this directory.`;
413
+ }
414
+ export function formatCompanionNotConnectedMessage(params) {
415
+ if (params.launchMode === "daemon_auto") {
416
+ return `${params.kind} companion is not connected yet. Send /${params.kind} in WeChat to open or reconnect the visible CLI automatically.`;
417
+ }
418
+ return `${params.kind} companion is not connected. Run "${getLocalCompanionCommandName(params.kind)}" in a second terminal for this directory.`;
419
+ }
420
+ export function isExpectedLocalCompanionClose(reason) {
421
+ return typeof reason === "string" && reason.length > 0;
422
+ }
423
+ export function buildCompanionReconnectTimeoutMessage(params) {
424
+ return `${params.kind} companion did not reconnect within ${Math.ceil(params.reconnectGraceMs / 1000)}s. Stopping transient bridge bound to ${getLocalCompanionCommandName(params.kind)}.`;
425
+ }
426
+ export function getCompanionDisconnectDisposition(params) {
427
+ const commandName = getLocalCompanionCommandName(params.kind);
428
+ if (shouldStopBridgeAfterCompanionDisconnect(params.lifecycle)) {
429
+ if (params.expectedClose) {
430
+ return {
431
+ action: "shutdown",
432
+ shutdownReason: "companion_closed",
433
+ message: `${params.kind} companion closed. Stopping transient bridge bound to ${commandName}.`,
434
+ };
435
+ }
436
+ return {
437
+ action: "wait_for_reconnect",
438
+ message: `${params.kind} companion disconnected unexpectedly. Waiting up to ${Math.ceil(params.reconnectGraceMs / 1000)}s for ${commandName} to reconnect before stopping this transient bridge.`,
439
+ };
440
+ }
441
+ if (params.expectedClose) {
442
+ if (params.launchMode === "daemon_auto") {
443
+ return {
444
+ action: "await_manual_reconnect",
445
+ message: `${params.kind} companion closed. Send /${params.kind} in WeChat to reopen the visible CLI automatically.`,
446
+ };
447
+ }
448
+ return {
449
+ action: "await_manual_reconnect",
450
+ message: `${params.kind} companion closed. Run "${commandName}" again in a second terminal for this directory.`,
451
+ };
452
+ }
453
+ if (params.launchMode === "daemon_auto") {
454
+ return {
455
+ action: "await_manual_reconnect",
456
+ message: `${params.kind} companion disconnected unexpectedly. Send /${params.kind} in WeChat to reopen the visible CLI automatically.`,
457
+ };
458
+ }
459
+ return {
460
+ action: "await_manual_reconnect",
461
+ message: `${params.kind} companion disconnected unexpectedly. Run "${commandName}" again in a second terminal for this directory to reconnect.`,
462
+ };
463
+ }
464
+ export class AbstractPtyAdapter {
465
+ options;
466
+ pty = null;
467
+ eventSink = () => undefined;
468
+ completionTimer = null;
469
+ state;
470
+ hasAcceptedInput = false;
471
+ shuttingDown = false;
472
+ currentPreview = "(idle)";
473
+ pendingApproval = null;
474
+ constructor(options) {
475
+ this.options = options;
476
+ this.state = {
477
+ kind: options.kind,
478
+ status: "stopped",
479
+ cwd: options.cwd,
480
+ command: options.command,
481
+ profile: options.profile,
482
+ };
483
+ }
484
+ setEventSink(sink) {
485
+ this.eventSink = sink;
486
+ }
487
+ async start() {
488
+ if (this.pty) {
489
+ return;
490
+ }
491
+ this.setStatus("starting", `Starting ${this.options.kind} adapter...`);
492
+ let spawnTarget = null;
493
+ try {
494
+ spawnTarget = resolveSpawnTarget(this.options.command, this.options.kind);
495
+ const env = this.buildEnv();
496
+ const ptyProcess = spawnPty(spawnTarget.file, [...spawnTarget.args, ...this.buildSpawnArgs()], buildPtySpawnOptions({
497
+ cwd: this.options.cwd,
498
+ env,
499
+ }));
500
+ this.pty = ptyProcess;
501
+ this.shuttingDown = false;
502
+ this.hasAcceptedInput = false;
503
+ this.state.pid = ptyProcess.pid;
504
+ this.state.startedAt = nowIso();
505
+ this.state.status = "idle";
506
+ this.state.pendingApproval = null;
507
+ ptyProcess.onData((data) => this.handleData(data));
508
+ ptyProcess.onExit(({ exitCode }) => this.handleExit(exitCode));
509
+ this.afterStart();
510
+ this.setStatus("idle", `${this.options.kind} adapter is ready.`);
511
+ }
512
+ catch (err) {
513
+ this.state.status = "error";
514
+ this.emit({
515
+ type: "fatal_error",
516
+ message: `Failed to start ${this.options.kind}${spawnTarget ? ` (${spawnTarget.file})` : ""}: ${String(err)}`,
517
+ timestamp: nowIso(),
518
+ });
519
+ throw err;
520
+ }
521
+ }
522
+ async sendInput(text) {
523
+ if (!this.pty) {
524
+ throw new Error(`${this.options.kind} adapter is not running.`);
525
+ }
526
+ this.hasAcceptedInput = true;
527
+ this.currentPreview = truncatePreview(text);
528
+ this.state.lastInputAt = nowIso();
529
+ this.pendingApproval = null;
530
+ this.state.pendingApproval = null;
531
+ this.writeToPty(this.prepareInput(text));
532
+ this.setStatus("busy");
533
+ this.scheduleTaskComplete(this.defaultCompletionDelayMs());
534
+ }
535
+ async listResumeSessions(_limit = 10) {
536
+ throw new Error("/resume is only supported for the codex adapter.");
537
+ }
538
+ async resumeSession(_sessionId) {
539
+ throw new Error("/resume is only supported for the codex adapter.");
540
+ }
541
+ async interrupt() {
542
+ if (!this.pty) {
543
+ return false;
544
+ }
545
+ this.writeToPty("\u0003");
546
+ this.scheduleTaskComplete(INTERRUPT_SETTLE_DELAY_MS);
547
+ this.emit({
548
+ type: "status",
549
+ status: this.state.status,
550
+ message: "Interrupt signal sent to the worker.",
551
+ timestamp: nowIso(),
552
+ });
553
+ return true;
554
+ }
555
+ async reset() {
556
+ await this.dispose();
557
+ await this.start();
558
+ }
559
+ async resolveApproval(action) {
560
+ if (!this.pendingApproval) {
561
+ return false;
562
+ }
563
+ const handled = await this.applyApproval(action, this.pendingApproval);
564
+ if (!handled) {
565
+ return false;
566
+ }
567
+ this.pendingApproval = null;
568
+ this.state.pendingApproval = null;
569
+ return true;
570
+ }
571
+ async submitUserInput(_answers) {
572
+ return false;
573
+ }
574
+ async dispose() {
575
+ this.clearCompletionTimer();
576
+ this.pendingApproval = null;
577
+ this.state.pendingApproval = null;
578
+ if (!this.pty) {
579
+ this.state.status = "stopped";
580
+ return;
581
+ }
582
+ this.shuttingDown = true;
583
+ try {
584
+ this.pty.kill();
585
+ }
586
+ catch {
587
+ // Best effort shutdown.
588
+ }
589
+ this.pty = null;
590
+ this.state.status = "stopped";
591
+ this.state.pid = undefined;
592
+ }
593
+ getState() {
594
+ return JSON.parse(JSON.stringify(this.state));
595
+ }
596
+ afterStart() {
597
+ // Optional hook.
598
+ }
599
+ prepareInput(text) {
600
+ return `${text.replace(/\r?\n/g, "\r")}\r`;
601
+ }
602
+ defaultCompletionDelayMs() {
603
+ return 5_000;
604
+ }
605
+ async applyApproval(action, pendingApproval) {
606
+ if (!this.pty) {
607
+ return false;
608
+ }
609
+ const input = action === "confirm"
610
+ ? pendingApproval.confirmInput ?? "y\r"
611
+ : pendingApproval.denyInput ?? "n\r";
612
+ this.setStatus("busy");
613
+ this.writeToPty(input);
614
+ this.scheduleTaskComplete(this.defaultCompletionDelayMs());
615
+ return true;
616
+ }
617
+ buildEnv() {
618
+ return buildCliEnvironment(this.options.kind);
619
+ }
620
+ emit(event) {
621
+ this.eventSink(event);
622
+ }
623
+ setStatus(status, message) {
624
+ this.state.status = status;
625
+ this.emit({
626
+ type: "status",
627
+ status,
628
+ message,
629
+ timestamp: nowIso(),
630
+ });
631
+ }
632
+ scheduleTaskComplete(delayMs) {
633
+ if (!this.hasAcceptedInput || this.state.status !== "busy") {
634
+ return;
635
+ }
636
+ this.clearCompletionTimer();
637
+ this.completionTimer = setTimeout(() => {
638
+ this.completionTimer = null;
639
+ if (this.state.status !== "busy") {
640
+ return;
641
+ }
642
+ this.setStatus("idle");
643
+ this.emit({
644
+ type: "task_complete",
645
+ summary: this.currentPreview,
646
+ timestamp: nowIso(),
647
+ });
648
+ }, delayMs);
649
+ }
650
+ clearCompletionTimer() {
651
+ if (!this.completionTimer) {
652
+ return;
653
+ }
654
+ clearTimeout(this.completionTimer);
655
+ this.completionTimer = null;
656
+ }
657
+ writeToPty(data) {
658
+ this.pty?.write(data);
659
+ }
660
+ handleData(rawText) {
661
+ const text = normalizeOutput(rawText);
662
+ if (!text) {
663
+ return;
664
+ }
665
+ this.state.lastOutputAt = nowIso();
666
+ if (!this.hasAcceptedInput) {
667
+ return;
668
+ }
669
+ if (!this.pendingApproval) {
670
+ const approval = detectCliApproval(text);
671
+ if (approval) {
672
+ this.pendingApproval = approval;
673
+ this.state.pendingApproval = approval;
674
+ this.setStatus("awaiting_approval", "CLI approval is required.");
675
+ this.emit({
676
+ type: "approval_required",
677
+ request: approval,
678
+ timestamp: nowIso(),
679
+ });
680
+ return;
681
+ }
682
+ }
683
+ this.emit({
684
+ type: "stdout",
685
+ text,
686
+ timestamp: nowIso(),
687
+ });
688
+ if (this.state.status === "busy") {
689
+ this.scheduleTaskComplete(this.defaultCompletionDelayMs());
690
+ }
691
+ }
692
+ handleExit(exitCode) {
693
+ this.clearCompletionTimer();
694
+ const expectedShutdown = this.shuttingDown;
695
+ this.shuttingDown = false;
696
+ this.pty = null;
697
+ this.state.status = "stopped";
698
+ this.state.pid = undefined;
699
+ this.pendingApproval = null;
700
+ this.state.pendingApproval = null;
701
+ if (expectedShutdown) {
702
+ this.emit({
703
+ type: "status",
704
+ status: "stopped",
705
+ message: `${this.options.kind} worker stopped.`,
706
+ timestamp: nowIso(),
707
+ });
708
+ return;
709
+ }
710
+ const exitLabel = typeof exitCode === "number" ? `code ${exitCode}` : "an unknown code";
711
+ this.emit({
712
+ type: "fatal_error",
713
+ message: `${this.options.kind} worker exited unexpectedly with ${exitLabel}.`,
714
+ timestamp: nowIso(),
715
+ });
716
+ }
717
+ }