@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.1
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/LICENSE.txt +661 -661
- package/README.md +468 -475
- package/bin/_run-entry.mjs +29 -6
- package/bin/wechat-check-update.mjs +6 -1
- package/bin/wechat-claude.mjs +6 -1
- package/bin/wechat-codex.mjs +6 -1
- package/bin/wechat-daemon.mjs +6 -1
- package/bin/wechat-opencode.mjs +6 -1
- package/bin/wechat-pi.mjs +6 -1
- package/bin/wechat-setup.mjs +6 -1
- package/bin/wecom-check-update.mjs +10 -0
- package/bin/wecom-claude.mjs +10 -0
- package/bin/wecom-codex.mjs +10 -0
- package/bin/wecom-daemon.mjs +10 -0
- package/bin/wecom-opencode.mjs +10 -0
- package/bin/wecom-pi.mjs +10 -0
- package/bin/wecom-setup.mjs +10 -0
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +161 -4
- package/dist/bridge/bridge-adapters.codex.js +206 -14
- package/dist/bridge/bridge-adapters.core.js +25 -1
- package/dist/bridge/bridge-adapters.opencode.js +298 -20
- package/dist/bridge/bridge-adapters.pi.js +45 -0
- package/dist/bridge/bridge-adapters.shared.js +6 -0
- package/dist/bridge/bridge-controller.js +6 -0
- package/dist/bridge/bridge-final-reply.js +6 -0
- package/dist/bridge/bridge-process-reaper.js +41 -2
- package/dist/bridge/bridge-state.js +16 -3
- package/dist/bridge/bridge-utils.js +6 -31
- package/dist/bridge/claude-hook.js +6 -0
- package/dist/bridge/claude-native-controls.js +145 -0
- package/dist/bridge/codex-visible-thread-proxy.js +140 -0
- package/dist/bridge/native-terminal-control.js +271 -0
- package/dist/bridge/opencode-v2-compat.js +450 -0
- package/dist/bridge/wechat-bridge.js +380 -171
- package/dist/bridge/wechat-forwarding.js +6 -0
- package/dist/bridge/wechat-outbound-queue.js +6 -0
- package/dist/channels/wechat/bridge-final-reply.js +13 -22
- package/dist/channels/wechat/index.js +6 -0
- package/dist/channels/wechat/wechat-driver.js +191 -0
- package/dist/channels/wechat/wechat-forwarding.js +18 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +49 -3
- package/dist/channels/wecom/bridge-final-reply.js +32 -0
- package/dist/channels/wecom/setup-cli.js +12 -0
- package/dist/channels/wecom/setup.js +182 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +72 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +147 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +314 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +30 -3
- package/dist/companion/codex-visible-client-link.js +107 -0
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion-start.js +62 -11
- package/dist/companion/local-companion.js +25 -1
- package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
- package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
- package/dist/companion/pi-tui-bridge-extension.js +62 -3
- package/dist/core/bridge-control.js +10 -0
- package/dist/core/bridge-defer.js +25 -0
- package/dist/core/channel-driver.js +1 -0
- package/dist/core/channel-types.js +3 -1
- package/dist/core/conversation-routing.js +58 -0
- package/dist/core/direct-run.js +23 -0
- package/dist/core/inbound-message-claims.js +67 -0
- package/dist/core/outbound-target.js +32 -0
- package/dist/core/text-utils.js +6 -0
- package/dist/core/turn-coordinator.js +85 -0
- package/dist/daemon/daemon-link.js +9 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +675 -122
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +9 -1
- package/dist/i18n/messages-zh.js +9 -1
- package/dist/runtime/create-runtime-host.js +6 -0
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/atomic-file.js +6 -0
- package/dist/utils/doctor.js +36 -10
- package/dist/utils/version-checker.js +17 -11
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +119 -13
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +169 -58
- package/package.json +16 -4
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import fs from "node:fs";
|
|
2
8
|
import path from "node:path";
|
|
3
9
|
import crypto from "node:crypto";
|
|
@@ -6,9 +12,11 @@ import { detectCliApproval, } from "./bridge-utils.js";
|
|
|
6
12
|
import { normalizeOutput, nowIso, summarizeOutput, truncatePreview, } from "../core/text-utils.js";
|
|
7
13
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
8
14
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
15
|
+
import { CodexVisibleThreadProxy } from "./codex-visible-thread-proxy.js";
|
|
9
16
|
import * as shared from "./bridge-adapters.shared.js";
|
|
10
|
-
import { requestCodexVisibleThreadSwitch } from "../companion/codex-visible-client-link.js";
|
|
11
|
-
import {
|
|
17
|
+
import { requestCodexVisibleClientShutdown, requestCodexVisibleThreadSwitch, } from "../companion/codex-visible-client-link.js";
|
|
18
|
+
import { readLocalCompanionEndpoint } from "../companion/local-companion-link.js";
|
|
19
|
+
import { appendBoundedLog as appendBridgeLog, BRIDGE_LOG_FILE, ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
12
20
|
import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
13
21
|
export function parseCodexCliVersion(output) {
|
|
14
22
|
const match = output.match(/\b(\d+\.\d+\.\d+)\b/);
|
|
@@ -21,7 +29,7 @@ export function isCodexVersionInCompatibilityRange(version) {
|
|
|
21
29
|
}
|
|
22
30
|
const major = parts[0] ?? -1;
|
|
23
31
|
const minor = parts[1] ?? -1;
|
|
24
|
-
return major === 0 && minor >= 149 && minor <=
|
|
32
|
+
return major === 0 && minor >= 149 && minor <= 157;
|
|
25
33
|
}
|
|
26
34
|
const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexDynamicToolCallFailureResponse, buildCodexMcpServerElicitationDeclineResponse, buildCodexPermissionsRequestApprovalResponse, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getCodexApprovalAutoResponse, getCodexWechatOutboundAttachmentDenyMessage, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
|
|
27
35
|
const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
|
|
@@ -73,11 +81,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
73
81
|
appServer = null;
|
|
74
82
|
nativeProcess = null;
|
|
75
83
|
appServerPort = null;
|
|
84
|
+
visibleThreadProxy = null;
|
|
85
|
+
visibleOutOfWorkspaceThreadId = null;
|
|
76
86
|
appServerShuttingDown = false;
|
|
77
87
|
appServerLog = "";
|
|
78
88
|
appServerAuthToken = null;
|
|
79
89
|
appServerAuthTokenFilePath = null;
|
|
80
90
|
rpcSocket = null;
|
|
91
|
+
lastRpcCloseDetail = "";
|
|
81
92
|
rpcShuttingDown = false;
|
|
82
93
|
rpcReconnectPromise = null;
|
|
83
94
|
cleanPanelExitInProgress = false;
|
|
@@ -86,6 +97,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
86
97
|
subscribedThreadIds = new Set();
|
|
87
98
|
pendingThreadSubscriptions = new Map();
|
|
88
99
|
sharedThreadId = null;
|
|
100
|
+
previousCollaborationMode = null;
|
|
89
101
|
announcedThreadId = null;
|
|
90
102
|
localThreadFollowBlockedUntilMs = 0;
|
|
91
103
|
pendingThreadStatusChecks = new Map();
|
|
@@ -157,8 +169,18 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
157
169
|
this.setStatus("starting", `Codex protocol compatibility: ${compatibility}.`);
|
|
158
170
|
await this.startAppServer();
|
|
159
171
|
await this.connectRpcClient();
|
|
160
|
-
await this.restoreInitialSharedThreadIfNeeded();
|
|
161
172
|
try {
|
|
173
|
+
if (this.isHeadlessRuntimeMode() && this.appServerPort && this.appServerAuthToken) {
|
|
174
|
+
const trace = (message) => appendBridgeLog(BRIDGE_LOG_FILE, `[${nowIso()}] codex_visible_proxy: ${message}\n`);
|
|
175
|
+
this.visibleThreadProxy = await CodexVisibleThreadProxy.start({
|
|
176
|
+
upstreamUrl: `ws://${CODEX_APP_SERVER_HOST}:${this.appServerPort}`,
|
|
177
|
+
token: this.appServerAuthToken,
|
|
178
|
+
onTrace: trace,
|
|
179
|
+
onThreadOpened: (threadId, cwd) => this.handleVisibleCodexThreadOpened(threadId, cwd, trace),
|
|
180
|
+
onVisibleTurn: (threadId) => this.handleVisibleCodexTurn(threadId, trace),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
await this.restoreInitialSharedThreadIfNeeded();
|
|
162
184
|
if (this.isNativePanelMode()) {
|
|
163
185
|
await this.startNativeClient();
|
|
164
186
|
}
|
|
@@ -182,6 +204,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
182
204
|
throw err;
|
|
183
205
|
}
|
|
184
206
|
}
|
|
207
|
+
async prepareVisibleClientSession() {
|
|
208
|
+
if (this.isNativePanelMode() || this.sharedThreadId) {
|
|
209
|
+
return Boolean(this.sharedThreadId);
|
|
210
|
+
}
|
|
211
|
+
const threadId = await this.ensureThreadStarted();
|
|
212
|
+
return Boolean(threadId);
|
|
213
|
+
}
|
|
185
214
|
buildSpawnArgs() {
|
|
186
215
|
if (!this.appServerPort) {
|
|
187
216
|
throw new Error("Codex app-server is not ready.");
|
|
@@ -248,6 +277,76 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
248
277
|
}
|
|
249
278
|
await this.resumeVisibleSharedThread(threadId);
|
|
250
279
|
}
|
|
280
|
+
async listModels() {
|
|
281
|
+
if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
|
|
282
|
+
throw new Error("Codex model selection requires an active app-server thread.");
|
|
283
|
+
}
|
|
284
|
+
const response = await this.sendRpcRequest("model/list", { includeHidden: false });
|
|
285
|
+
const data = isRecord(response) && Array.isArray(response.data) ? response.data : [];
|
|
286
|
+
const current = await this.readCodexThreadSettings();
|
|
287
|
+
const models = data.flatMap((item) => {
|
|
288
|
+
if (!isRecord(item))
|
|
289
|
+
return [];
|
|
290
|
+
const id = typeof item.id === "string" ? item.id : typeof item.model === "string" ? item.model : "";
|
|
291
|
+
if (!id)
|
|
292
|
+
return [];
|
|
293
|
+
const displayName = typeof item.displayName === "string" ? item.displayName : id;
|
|
294
|
+
const efforts = Array.isArray(item.supportedReasoningEfforts)
|
|
295
|
+
? item.supportedReasoningEfforts.filter((value) => typeof value === "string")
|
|
296
|
+
: undefined;
|
|
297
|
+
return [{ id, displayName, isCurrent: id === current.model, supportedReasoningEfforts: efforts }];
|
|
298
|
+
});
|
|
299
|
+
return models;
|
|
300
|
+
}
|
|
301
|
+
async selectModel(modelId) {
|
|
302
|
+
this.assertVisibleCodexWorkspace();
|
|
303
|
+
const models = await this.listModels();
|
|
304
|
+
const selected = models.find((model) => model.id === modelId);
|
|
305
|
+
if (!selected)
|
|
306
|
+
throw new Error(`Codex model ${modelId} is not available.`);
|
|
307
|
+
await this.sendRpcRequest("thread/settings/update", { threadId: this.sharedThreadId, model: selected.id });
|
|
308
|
+
return selected;
|
|
309
|
+
}
|
|
310
|
+
async setPlanMode(enabled) {
|
|
311
|
+
this.assertVisibleCodexWorkspace();
|
|
312
|
+
if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
|
|
313
|
+
throw new Error("Codex plan mode requires an active app-server thread.");
|
|
314
|
+
}
|
|
315
|
+
const settings = await this.readCodexThreadSettings();
|
|
316
|
+
if (enabled) {
|
|
317
|
+
const response = await this.sendRpcRequest("collaborationMode/list", {});
|
|
318
|
+
const modes = isRecord(response) && Array.isArray(response.data) ? response.data : [];
|
|
319
|
+
const plan = modes.find((item) => isRecord(item) && (item.mode === "plan" || item.name?.toString().toLowerCase() === "plan"));
|
|
320
|
+
if (!isRecord(plan))
|
|
321
|
+
throw new Error("Codex plan mode is not available in this app-server.");
|
|
322
|
+
this.previousCollaborationMode = isRecord(settings.collaborationMode) ? settings.collaborationMode : null;
|
|
323
|
+
const mode = typeof plan.mode === "string" ? plan.mode : "plan";
|
|
324
|
+
const model = typeof plan.model === "string" ? plan.model : settings.model;
|
|
325
|
+
const effort = plan.reasoning_effort ?? plan.reasoningEffort ?? null;
|
|
326
|
+
await this.sendRpcRequest("thread/settings/update", {
|
|
327
|
+
threadId: this.sharedThreadId,
|
|
328
|
+
collaborationMode: { mode, settings: { model, reasoning_effort: effort, developer_instructions: null } },
|
|
329
|
+
});
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
const previous = this.previousCollaborationMode ?? {
|
|
333
|
+
mode: "default",
|
|
334
|
+
settings: { model: settings.model, reasoning_effort: settings.effort ?? null, developer_instructions: null },
|
|
335
|
+
};
|
|
336
|
+
await this.sendRpcRequest("thread/settings/update", {
|
|
337
|
+
threadId: this.sharedThreadId,
|
|
338
|
+
collaborationMode: previous,
|
|
339
|
+
});
|
|
340
|
+
this.previousCollaborationMode = null;
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
async readCodexThreadSettings() {
|
|
344
|
+
const response = await this.sendRpcRequest("thread/read", { threadId: this.sharedThreadId, includeTurns: false });
|
|
345
|
+
const thread = isRecord(response) && isRecord(response.thread) ? response.thread : null;
|
|
346
|
+
if (!thread || typeof thread.model !== "string")
|
|
347
|
+
throw new Error("Codex did not return the current thread model.");
|
|
348
|
+
return { model: thread.model, effort: thread.reasoningEffort, collaborationMode: thread.collaborationMode };
|
|
349
|
+
}
|
|
251
350
|
async interrupt() {
|
|
252
351
|
if (this.usesRpcTurnTransport()) {
|
|
253
352
|
return await this.interruptPanelTurn();
|
|
@@ -315,6 +414,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
315
414
|
return true;
|
|
316
415
|
}
|
|
317
416
|
async dispose() {
|
|
417
|
+
await this.shutdownVisibleClient();
|
|
318
418
|
this.resetTurnTracking({ preserveThread: false });
|
|
319
419
|
if (this.isEmbeddedCliMode()) {
|
|
320
420
|
this.detachLocalInputForwarding();
|
|
@@ -346,6 +446,25 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
346
446
|
}
|
|
347
447
|
await this.stopAppServer();
|
|
348
448
|
}
|
|
449
|
+
async shutdownVisibleClient() {
|
|
450
|
+
const endpoint = readLocalCompanionEndpoint(this.options.cwd, { adapter: "codex" });
|
|
451
|
+
if (!endpoint?.companionPid ||
|
|
452
|
+
endpoint.instanceId !== this.localClientInstanceId ||
|
|
453
|
+
!endpoint.codexControlPort ||
|
|
454
|
+
!endpoint.codexControlToken) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
await requestCodexVisibleClientShutdown({
|
|
459
|
+
cwd: this.options.cwd,
|
|
460
|
+
instanceId: endpoint.instanceId,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// The visible client may already be exiting. Its endpoint is cleared
|
|
465
|
+
// by the supervisor when the child process finishes.
|
|
466
|
+
}
|
|
467
|
+
}
|
|
349
468
|
getLocalClientEndpoint() {
|
|
350
469
|
if (!this.isHeadlessRuntimeMode() || !this.appServerPort || !this.appServerAuthToken) {
|
|
351
470
|
return null;
|
|
@@ -360,7 +479,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
360
479
|
renderMode: "headless",
|
|
361
480
|
bridgeOwnerPid: process.pid,
|
|
362
481
|
serverPort: this.appServerPort,
|
|
363
|
-
serverUrl: `ws://${CODEX_APP_SERVER_HOST}:${this.appServerPort}`,
|
|
482
|
+
serverUrl: this.visibleThreadProxy?.url ?? `ws://${CODEX_APP_SERVER_HOST}:${this.appServerPort}`,
|
|
364
483
|
remoteAuthTokenEnv: CODEX_REMOTE_AUTH_TOKEN_ENV,
|
|
365
484
|
cwd: this.options.cwd,
|
|
366
485
|
command: this.options.command,
|
|
@@ -916,6 +1035,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
916
1035
|
}
|
|
917
1036
|
}
|
|
918
1037
|
async sendPanelTurn(text) {
|
|
1038
|
+
this.assertVisibleCodexWorkspace();
|
|
919
1039
|
if (this.isNativePanelMode() && !this.nativeProcess) {
|
|
920
1040
|
throw new Error("codex panel is not running.");
|
|
921
1041
|
}
|
|
@@ -1064,6 +1184,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1064
1184
|
});
|
|
1065
1185
|
this.appServer = null;
|
|
1066
1186
|
this.appServerPort = null;
|
|
1187
|
+
void this.visibleThreadProxy?.close();
|
|
1188
|
+
this.visibleThreadProxy = null;
|
|
1067
1189
|
this.appServerShuttingDown = false;
|
|
1068
1190
|
this.deleteAppServerAuthTokenFile();
|
|
1069
1191
|
this.appServerAuthToken = null;
|
|
@@ -1086,6 +1208,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1086
1208
|
});
|
|
1087
1209
|
this.appServer = null;
|
|
1088
1210
|
this.appServerPort = null;
|
|
1211
|
+
void this.visibleThreadProxy?.close();
|
|
1212
|
+
this.visibleThreadProxy = null;
|
|
1089
1213
|
this.appServerShuttingDown = false;
|
|
1090
1214
|
this.deleteAppServerAuthTokenFile();
|
|
1091
1215
|
this.appServerAuthToken = null;
|
|
@@ -1192,8 +1316,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1192
1316
|
socket.addEventListener("message", (event) => {
|
|
1193
1317
|
this.handleRpcMessageData(event.data);
|
|
1194
1318
|
});
|
|
1195
|
-
socket.addEventListener("close", () => {
|
|
1196
|
-
this.handleRpcSocketClosed();
|
|
1319
|
+
socket.addEventListener("close", (event) => {
|
|
1320
|
+
this.handleRpcSocketClosed(event.code, event.reason);
|
|
1197
1321
|
});
|
|
1198
1322
|
}
|
|
1199
1323
|
async disconnectRpcClient() {
|
|
@@ -1231,13 +1355,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1231
1355
|
});
|
|
1232
1356
|
this.rpcShuttingDown = false;
|
|
1233
1357
|
}
|
|
1234
|
-
handleRpcSocketClosed() {
|
|
1358
|
+
handleRpcSocketClosed(code, reason) {
|
|
1235
1359
|
const expectedShutdown = shouldSuppressCodexTransportFatalError({
|
|
1236
1360
|
transportShuttingDown: this.rpcShuttingDown,
|
|
1237
1361
|
shuttingDown: this.shuttingDown,
|
|
1238
1362
|
cleanPanelExitInProgress: this.cleanPanelExitInProgress,
|
|
1239
1363
|
});
|
|
1240
1364
|
this.rpcSocket = null;
|
|
1365
|
+
this.lastRpcCloseDetail = `close_code=${code ?? "unknown"}${reason ? ` reason=${truncatePreview(reason, 240)}` : ""}`;
|
|
1241
1366
|
this.subscribedThreadIds.clear();
|
|
1242
1367
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
1243
1368
|
this.rpcShuttingDown = false;
|
|
@@ -1269,7 +1394,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1269
1394
|
const details = this.describeAppServerLog();
|
|
1270
1395
|
this.emit({
|
|
1271
1396
|
type: "fatal_error",
|
|
1272
|
-
message: `codex app-server websocket closed unexpectedly.${details}`,
|
|
1397
|
+
message: `codex app-server websocket closed unexpectedly (${this.lastRpcCloseDetail || "no close details"}).${details}`,
|
|
1273
1398
|
timestamp: nowIso(),
|
|
1274
1399
|
});
|
|
1275
1400
|
this.terminateCodexClient();
|
|
@@ -1307,7 +1432,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1307
1432
|
}
|
|
1308
1433
|
this.emit({
|
|
1309
1434
|
type: "fatal_error",
|
|
1310
|
-
message: `codex app-server websocket closed unexpectedly and could not reconnect: ${lastError}.${details}`,
|
|
1435
|
+
message: `codex app-server websocket closed unexpectedly and could not reconnect (${this.lastRpcCloseDetail || "no close details"}): ${lastError}.${details}`,
|
|
1311
1436
|
timestamp: nowIso(),
|
|
1312
1437
|
});
|
|
1313
1438
|
this.terminateCodexClient();
|
|
@@ -1460,6 +1585,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1460
1585
|
if (typeof thread.parentThreadId === "string") {
|
|
1461
1586
|
throw new Error(`Codex thread ${targetThreadId} is a subagent thread and cannot be resumed.`);
|
|
1462
1587
|
}
|
|
1588
|
+
if (thread.canAcceptDirectInput === false) {
|
|
1589
|
+
throw new Error(`Codex thread ${targetThreadId} is owned by a parent agent and cannot accept direct input.`);
|
|
1590
|
+
}
|
|
1463
1591
|
if (thread.ephemeral === true) {
|
|
1464
1592
|
throw new Error(`Codex thread ${targetThreadId} is ephemeral and cannot be resumed.`);
|
|
1465
1593
|
}
|
|
@@ -1483,6 +1611,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1483
1611
|
if (typeof thread.parentThreadId === "string") {
|
|
1484
1612
|
throw new Error(`Codex thread ${targetThreadId} is a subagent thread.`);
|
|
1485
1613
|
}
|
|
1614
|
+
if (thread.canAcceptDirectInput === false) {
|
|
1615
|
+
throw new Error(`Codex thread ${targetThreadId} is owned by a parent agent.`);
|
|
1616
|
+
}
|
|
1486
1617
|
if (thread.ephemeral === true) {
|
|
1487
1618
|
throw new Error(`Codex thread ${targetThreadId} is ephemeral.`);
|
|
1488
1619
|
}
|
|
@@ -1549,6 +1680,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1549
1680
|
reason: "wechat_resume",
|
|
1550
1681
|
notify: true,
|
|
1551
1682
|
});
|
|
1683
|
+
this.clearVisibleCodexWorkspaceBlock();
|
|
1552
1684
|
this.bridgeResumeReplayThreadId = targetThreadId;
|
|
1553
1685
|
this.bridgeResumeReplayUntilMs = Date.now() + CODEX_RESUME_REPLAY_SETTLE_MS;
|
|
1554
1686
|
this.setStatus("starting", `Codex resume committed thread ${targetThreadId.slice(0, 12)}.`);
|
|
@@ -1770,6 +1902,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1770
1902
|
if (!options.preserveThread) {
|
|
1771
1903
|
this.clearPendingThreadAnnouncement();
|
|
1772
1904
|
this.announcedThreadId = null;
|
|
1905
|
+
this.visibleOutOfWorkspaceThreadId = null;
|
|
1773
1906
|
}
|
|
1774
1907
|
if (!options.preserveThread) {
|
|
1775
1908
|
this.updateSharedThread(null);
|
|
@@ -1889,6 +2022,62 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1889
2022
|
}, CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS);
|
|
1890
2023
|
pending.timer.unref?.();
|
|
1891
2024
|
}
|
|
2025
|
+
handleVisibleCodexThreadOpened(threadId, cwd, trace) {
|
|
2026
|
+
if (this.pendingVisibleResume || this.shuttingDown) {
|
|
2027
|
+
trace?.(`ignored thread=${threadId.slice(0, 12)} reason=bridge_switch`);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
if (cwd && normalizeComparablePath(cwd) !== normalizeComparablePath(this.options.cwd)) {
|
|
2031
|
+
trace?.(`paused thread=${threadId.slice(0, 12)} reason=workspace`);
|
|
2032
|
+
this.interruptWechatTurnForLocalThreadSwitch(threadId);
|
|
2033
|
+
this.clearPendingThreadAnnouncement();
|
|
2034
|
+
this.pendingThreadFollowId = null;
|
|
2035
|
+
if (this.visibleOutOfWorkspaceThreadId !== threadId) {
|
|
2036
|
+
this.visibleOutOfWorkspaceThreadId = threadId;
|
|
2037
|
+
this.emit({
|
|
2038
|
+
type: "notice",
|
|
2039
|
+
level: "warning",
|
|
2040
|
+
text: `The visible Codex terminal switched to ${cwd}, outside the bridge workspace ${this.options.cwd}. WeChat input is paused instead of being sent to the previous session. Resume a session from the bridge workspace, or restart the bridge in ${cwd}.`,
|
|
2041
|
+
timestamp: nowIso(),
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
this.clearVisibleCodexWorkspaceBlock();
|
|
2047
|
+
trace?.(`follow thread=${threadId.slice(0, 12)} source=visible_open`);
|
|
2048
|
+
this.trackLocalSharedThread(threadId, { reason: "local_follow", signal: "visible_request" });
|
|
2049
|
+
}
|
|
2050
|
+
handleVisibleCodexTurn(threadId, trace) {
|
|
2051
|
+
if (this.pendingVisibleResume || this.shuttingDown)
|
|
2052
|
+
return;
|
|
2053
|
+
if (threadId === this.sharedThreadId) {
|
|
2054
|
+
this.clearVisibleCodexWorkspaceBlock();
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
if (!findCodexSessionFile(this.options.cwd, Date.now(), { threadId })) {
|
|
2058
|
+
trace?.(`ignored thread=${threadId.slice(0, 12)} reason=workspace_or_unpersisted`);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
this.clearVisibleCodexWorkspaceBlock();
|
|
2062
|
+
trace?.(`follow thread=${threadId.slice(0, 12)} source=visible_turn`);
|
|
2063
|
+
this.trackLocalSharedThread(threadId, { reason: "local_follow", signal: "visible_request" });
|
|
2064
|
+
}
|
|
2065
|
+
clearVisibleCodexWorkspaceBlock() {
|
|
2066
|
+
if (!this.visibleOutOfWorkspaceThreadId)
|
|
2067
|
+
return;
|
|
2068
|
+
this.visibleOutOfWorkspaceThreadId = null;
|
|
2069
|
+
this.emit({
|
|
2070
|
+
type: "notice",
|
|
2071
|
+
level: "info",
|
|
2072
|
+
text: `The visible Codex terminal is back in the bridge workspace ${this.options.cwd}. WeChat input has resumed.`,
|
|
2073
|
+
timestamp: nowIso(),
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
assertVisibleCodexWorkspace() {
|
|
2077
|
+
if (this.visibleOutOfWorkspaceThreadId) {
|
|
2078
|
+
throw new Error(`The visible Codex terminal is outside the bridge workspace ${this.options.cwd}. WeChat input is paused; resume a session from this workspace or restart the bridge in the terminal's workspace.`);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
1892
2081
|
trackLocalSharedThread(threadId, options) {
|
|
1893
2082
|
const weakSignal = options.signal === "status_changed" ||
|
|
1894
2083
|
options.signal === "thread_started" ||
|
|
@@ -1896,7 +2085,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1896
2085
|
if (weakSignal && Date.now() < this.localThreadFollowBlockedUntilMs) {
|
|
1897
2086
|
return;
|
|
1898
2087
|
}
|
|
1899
|
-
if (options.signal === "user_message" || options.signal === "turn_started") {
|
|
2088
|
+
if (options.signal === "user_message" || options.signal === "turn_started" || options.signal === "visible_request") {
|
|
1900
2089
|
this.localThreadFollowBlockedUntilMs = 0;
|
|
1901
2090
|
}
|
|
1902
2091
|
if (this.pendingVisibleResume) {
|
|
@@ -2232,7 +2421,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2232
2421
|
origin: "local",
|
|
2233
2422
|
};
|
|
2234
2423
|
}
|
|
2235
|
-
if (method === "turn/started" && !this.activeTurn) {
|
|
2424
|
+
if (method === "turn/started" && !this.activeTurn && !this.visibleThreadProxy) {
|
|
2236
2425
|
return {
|
|
2237
2426
|
threadId,
|
|
2238
2427
|
turnId,
|
|
@@ -2506,7 +2695,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2506
2695
|
if (!threadId) {
|
|
2507
2696
|
return;
|
|
2508
2697
|
}
|
|
2509
|
-
if (this.pendingVisibleResume) {
|
|
2698
|
+
if (this.pendingVisibleResume || this.visibleThreadProxy) {
|
|
2510
2699
|
return;
|
|
2511
2700
|
}
|
|
2512
2701
|
if (this.isBridgeResumeReplay(threadId)) {
|
|
@@ -2585,7 +2774,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2585
2774
|
if (!threadId) {
|
|
2586
2775
|
return;
|
|
2587
2776
|
}
|
|
2588
|
-
if (this.isBridgeResumeReplay(threadId)) {
|
|
2777
|
+
if (this.visibleThreadProxy || this.isBridgeResumeReplay(threadId)) {
|
|
2589
2778
|
return;
|
|
2590
2779
|
}
|
|
2591
2780
|
if (this.isRecentlyBridgeOwnedThread(threadId)) {
|
|
@@ -2863,6 +3052,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2863
3052
|
this.rememberCompletedTurn(turnId);
|
|
2864
3053
|
}
|
|
2865
3054
|
async stopAppServer() {
|
|
3055
|
+
const visibleThreadProxy = this.visibleThreadProxy;
|
|
3056
|
+
this.visibleThreadProxy = null;
|
|
3057
|
+
await visibleThreadProxy?.close();
|
|
2866
3058
|
if (!this.appServer) {
|
|
2867
3059
|
this.appServerPort = null;
|
|
2868
3060
|
this.appServerShuttingDown = false;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import net from "node:net";
|
|
2
8
|
import path from "node:path";
|
|
3
9
|
import { spawn as spawnPty } from "node-pty";
|
|
@@ -15,7 +21,7 @@ const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, b
|
|
|
15
21
|
* the script path contains spaces: node-pty quotes the already-quoted path,
|
|
16
22
|
* making cmd.exe treat the literal quotes as part of the filename.
|
|
17
23
|
*/
|
|
18
|
-
function unwrapCmdExeForPty(rawTarget) {
|
|
24
|
+
export function unwrapCmdExeForPty(rawTarget) {
|
|
19
25
|
if (process.platform !== "win32") {
|
|
20
26
|
return rawTarget;
|
|
21
27
|
}
|
|
@@ -166,6 +172,15 @@ export class LocalCompanionProxyAdapter {
|
|
|
166
172
|
command: "create_session",
|
|
167
173
|
});
|
|
168
174
|
}
|
|
175
|
+
async listModels() {
|
|
176
|
+
return await this.sendRequest({ command: "list_models" });
|
|
177
|
+
}
|
|
178
|
+
async selectModel(modelId) {
|
|
179
|
+
return await this.sendRequest({ command: "select_model", modelId });
|
|
180
|
+
}
|
|
181
|
+
async setPlanMode(enabled) {
|
|
182
|
+
return await this.sendRequest({ command: "set_plan_mode", enabled });
|
|
183
|
+
}
|
|
169
184
|
async interrupt() {
|
|
170
185
|
const result = await this.sendRequest({
|
|
171
186
|
command: "interrupt",
|
|
@@ -612,6 +627,15 @@ export class AbstractPtyAdapter {
|
|
|
612
627
|
setEventSink(sink) {
|
|
613
628
|
this.eventSink = sink;
|
|
614
629
|
}
|
|
630
|
+
async listModels() {
|
|
631
|
+
throw new Error(`/${this.options.kind} model selection is not available.`);
|
|
632
|
+
}
|
|
633
|
+
async selectModel(_modelId) {
|
|
634
|
+
throw new Error(`/${this.options.kind} model selection is not available.`);
|
|
635
|
+
}
|
|
636
|
+
async setPlanMode(_enabled) {
|
|
637
|
+
throw new Error(`/${this.options.kind} plan mode is not available.`);
|
|
638
|
+
}
|
|
615
639
|
async start() {
|
|
616
640
|
if (this.pty) {
|
|
617
641
|
return;
|