@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.
- package/README.md +325 -221
- package/bin/wechat-daemon.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +95 -0
- package/dist/bridge/bridge-adapters.claude.js +1 -1
- package/dist/bridge/bridge-adapters.codex.js +18 -3
- package/dist/bridge/bridge-adapters.core.js +51 -7
- package/dist/bridge/bridge-adapters.opencode.js +220 -22
- package/dist/bridge/bridge-adapters.shared.js +4 -97
- package/dist/bridge/bridge-controller.js +6 -2
- package/dist/bridge/bridge-final-reply.js +18 -3
- package/dist/bridge/bridge-state.js +9 -3
- package/dist/bridge/bridge-utils.js +81 -14
- package/dist/bridge/claude-hooks.js +2 -2
- package/dist/bridge/wechat-bridge.js +139 -31
- package/dist/companion/codex-remote-client.js +11 -4
- package/dist/companion/local-companion-link.js +56 -26
- package/dist/companion/local-companion-start.js +54 -6
- package/dist/companion/local-companion.js +6 -1
- package/dist/daemon/daemon-link.js +175 -0
- package/dist/daemon/wechat-daemon.js +1202 -0
- package/dist/utils/version-checker.js +1 -1
- package/dist/wechat/channel-config.js +126 -33
- package/dist/wechat/standalone-bot.js +5 -6
- package/dist/wechat/wechat-channel.js +21 -10
- package/dist/wechat/wechat-transport.js +336 -10
- package/package.json +12 -2
|
@@ -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.
|
|
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",
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.`,
|