@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.0
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 +467 -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 +118 -7
- package/dist/bridge/bridge-adapters.core.js +25 -1
- package/dist/bridge/bridge-adapters.opencode.js +156 -1
- 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 +133 -0
- package/dist/bridge/native-terminal-control.js +259 -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/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 +13 -3
- 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";
|
|
@@ -7,7 +13,8 @@ import { normalizeOutput, nowIso, summarizeOutput, truncatePreview, } from "../c
|
|
|
7
13
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
8
14
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
9
15
|
import * as shared from "./bridge-adapters.shared.js";
|
|
10
|
-
import { requestCodexVisibleThreadSwitch } from "../companion/codex-visible-client-link.js";
|
|
16
|
+
import { requestCodexVisibleClientShutdown, requestCodexVisibleThreadSwitch, } from "../companion/codex-visible-client-link.js";
|
|
17
|
+
import { readLocalCompanionEndpoint } from "../companion/local-companion-link.js";
|
|
11
18
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
12
19
|
import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
13
20
|
export function parseCodexCliVersion(output) {
|
|
@@ -21,7 +28,7 @@ export function isCodexVersionInCompatibilityRange(version) {
|
|
|
21
28
|
}
|
|
22
29
|
const major = parts[0] ?? -1;
|
|
23
30
|
const minor = parts[1] ?? -1;
|
|
24
|
-
return major === 0 && minor >= 149 && minor <=
|
|
31
|
+
return major === 0 && minor >= 149 && minor <= 155;
|
|
25
32
|
}
|
|
26
33
|
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
34
|
const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
|
|
@@ -78,6 +85,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
78
85
|
appServerAuthToken = null;
|
|
79
86
|
appServerAuthTokenFilePath = null;
|
|
80
87
|
rpcSocket = null;
|
|
88
|
+
lastRpcCloseDetail = "";
|
|
81
89
|
rpcShuttingDown = false;
|
|
82
90
|
rpcReconnectPromise = null;
|
|
83
91
|
cleanPanelExitInProgress = false;
|
|
@@ -86,6 +94,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
86
94
|
subscribedThreadIds = new Set();
|
|
87
95
|
pendingThreadSubscriptions = new Map();
|
|
88
96
|
sharedThreadId = null;
|
|
97
|
+
previousCollaborationMode = null;
|
|
89
98
|
announcedThreadId = null;
|
|
90
99
|
localThreadFollowBlockedUntilMs = 0;
|
|
91
100
|
pendingThreadStatusChecks = new Map();
|
|
@@ -182,6 +191,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
182
191
|
throw err;
|
|
183
192
|
}
|
|
184
193
|
}
|
|
194
|
+
async prepareVisibleClientSession() {
|
|
195
|
+
if (this.isNativePanelMode() || this.sharedThreadId) {
|
|
196
|
+
return Boolean(this.sharedThreadId);
|
|
197
|
+
}
|
|
198
|
+
const threadId = await this.ensureThreadStarted();
|
|
199
|
+
return Boolean(threadId);
|
|
200
|
+
}
|
|
185
201
|
buildSpawnArgs() {
|
|
186
202
|
if (!this.appServerPort) {
|
|
187
203
|
throw new Error("Codex app-server is not ready.");
|
|
@@ -248,6 +264,74 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
248
264
|
}
|
|
249
265
|
await this.resumeVisibleSharedThread(threadId);
|
|
250
266
|
}
|
|
267
|
+
async listModels() {
|
|
268
|
+
if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
|
|
269
|
+
throw new Error("Codex model selection requires an active app-server thread.");
|
|
270
|
+
}
|
|
271
|
+
const response = await this.sendRpcRequest("model/list", { includeHidden: false });
|
|
272
|
+
const data = isRecord(response) && Array.isArray(response.data) ? response.data : [];
|
|
273
|
+
const current = await this.readCodexThreadSettings();
|
|
274
|
+
const models = data.flatMap((item) => {
|
|
275
|
+
if (!isRecord(item))
|
|
276
|
+
return [];
|
|
277
|
+
const id = typeof item.id === "string" ? item.id : typeof item.model === "string" ? item.model : "";
|
|
278
|
+
if (!id)
|
|
279
|
+
return [];
|
|
280
|
+
const displayName = typeof item.displayName === "string" ? item.displayName : id;
|
|
281
|
+
const efforts = Array.isArray(item.supportedReasoningEfforts)
|
|
282
|
+
? item.supportedReasoningEfforts.filter((value) => typeof value === "string")
|
|
283
|
+
: undefined;
|
|
284
|
+
return [{ id, displayName, isCurrent: id === current.model, supportedReasoningEfforts: efforts }];
|
|
285
|
+
});
|
|
286
|
+
return models;
|
|
287
|
+
}
|
|
288
|
+
async selectModel(modelId) {
|
|
289
|
+
const models = await this.listModels();
|
|
290
|
+
const selected = models.find((model) => model.id === modelId);
|
|
291
|
+
if (!selected)
|
|
292
|
+
throw new Error(`Codex model ${modelId} is not available.`);
|
|
293
|
+
await this.sendRpcRequest("thread/settings/update", { threadId: this.sharedThreadId, model: selected.id });
|
|
294
|
+
return selected;
|
|
295
|
+
}
|
|
296
|
+
async setPlanMode(enabled) {
|
|
297
|
+
if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
|
|
298
|
+
throw new Error("Codex plan mode requires an active app-server thread.");
|
|
299
|
+
}
|
|
300
|
+
const settings = await this.readCodexThreadSettings();
|
|
301
|
+
if (enabled) {
|
|
302
|
+
const response = await this.sendRpcRequest("collaborationMode/list", {});
|
|
303
|
+
const modes = isRecord(response) && Array.isArray(response.data) ? response.data : [];
|
|
304
|
+
const plan = modes.find((item) => isRecord(item) && (item.mode === "plan" || item.name?.toString().toLowerCase() === "plan"));
|
|
305
|
+
if (!isRecord(plan))
|
|
306
|
+
throw new Error("Codex plan mode is not available in this app-server.");
|
|
307
|
+
this.previousCollaborationMode = isRecord(settings.collaborationMode) ? settings.collaborationMode : null;
|
|
308
|
+
const mode = typeof plan.mode === "string" ? plan.mode : "plan";
|
|
309
|
+
const model = typeof plan.model === "string" ? plan.model : settings.model;
|
|
310
|
+
const effort = plan.reasoning_effort ?? plan.reasoningEffort ?? null;
|
|
311
|
+
await this.sendRpcRequest("thread/settings/update", {
|
|
312
|
+
threadId: this.sharedThreadId,
|
|
313
|
+
collaborationMode: { mode, settings: { model, reasoning_effort: effort, developer_instructions: null } },
|
|
314
|
+
});
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
const previous = this.previousCollaborationMode ?? {
|
|
318
|
+
mode: "default",
|
|
319
|
+
settings: { model: settings.model, reasoning_effort: settings.effort ?? null, developer_instructions: null },
|
|
320
|
+
};
|
|
321
|
+
await this.sendRpcRequest("thread/settings/update", {
|
|
322
|
+
threadId: this.sharedThreadId,
|
|
323
|
+
collaborationMode: previous,
|
|
324
|
+
});
|
|
325
|
+
this.previousCollaborationMode = null;
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
async readCodexThreadSettings() {
|
|
329
|
+
const response = await this.sendRpcRequest("thread/read", { threadId: this.sharedThreadId, includeTurns: false });
|
|
330
|
+
const thread = isRecord(response) && isRecord(response.thread) ? response.thread : null;
|
|
331
|
+
if (!thread || typeof thread.model !== "string")
|
|
332
|
+
throw new Error("Codex did not return the current thread model.");
|
|
333
|
+
return { model: thread.model, effort: thread.reasoningEffort, collaborationMode: thread.collaborationMode };
|
|
334
|
+
}
|
|
251
335
|
async interrupt() {
|
|
252
336
|
if (this.usesRpcTurnTransport()) {
|
|
253
337
|
return await this.interruptPanelTurn();
|
|
@@ -315,6 +399,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
315
399
|
return true;
|
|
316
400
|
}
|
|
317
401
|
async dispose() {
|
|
402
|
+
await this.shutdownVisibleClient();
|
|
318
403
|
this.resetTurnTracking({ preserveThread: false });
|
|
319
404
|
if (this.isEmbeddedCliMode()) {
|
|
320
405
|
this.detachLocalInputForwarding();
|
|
@@ -346,6 +431,25 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
346
431
|
}
|
|
347
432
|
await this.stopAppServer();
|
|
348
433
|
}
|
|
434
|
+
async shutdownVisibleClient() {
|
|
435
|
+
const endpoint = readLocalCompanionEndpoint(this.options.cwd, { adapter: "codex" });
|
|
436
|
+
if (!endpoint?.companionPid ||
|
|
437
|
+
endpoint.instanceId !== this.localClientInstanceId ||
|
|
438
|
+
!endpoint.codexControlPort ||
|
|
439
|
+
!endpoint.codexControlToken) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
await requestCodexVisibleClientShutdown({
|
|
444
|
+
cwd: this.options.cwd,
|
|
445
|
+
instanceId: endpoint.instanceId,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// The visible client may already be exiting. Its endpoint is cleared
|
|
450
|
+
// by the supervisor when the child process finishes.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
349
453
|
getLocalClientEndpoint() {
|
|
350
454
|
if (!this.isHeadlessRuntimeMode() || !this.appServerPort || !this.appServerAuthToken) {
|
|
351
455
|
return null;
|
|
@@ -1192,8 +1296,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1192
1296
|
socket.addEventListener("message", (event) => {
|
|
1193
1297
|
this.handleRpcMessageData(event.data);
|
|
1194
1298
|
});
|
|
1195
|
-
socket.addEventListener("close", () => {
|
|
1196
|
-
this.handleRpcSocketClosed();
|
|
1299
|
+
socket.addEventListener("close", (event) => {
|
|
1300
|
+
this.handleRpcSocketClosed(event.code, event.reason);
|
|
1197
1301
|
});
|
|
1198
1302
|
}
|
|
1199
1303
|
async disconnectRpcClient() {
|
|
@@ -1231,13 +1335,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1231
1335
|
});
|
|
1232
1336
|
this.rpcShuttingDown = false;
|
|
1233
1337
|
}
|
|
1234
|
-
handleRpcSocketClosed() {
|
|
1338
|
+
handleRpcSocketClosed(code, reason) {
|
|
1235
1339
|
const expectedShutdown = shouldSuppressCodexTransportFatalError({
|
|
1236
1340
|
transportShuttingDown: this.rpcShuttingDown,
|
|
1237
1341
|
shuttingDown: this.shuttingDown,
|
|
1238
1342
|
cleanPanelExitInProgress: this.cleanPanelExitInProgress,
|
|
1239
1343
|
});
|
|
1240
1344
|
this.rpcSocket = null;
|
|
1345
|
+
this.lastRpcCloseDetail = `close_code=${code ?? "unknown"}${reason ? ` reason=${truncatePreview(reason, 240)}` : ""}`;
|
|
1241
1346
|
this.subscribedThreadIds.clear();
|
|
1242
1347
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
1243
1348
|
this.rpcShuttingDown = false;
|
|
@@ -1269,7 +1374,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1269
1374
|
const details = this.describeAppServerLog();
|
|
1270
1375
|
this.emit({
|
|
1271
1376
|
type: "fatal_error",
|
|
1272
|
-
message: `codex app-server websocket closed unexpectedly.${details}`,
|
|
1377
|
+
message: `codex app-server websocket closed unexpectedly (${this.lastRpcCloseDetail || "no close details"}).${details}`,
|
|
1273
1378
|
timestamp: nowIso(),
|
|
1274
1379
|
});
|
|
1275
1380
|
this.terminateCodexClient();
|
|
@@ -1307,7 +1412,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1307
1412
|
}
|
|
1308
1413
|
this.emit({
|
|
1309
1414
|
type: "fatal_error",
|
|
1310
|
-
message: `codex app-server websocket closed unexpectedly and could not reconnect: ${lastError}.${details}`,
|
|
1415
|
+
message: `codex app-server websocket closed unexpectedly and could not reconnect (${this.lastRpcCloseDetail || "no close details"}): ${lastError}.${details}`,
|
|
1311
1416
|
timestamp: nowIso(),
|
|
1312
1417
|
});
|
|
1313
1418
|
this.terminateCodexClient();
|
|
@@ -1460,6 +1565,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1460
1565
|
if (typeof thread.parentThreadId === "string") {
|
|
1461
1566
|
throw new Error(`Codex thread ${targetThreadId} is a subagent thread and cannot be resumed.`);
|
|
1462
1567
|
}
|
|
1568
|
+
if (thread.canAcceptDirectInput === false) {
|
|
1569
|
+
throw new Error(`Codex thread ${targetThreadId} is owned by a parent agent and cannot accept direct input.`);
|
|
1570
|
+
}
|
|
1463
1571
|
if (thread.ephemeral === true) {
|
|
1464
1572
|
throw new Error(`Codex thread ${targetThreadId} is ephemeral and cannot be resumed.`);
|
|
1465
1573
|
}
|
|
@@ -1483,6 +1591,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1483
1591
|
if (typeof thread.parentThreadId === "string") {
|
|
1484
1592
|
throw new Error(`Codex thread ${targetThreadId} is a subagent thread.`);
|
|
1485
1593
|
}
|
|
1594
|
+
if (thread.canAcceptDirectInput === false) {
|
|
1595
|
+
throw new Error(`Codex thread ${targetThreadId} is owned by a parent agent.`);
|
|
1596
|
+
}
|
|
1486
1597
|
if (thread.ephemeral === true) {
|
|
1487
1598
|
throw new Error(`Codex thread ${targetThreadId} is ephemeral.`);
|
|
1488
1599
|
}
|
|
@@ -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;
|
|
@@ -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 { spawn as spawnChildProcess } from "node:child_process";
|
|
2
8
|
import { randomBytes } from "node:crypto";
|
|
3
9
|
import fs from "node:fs";
|
|
@@ -85,6 +91,139 @@ export class OpenCodeServerAdapter {
|
|
|
85
91
|
pendingLocalSessionCreateFollowUntilMs = 0;
|
|
86
92
|
pendingPermission = null;
|
|
87
93
|
pendingQuestion = null;
|
|
94
|
+
previousAgentBySession = new Map();
|
|
95
|
+
async listModels() {
|
|
96
|
+
this.assertRemoteControlReady();
|
|
97
|
+
const session = await this.ensureControlSession();
|
|
98
|
+
const models = await this.listControlModels();
|
|
99
|
+
return models.map((model) => ({
|
|
100
|
+
id: model.id,
|
|
101
|
+
displayName: model.displayName,
|
|
102
|
+
isCurrent: session.model?.providerID === model.providerID &&
|
|
103
|
+
session.model.id === model.modelID,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
async selectModel(modelId) {
|
|
107
|
+
this.assertRemoteControlReady();
|
|
108
|
+
const session = await this.ensureControlSession();
|
|
109
|
+
const models = await this.listControlModels();
|
|
110
|
+
const target = models.find((model) => model.id === modelId);
|
|
111
|
+
if (!target) {
|
|
112
|
+
throw new Error("This OpenCode model is no longer available. Send /model again.");
|
|
113
|
+
}
|
|
114
|
+
const currentVariant = session.model?.variant;
|
|
115
|
+
const variant = currentVariant && currentVariant !== "default" && target.variants.includes(currentVariant)
|
|
116
|
+
? currentVariant
|
|
117
|
+
: "default";
|
|
118
|
+
const result = await this.client.v2.session.switchModel({
|
|
119
|
+
sessionID: session.id,
|
|
120
|
+
model: {
|
|
121
|
+
providerID: target.providerID,
|
|
122
|
+
id: target.modelID,
|
|
123
|
+
variant,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
this.unwrapOrThrow(result);
|
|
127
|
+
const updated = await this.getSessionForCurrentDirectory(session.id);
|
|
128
|
+
if (!updated ||
|
|
129
|
+
updated.model?.providerID !== target.providerID ||
|
|
130
|
+
updated.model.id !== target.modelID ||
|
|
131
|
+
(updated.model.variant ?? "default") !== variant) {
|
|
132
|
+
throw new Error("OpenCode did not confirm the selected model.");
|
|
133
|
+
}
|
|
134
|
+
return { id: target.id, displayName: target.displayName, isCurrent: true };
|
|
135
|
+
}
|
|
136
|
+
async setPlanMode(enabled) {
|
|
137
|
+
this.assertRemoteControlReady();
|
|
138
|
+
const session = await this.ensureControlSession();
|
|
139
|
+
const agents = await this.listPrimaryAgents();
|
|
140
|
+
const planAgent = agents.find((agent) => agent.name === "plan");
|
|
141
|
+
if (!planAgent) {
|
|
142
|
+
throw new Error("OpenCode primary agent 'plan' is not available.");
|
|
143
|
+
}
|
|
144
|
+
const currentAgent = session.agent ?? agents[0]?.name;
|
|
145
|
+
if (!currentAgent) {
|
|
146
|
+
throw new Error("OpenCode did not return any primary agents.");
|
|
147
|
+
}
|
|
148
|
+
if (enabled && currentAgent === "plan") {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (!enabled && currentAgent !== "plan") {
|
|
152
|
+
this.previousAgentBySession.delete(session.id);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const previousAgent = this.previousAgentBySession.get(session.id);
|
|
156
|
+
const target = enabled
|
|
157
|
+
? "plan"
|
|
158
|
+
: agents.some((agent) => agent.name === previousAgent)
|
|
159
|
+
? previousAgent
|
|
160
|
+
: agents.find((agent) => agent.name === "build")?.name ??
|
|
161
|
+
agents.find((agent) => agent.name !== "plan")?.name;
|
|
162
|
+
if (!target) {
|
|
163
|
+
throw new Error("OpenCode did not return an agent that can replace plan mode.");
|
|
164
|
+
}
|
|
165
|
+
const result = await this.client.v2.session.switchAgent({
|
|
166
|
+
sessionID: session.id,
|
|
167
|
+
agent: target,
|
|
168
|
+
});
|
|
169
|
+
this.unwrapOrThrow(result);
|
|
170
|
+
const updated = await this.getSessionForCurrentDirectory(session.id);
|
|
171
|
+
if (!updated || updated.agent !== target) {
|
|
172
|
+
throw new Error("OpenCode did not confirm the selected agent.");
|
|
173
|
+
}
|
|
174
|
+
if (enabled) {
|
|
175
|
+
this.previousAgentBySession.set(session.id, currentAgent);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.previousAgentBySession.delete(session.id);
|
|
179
|
+
}
|
|
180
|
+
return enabled;
|
|
181
|
+
}
|
|
182
|
+
assertRemoteControlReady() {
|
|
183
|
+
if (!this.client) {
|
|
184
|
+
throw new Error("OpenCode is not running.");
|
|
185
|
+
}
|
|
186
|
+
if (this.state.status === "busy") {
|
|
187
|
+
throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
|
|
188
|
+
}
|
|
189
|
+
if (this.pendingPermission) {
|
|
190
|
+
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
191
|
+
}
|
|
192
|
+
if (this.pendingQuestion) {
|
|
193
|
+
throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async ensureControlSession() {
|
|
197
|
+
const session = await this.ensureSession();
|
|
198
|
+
if (session.id !== this.activeSessionId) {
|
|
199
|
+
this.switchSharedSession(session, {
|
|
200
|
+
source: "wechat",
|
|
201
|
+
reason: "wechat_resume",
|
|
202
|
+
syncVisible: true,
|
|
203
|
+
forceVisibleSync: true,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return session;
|
|
207
|
+
}
|
|
208
|
+
async listPrimaryAgents() {
|
|
209
|
+
if (!this.client)
|
|
210
|
+
throw new Error("OpenCode is not running.");
|
|
211
|
+
const agents = this.unwrapOrThrow(await this.client.app.agents({
|
|
212
|
+
directory: this.options.cwd,
|
|
213
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
214
|
+
}, { signal: AbortSignal.timeout(10_000) }));
|
|
215
|
+
return agents.filter((agent) => agent.mode !== "subagent" && !agent.hidden);
|
|
216
|
+
}
|
|
217
|
+
async listControlModels() {
|
|
218
|
+
if (!this.client)
|
|
219
|
+
throw new Error("OpenCode is not running.");
|
|
220
|
+
const providers = this.unwrapOrThrow(await this.client.provider.list({ directory: this.options.cwd, workspace: this.activeWorkspaceId ?? undefined }, { signal: AbortSignal.timeout(10_000) }));
|
|
221
|
+
return providers.all.filter((provider) => providers.connected.includes(provider.id)).flatMap((provider) => Object.entries(provider.models).filter(([id, model]) => model.status !== "deprecated" && !(provider.id === "opencode" && id.includes("-nano"))).map(([id, model]) => ({
|
|
222
|
+
id: `${provider.id}/${id}`, displayName: `${model.name || id} (${provider.name})`,
|
|
223
|
+
providerID: provider.id, modelID: id,
|
|
224
|
+
variants: Object.keys(model.variants ?? {}),
|
|
225
|
+
})));
|
|
226
|
+
}
|
|
88
227
|
constructor(options) {
|
|
89
228
|
this.options = options;
|
|
90
229
|
this.state = {
|
|
@@ -170,10 +309,21 @@ export class OpenCodeServerAdapter {
|
|
|
170
309
|
this.recordPendingWechatPromptMirrorSuppression(session.id, normalized);
|
|
171
310
|
this.beginTrackedTurn(normalized, "wechat");
|
|
172
311
|
try {
|
|
312
|
+
const selection = await this.getSessionForCurrentDirectory(session.id) ?? session;
|
|
173
313
|
const result = await this.client.session.promptAsync({
|
|
174
314
|
sessionID: session.id,
|
|
175
315
|
directory: this.options.cwd,
|
|
176
316
|
workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
|
|
317
|
+
...(selection.agent ? { agent: selection.agent } : {}),
|
|
318
|
+
...(selection.model
|
|
319
|
+
? {
|
|
320
|
+
model: {
|
|
321
|
+
providerID: selection.model.providerID,
|
|
322
|
+
modelID: selection.model.id,
|
|
323
|
+
},
|
|
324
|
+
variant: selection.model.variant ?? "default",
|
|
325
|
+
}
|
|
326
|
+
: {}),
|
|
177
327
|
parts: [{ type: "text", text: normalized }],
|
|
178
328
|
});
|
|
179
329
|
if (result.error !== undefined) {
|
|
@@ -411,6 +561,7 @@ export class OpenCodeServerAdapter {
|
|
|
411
561
|
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
412
562
|
this.clearObservedMessageTracking();
|
|
413
563
|
this.recentSdkEventObservations.clear();
|
|
564
|
+
this.previousAgentBySession.clear();
|
|
414
565
|
this.outputBatcher.clear();
|
|
415
566
|
this.clearStreamedPartState();
|
|
416
567
|
this.clearPendingPermissionState();
|
|
@@ -1535,7 +1686,11 @@ export class OpenCodeServerAdapter {
|
|
|
1535
1686
|
return;
|
|
1536
1687
|
}
|
|
1537
1688
|
const sessionId = this.extractSessionId(properties);
|
|
1538
|
-
if (!sessionId
|
|
1689
|
+
if (!sessionId) {
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
this.previousAgentBySession.delete(sessionId);
|
|
1693
|
+
if (sessionId !== this.activeSessionId) {
|
|
1539
1694
|
return;
|
|
1540
1695
|
}
|
|
1541
1696
|
this.clearTrackedTurnForLocalSessionSwitch();
|
|
@@ -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 crypto from "node:crypto";
|
|
2
8
|
import fs from "node:fs";
|
|
3
9
|
import net from "node:net";
|
|
@@ -356,6 +362,41 @@ export class PiTuiAdapter {
|
|
|
356
362
|
this.pendingSessionSwitch = null;
|
|
357
363
|
}
|
|
358
364
|
}
|
|
365
|
+
async listModels() {
|
|
366
|
+
if (this.state.status === "busy") {
|
|
367
|
+
throw new Error("Pi is still working. Wait for the current turn before listing models.");
|
|
368
|
+
}
|
|
369
|
+
const response = await this.sendCommand("list_models");
|
|
370
|
+
if (!isRecord(response.data) || !Array.isArray(response.data.models)) {
|
|
371
|
+
throw new Error("Pi TUI returned an invalid model list.");
|
|
372
|
+
}
|
|
373
|
+
return response.data.models
|
|
374
|
+
.filter((model) => isRecord(model))
|
|
375
|
+
.filter((model) => typeof model.id === "string" && typeof model.displayName === "string")
|
|
376
|
+
.map((model) => ({
|
|
377
|
+
id: model.id,
|
|
378
|
+
displayName: model.displayName,
|
|
379
|
+
isCurrent: model.isCurrent === true,
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
async selectModel(modelId) {
|
|
383
|
+
if (this.state.status === "busy") {
|
|
384
|
+
throw new Error("Pi is still working. Wait for the current turn before switching models.");
|
|
385
|
+
}
|
|
386
|
+
const response = await this.sendCommand("select_model", { modelId });
|
|
387
|
+
if (!isRecord(response.data) || !isRecord(response.data.model)) {
|
|
388
|
+
throw new Error("Pi TUI returned an invalid selected model.");
|
|
389
|
+
}
|
|
390
|
+
const model = response.data.model;
|
|
391
|
+
if (typeof model.id !== "string" || typeof model.displayName !== "string") {
|
|
392
|
+
throw new Error("Pi TUI returned an invalid selected model.");
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
id: model.id,
|
|
396
|
+
displayName: model.displayName,
|
|
397
|
+
isCurrent: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
359
400
|
async interrupt() {
|
|
360
401
|
if (!this.socket || this.state.status !== "busy") {
|
|
361
402
|
return false;
|
|
@@ -569,6 +610,10 @@ export class PiTuiAdapter {
|
|
|
569
610
|
else if (this.currentAssistantText) {
|
|
570
611
|
this.emit({ type: "final_reply", text: this.currentAssistantText, timestamp: nowIso() });
|
|
571
612
|
}
|
|
613
|
+
// Mirror the other adapters: a settled turn always reports completion so
|
|
614
|
+
// bridge/daemon ownership state is released even when the turn produced
|
|
615
|
+
// no visible reply.
|
|
616
|
+
this.emit({ type: "task_complete", timestamp: nowIso() });
|
|
572
617
|
this.state.lastOutputAt = nowIso();
|
|
573
618
|
this.clearActiveTurn();
|
|
574
619
|
this.setStatus("idle");
|
|
@@ -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 os from "node:os";
|
|
3
9
|
import path from "node:path";
|
|
@@ -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 { clearLocalCompanionEndpoint, readLocalCompanionEndpoint, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
2
8
|
import { hasLocalClientEndpointProvider } from "../runtime/runtime-types.js";
|
|
3
9
|
export class BridgeController {
|
|
@@ -1,2 +1,8 @@
|
|
|
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
|
/** @deprecated Import from ../channels/wechat/bridge-final-reply.ts instead. */
|
|
2
8
|
export * from "../channels/wechat/bridge-final-reply.js";
|
|
@@ -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 { spawnSync } from "node:child_process";
|
|
2
8
|
import path from "node:path";
|
|
3
9
|
const PEER_BRIDGE_EXIT_TIMEOUT_MS = 4_000;
|
|
@@ -113,7 +119,7 @@ function normalizeBridgeProcessRecord(value) {
|
|
|
113
119
|
}
|
|
114
120
|
return record;
|
|
115
121
|
}
|
|
116
|
-
|
|
122
|
+
function parseWindowsProcessProbeRecords(stdout) {
|
|
117
123
|
const trimmed = stdout.trim();
|
|
118
124
|
if (!trimmed) {
|
|
119
125
|
return [];
|
|
@@ -128,7 +134,13 @@ export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = proces
|
|
|
128
134
|
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
129
135
|
return values
|
|
130
136
|
.map(normalizeBridgeProcessRecord)
|
|
131
|
-
.filter((record) => Boolean(record))
|
|
137
|
+
.filter((record) => Boolean(record));
|
|
138
|
+
}
|
|
139
|
+
export function parseWindowsProcessRecordProbeOutput(stdout, currentPid = process.pid) {
|
|
140
|
+
return (parseWindowsProcessProbeRecords(stdout).find((record) => record.pid !== currentPid) ?? null);
|
|
141
|
+
}
|
|
142
|
+
export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
|
|
143
|
+
return parseWindowsProcessProbeRecords(stdout)
|
|
132
144
|
.filter((record) => record.pid !== currentPid && isWechatBridgeCommandLine(record.commandLine));
|
|
133
145
|
}
|
|
134
146
|
export function parsePosixBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
|
|
@@ -195,10 +207,37 @@ export function listPeerBridgeProcesses(currentPid = process.pid) {
|
|
|
195
207
|
? listWindowsBridgeProcesses(currentPid)
|
|
196
208
|
: listPosixBridgeProcesses(currentPid);
|
|
197
209
|
}
|
|
210
|
+
function getWindowsProcessRecordByPid(pid, currentPid = process.pid) {
|
|
211
|
+
const probe = spawnSync("powershell.exe", [
|
|
212
|
+
"-NoLogo",
|
|
213
|
+
"-NoProfile",
|
|
214
|
+
"-NonInteractive",
|
|
215
|
+
"-ExecutionPolicy",
|
|
216
|
+
"Bypass",
|
|
217
|
+
"-Command",
|
|
218
|
+
[
|
|
219
|
+
"$ErrorActionPreference='Stop';",
|
|
220
|
+
`Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}'`,
|
|
221
|
+
"| Select-Object ProcessId,ParentProcessId,Name,CommandLine",
|
|
222
|
+
"| ConvertTo-Json -Compress",
|
|
223
|
+
].join(" "),
|
|
224
|
+
], {
|
|
225
|
+
encoding: "utf8",
|
|
226
|
+
windowsHide: true,
|
|
227
|
+
timeout: 8_000,
|
|
228
|
+
});
|
|
229
|
+
if (probe.status !== 0 || typeof probe.stdout !== "string") {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return parseWindowsProcessRecordProbeOutput(probe.stdout, currentPid);
|
|
233
|
+
}
|
|
198
234
|
export function getProcessRecordByPid(pid, currentPid = process.pid) {
|
|
199
235
|
if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) {
|
|
200
236
|
return null;
|
|
201
237
|
}
|
|
238
|
+
if (process.platform === "win32") {
|
|
239
|
+
return getWindowsProcessRecordByPid(pid, currentPid);
|
|
240
|
+
}
|
|
202
241
|
return listAllProcessesRaw(currentPid).find((record) => record.pid === pid) ?? null;
|
|
203
242
|
}
|
|
204
243
|
export function listWechatDaemonProcesses(params = {}) {
|