@unlinearity/cli-wechat-bridge 1.0.3 → 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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/daemon/wechat-daemon.js");
@@ -0,0 +1,95 @@
1
+ export function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ export function getCodexRpcRequestId(value) {
5
+ return typeof value === "string" || typeof value === "number" ? value : null;
6
+ }
7
+ export function getNotificationThreadId(params) {
8
+ if (!isRecord(params)) {
9
+ return null;
10
+ }
11
+ if (typeof params.threadId === "string") {
12
+ return params.threadId;
13
+ }
14
+ if (isRecord(params.thread) && typeof params.thread.id === "string") {
15
+ return params.thread.id;
16
+ }
17
+ return null;
18
+ }
19
+ export function getNotificationTurnId(params) {
20
+ if (!isRecord(params)) {
21
+ return null;
22
+ }
23
+ if (typeof params.turnId === "string") {
24
+ return params.turnId;
25
+ }
26
+ if (isRecord(params.turn) && typeof params.turn.id === "string") {
27
+ return params.turn.id;
28
+ }
29
+ return null;
30
+ }
31
+ export function describeUnknownError(error) {
32
+ if (error instanceof Error && error.message) {
33
+ return error.message;
34
+ }
35
+ return String(error);
36
+ }
37
+ export function normalizeCodexRpcError(error) {
38
+ if (isRecord(error)) {
39
+ const message = typeof error.message === "string"
40
+ ? error.message
41
+ : typeof error.code === "number"
42
+ ? `RPC error ${error.code}`
43
+ : "";
44
+ const data = typeof error.data === "string"
45
+ ? error.data
46
+ : typeof error.details === "string"
47
+ ? error.details
48
+ : "";
49
+ const combined = [message, data].filter(Boolean).join(": ");
50
+ if (combined) {
51
+ return combined;
52
+ }
53
+ }
54
+ return describeUnknownError(error);
55
+ }
56
+ export function getLocalCompanionCommandName(kind) {
57
+ switch (kind) {
58
+ case "codex":
59
+ return "wechat-codex";
60
+ case "claude":
61
+ return "wechat-claude";
62
+ case "opencode":
63
+ return "wechat-opencode";
64
+ default:
65
+ return "local companion";
66
+ }
67
+ }
68
+ export function getSharedSessionIdFromAdapterState(state) {
69
+ return state.sharedSessionId ?? state.sharedThreadId;
70
+ }
71
+ export function quoteWindowsCommandArg(value) {
72
+ return `"${value.replace(/"/g, '""')}"`;
73
+ }
74
+ export function quotePosixCommandArg(value) {
75
+ return `'${value.replace(/'/g, `'\\''`)}'`;
76
+ }
77
+ export function isRecentIsoTimestamp(timestamp, maxAgeMs) {
78
+ const parsedMs = Date.parse(timestamp);
79
+ if (!Number.isFinite(parsedMs)) {
80
+ return false;
81
+ }
82
+ return parsedMs >= Date.now() - maxAgeMs;
83
+ }
84
+ export function coerceWebSocketMessageData(data) {
85
+ if (typeof data === "string") {
86
+ return data;
87
+ }
88
+ if (data instanceof ArrayBuffer) {
89
+ return Buffer.from(data).toString("utf8");
90
+ }
91
+ if (ArrayBuffer.isView(data)) {
92
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
93
+ }
94
+ return null;
95
+ }
@@ -7,7 +7,7 @@ import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettin
7
7
  import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
8
8
  import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
9
9
  import * as shared from "./bridge-adapters.shared.js";
10
- const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, MODULE_DIR, buildClaudeCliArgs, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
10
+ const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
11
11
  const CLAUDE_COMPACT_OUTPUT_LINE_RE = /^Compacted(?:\s*\(.*full summary.*\))?$/i;
12
12
  const CLAUDE_COMPACT_FAILURE_RE = /Error:\s*Error during compaction:|(?:^|\b)API Error:|\b(?:compact|compaction)\s+failed\b|^Error:/i;
13
13
  const CLAUDE_COMPACT_DEDUP_MS = 2_000;
@@ -469,6 +469,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
469
469
  }
470
470
  this.sessionReadOffset = 0;
471
471
  this.sessionPartialLine = "";
472
+ this.seedSessionReplayCutoff(startedAtMs);
472
473
  }
473
474
  let content;
474
475
  try {
@@ -495,6 +496,17 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
495
496
  this.handleSessionLogLine(line);
496
497
  }
497
498
  }
499
+ seedSessionReplayCutoff(startedAtMs) {
500
+ if (this.sessionIgnoreBeforeMs !== null ||
501
+ this.pendingTurnStart ||
502
+ this.activeTurn ||
503
+ this.state.activeTurnId) {
504
+ return;
505
+ }
506
+ if (Number.isFinite(startedAtMs)) {
507
+ this.sessionIgnoreBeforeMs = startedAtMs;
508
+ }
509
+ }
498
510
  maybeApplyRecentSessionFallback() {
499
511
  if (!this.isNativePanelMode()) {
500
512
  return;
@@ -538,6 +550,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
538
550
  this.sessionReadOffset = 0;
539
551
  this.sessionPartialLine = "";
540
552
  this.sessionFinalText = null;
553
+ this.seedSessionReplayCutoff(startedAtMs);
541
554
  }
542
555
  }
543
556
  handleSessionLogLine(line) {
@@ -790,7 +803,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
790
803
  this.pendingTurnThreadId = null;
791
804
  this.interruptPendingTurnStart = false;
792
805
  this.state.activeTurnOrigin = undefined;
793
- if (!this.activeTurn && this.state.status === "busy") {
806
+ if (!this.activeTurn && this.getState().status === "busy") {
794
807
  this.setStatus("idle");
795
808
  }
796
809
  throw error;
@@ -886,7 +899,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
886
899
  catch (err) {
887
900
  await this.stopAppServer();
888
901
  const details = this.describeAppServerLog();
889
- throw new Error(`Failed to start Codex app-server: ${String(err)}${details}`);
902
+ throw new Error(`Failed to start Codex app-server: ${String(err)}${details}`, {
903
+ cause: err,
904
+ });
890
905
  }
891
906
  }
892
907
  async connectRpcClient() {
@@ -1982,7 +1997,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1982
1997
  if (deltaFallback.length === 0) {
1983
1998
  return null;
1984
1999
  }
1985
- return deltaFallback[deltaFallback.length - 1];
2000
+ return deltaFallback[deltaFallback.length - 1] ?? null;
1986
2001
  }
1987
2002
  recordTurnActivity(turnId, timestamp = Date.now()) {
1988
2003
  const timestampMs = typeof timestamp === "number" ? timestamp : Date.parse(timestamp);
@@ -3,6 +3,7 @@ import { spawn as spawnPty } from "node-pty";
3
3
  import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, sendLocalCompanionMessage, updateLocalCompanionOccupancy, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
4
4
  import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
5
5
  import * as shared from "./bridge-adapters.shared.js";
6
+ import { LOCAL_CLIENT_PROTOCOL_VERSION } from "../runtime/runtime-types.js";
6
7
  const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, } = shared;
7
8
  export class LocalCompanionProxyAdapter {
8
9
  options;
@@ -46,7 +47,10 @@ export class LocalCompanionProxyAdapter {
46
47
  this.shuttingDown = false;
47
48
  this.expectedCloseReason = null;
48
49
  this.clearReconnectShutdownTimer();
49
- this.setStatus("starting", `Waiting for manual ${this.options.kind} companion connection. Run "${getLocalCompanionCommandName(this.options.kind)}" in a second terminal for this directory.`);
50
+ this.setStatus("starting", formatLocalCompanionStartupMessage({
51
+ kind: this.options.kind,
52
+ launchMode: this.options.companionLaunchMode,
53
+ }));
50
54
  await new Promise((resolve, reject) => {
51
55
  const server = net.createServer((socket) => {
52
56
  this.handlePanelSocket(socket);
@@ -62,6 +66,8 @@ export class LocalCompanionProxyAdapter {
62
66
  return;
63
67
  }
64
68
  this.endpoint = {
69
+ protocolVersion: LOCAL_CLIENT_PROTOCOL_VERSION,
70
+ runtimeKind: "legacy_adapter",
65
71
  instanceId: `${process.pid}-${Date.now().toString(36)}`,
66
72
  kind: this.options.kind,
67
73
  port: address.port,
@@ -74,7 +80,8 @@ export class LocalCompanionProxyAdapter {
74
80
  transcriptPath: this.state.transcriptPath,
75
81
  startedAt: nowIso(),
76
82
  };
77
- writeLocalCompanionEndpoint(this.endpoint);
83
+ const endpoint = this.endpoint;
84
+ writeLocalCompanionEndpoint(endpoint);
78
85
  resolve();
79
86
  });
80
87
  });
@@ -129,7 +136,9 @@ export class LocalCompanionProxyAdapter {
129
136
  this.expectedCloseReason = null;
130
137
  this.clearReconnectShutdownTimer();
131
138
  this.rejectPendingRequests(`${this.options.kind} companion proxy is shutting down.`);
132
- clearLocalCompanionEndpoint(this.options.cwd, this.endpoint?.instanceId);
139
+ clearLocalCompanionEndpoint(this.options.cwd, this.endpoint?.instanceId, {
140
+ adapter: this.options.kind,
141
+ });
133
142
  if (this.socket) {
134
143
  try {
135
144
  sendLocalCompanionMessage(this.socket, {
@@ -181,10 +190,15 @@ export class LocalCompanionProxyAdapter {
181
190
  this.expectedCloseReason = null;
182
191
  this.socket = socket;
183
192
  this.detachMessageListener = detachListener;
193
+ const companionConnectedAt = nowIso();
194
+ if (this.endpoint) {
195
+ this.endpoint.companionPid = message.companionPid;
196
+ this.endpoint.companionConnectedAt = companionConnectedAt;
197
+ }
184
198
  updateLocalCompanionOccupancy(this.options.cwd, {
185
199
  companionPid: message.companionPid,
186
- companionConnectedAt: nowIso(),
187
- }, this.endpoint?.instanceId);
200
+ companionConnectedAt,
201
+ }, this.endpoint?.instanceId, { adapter: this.options.kind });
188
202
  sendLocalCompanionMessage(socket, { type: "hello_ack" });
189
203
  return;
190
204
  }
@@ -194,7 +208,9 @@ export class LocalCompanionProxyAdapter {
194
208
  if (this.socket === socket) {
195
209
  const expectedCloseReason = this.expectedCloseReason;
196
210
  this.expectedCloseReason = null;
197
- clearLocalCompanionOccupancy(this.options.cwd, this.endpoint?.instanceId);
211
+ clearLocalCompanionOccupancy(this.options.cwd, this.endpoint?.instanceId, {
212
+ adapter: this.options.kind,
213
+ });
198
214
  this.detachPanelSocket();
199
215
  if (!this.shuttingDown) {
200
216
  this.handleCompanionDisconnect(expectedCloseReason);
@@ -292,6 +308,7 @@ export class LocalCompanionProxyAdapter {
292
308
  lifecycle: this.options.lifecycle,
293
309
  expectedClose: isExpectedLocalCompanionClose(expectedCloseReason),
294
310
  reconnectGraceMs: LOCAL_COMPANION_RECONNECT_GRACE_MS,
311
+ launchMode: this.options.companionLaunchMode,
295
312
  });
296
313
  if (disposition.action === "shutdown") {
297
314
  this.clearReconnectShutdownTimer();
@@ -358,7 +375,10 @@ export class LocalCompanionProxyAdapter {
358
375
  async sendRequest(payload) {
359
376
  const socket = this.socket;
360
377
  if (!socket) {
361
- throw new Error(`${this.options.kind} companion is not connected. Run "${getLocalCompanionCommandName(this.options.kind)}" in a second terminal for this directory.`);
378
+ throw new Error(formatCompanionNotConnectedMessage({
379
+ kind: this.options.kind,
380
+ launchMode: this.options.companionLaunchMode,
381
+ }));
362
382
  }
363
383
  if (!this.state.pid && payload.command !== "dispose") {
364
384
  throw new Error(`${this.options.kind} companion is connected but not ready yet. Wait for it to finish starting.`);
@@ -385,6 +405,18 @@ export function buildCompanionHealthPatch(state, timestamp) {
385
405
  companionWorkerPid: state.pid,
386
406
  };
387
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
+ }
388
420
  export function isExpectedLocalCompanionClose(reason) {
389
421
  return typeof reason === "string" && reason.length > 0;
390
422
  }
@@ -407,11 +439,23 @@ export function getCompanionDisconnectDisposition(params) {
407
439
  };
408
440
  }
409
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
+ }
410
448
  return {
411
449
  action: "await_manual_reconnect",
412
450
  message: `${params.kind} companion closed. Run "${commandName}" again in a second terminal for this directory.`,
413
451
  };
414
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
+ }
415
459
  return {
416
460
  action: "await_manual_reconnect",
417
461
  message: `${params.kind} companion disconnected unexpectedly. Run "${commandName}" again in a second terminal for this directory to reconnect.`,