cli-wechat-bridge 1.1.8 → 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/README.md +21 -7
- package/bin/_run-entry.mjs +6 -1
- 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 +6 -1
- package/bin/wecom-claude.mjs +6 -1
- package/bin/wecom-codex.mjs +6 -1
- package/bin/wecom-daemon.mjs +6 -1
- package/bin/wecom-opencode.mjs +6 -1
- package/bin/wecom-pi.mjs +6 -1
- package/bin/wecom-setup.mjs +6 -1
- package/dist/bridge/bridge-adapters.claude.js +6 -0
- package/dist/bridge/bridge-adapters.codex.js +95 -8
- package/dist/bridge/bridge-adapters.core.js +6 -0
- package/dist/bridge/bridge-adapters.opencode.js +148 -19
- package/dist/bridge/bridge-adapters.pi.js +6 -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 +6 -0
- package/dist/bridge/bridge-state.js +6 -0
- package/dist/bridge/bridge-utils.js +6 -31
- package/dist/bridge/claude-hook.js +6 -0
- package/dist/bridge/claude-native-controls.js +14 -2
- package/dist/bridge/codex-visible-thread-proxy.js +140 -0
- package/dist/bridge/native-terminal-control.js +18 -0
- package/dist/bridge/opencode-v2-compat.js +450 -0
- package/dist/bridge/wechat-bridge.js +148 -241
- 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 +39 -0
- package/dist/channels/wecom/bridge-final-reply.js +6 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +6 -0
- package/dist/channels/wecom/wecom-config.js +6 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +6 -0
- package/dist/channels/wecom/wecom-transport.js +6 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +6 -0
- package/dist/companion/codex-visible-client-link.js +6 -0
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion-start.js +6 -0
- package/dist/companion/local-companion.js +6 -0
- 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 +6 -0
- package/dist/core/bridge-control.js +2 -0
- package/dist/core/bridge-defer.js +25 -0
- package/dist/core/channel-driver.js +1 -0
- package/dist/core/conversation-routing.js +11 -1
- package/dist/core/direct-run.js +6 -0
- package/dist/core/inbound-message-claims.js +6 -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 +6 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +451 -173
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +7 -1
- package/dist/i18n/messages-zh.js +7 -1
- package/dist/runtime/create-runtime-host.js +6 -0
- package/dist/utils/atomic-file.js +6 -0
- package/dist/utils/doctor.js +6 -0
- package/dist/utils/version-checker.js +6 -0
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +6 -0
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +73 -0
- package/package.json +5 -2
|
@@ -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";
|
|
@@ -6,6 +12,7 @@ import path from "node:path";
|
|
|
6
12
|
import { fileURLToPath } from "node:url";
|
|
7
13
|
import { OPENCODE_SERVER_HOST, OPENCODE_SERVER_READY_TIMEOUT_MS, OPENCODE_SSE_RECONNECT_DELAY_MS, OPENCODE_SESSION_IDLE_SETTLE_MS, OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS, OPENCODE_HTTP_READY_TIMEOUT_MS, OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS, OPENCODE_HTTP_READY_PROBE_INTERVAL_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
|
|
8
14
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
15
|
+
import { createOpenCodeV2CompatClient } from "./opencode-v2-compat.js";
|
|
9
16
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
10
17
|
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
11
18
|
const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
|
|
@@ -18,7 +25,11 @@ const TUI_SESSION_SELECT_SUPPRESSION_TTL_MS = 5_000;
|
|
|
18
25
|
const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
|
|
19
26
|
const OPENCODE_TUI_SELECT_RETRY_DELAYS_MS = [250, 750, 1_500];
|
|
20
27
|
const OPENCODE_MINIMUM_SUPPORTED_VERSION = "1.18.0";
|
|
21
|
-
const OPENCODE_MAXIMUM_SUPPORTED_MAJOR =
|
|
28
|
+
const OPENCODE_MAXIMUM_SUPPORTED_MAJOR = 3;
|
|
29
|
+
const OPENCODE_VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
30
|
+
export function parseOpenCodeCliVersion(output) {
|
|
31
|
+
return /\bv?(\d+\.\d+\.\d+)\b/.exec(output)?.[1] ?? null;
|
|
32
|
+
}
|
|
22
33
|
const OPENCODE_TUI_ROUTE_READY_TIMEOUT_MS = 10_000;
|
|
23
34
|
const OPENCODE_TUI_ROUTE_MAX_BUFFER_SIZE = 1024 * 1024;
|
|
24
35
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
@@ -77,6 +88,8 @@ export class OpenCodeServerAdapter {
|
|
|
77
88
|
tuiRoutePort = 0;
|
|
78
89
|
tuiRouteConfigDir = null;
|
|
79
90
|
tuiRouteConfigPath = null;
|
|
91
|
+
tuiRoutePluginPath = null;
|
|
92
|
+
openCodeMajor = 1;
|
|
80
93
|
visibleTuiSessionId = null;
|
|
81
94
|
tuiRouteStateReceived = false;
|
|
82
95
|
expectedVisibleTuiSessionId = null;
|
|
@@ -244,6 +257,7 @@ export class OpenCodeServerAdapter {
|
|
|
244
257
|
this.shuttingDown = false;
|
|
245
258
|
this.setStatus("starting", "Starting OpenCode companion...");
|
|
246
259
|
try {
|
|
260
|
+
await this.probeOpenCodeVersion();
|
|
247
261
|
this.serverPort = await reserveLocalPort();
|
|
248
262
|
const serverProcess = await this.startServerProcess();
|
|
249
263
|
await waitForTcpPort(OPENCODE_SERVER_HOST, this.serverPort, OPENCODE_SERVER_READY_TIMEOUT_MS);
|
|
@@ -626,13 +640,28 @@ export class OpenCodeServerAdapter {
|
|
|
626
640
|
resolve(address.port);
|
|
627
641
|
});
|
|
628
642
|
});
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const pluginPath = path.join(MODULE_DIR, "..", "companion",
|
|
643
|
+
const pluginName = this.openCodeMajor >= 2
|
|
644
|
+
? "opencode-v2-tui-bridge-plugin"
|
|
645
|
+
: "opencode-tui-bridge-plugin";
|
|
646
|
+
const pluginPath = path.join(MODULE_DIR, "..", "companion", `${pluginName}${RUNTIME_ENTRY_EXTENSION}`);
|
|
633
647
|
if (!fs.existsSync(pluginPath)) {
|
|
634
648
|
throw new Error(`OpenCode TUI bridge plugin was not found: ${pluginPath}`);
|
|
635
649
|
}
|
|
650
|
+
const workspaceDir = ensureWorkspaceChannelDir(this.options.cwd).workspaceDir;
|
|
651
|
+
const configDir = fs.mkdtempSync(path.join(workspaceDir, "opencode-tui-route-"));
|
|
652
|
+
this.tuiRouteConfigDir = configDir;
|
|
653
|
+
if (this.openCodeMajor >= 2) {
|
|
654
|
+
fs.copyFileSync(pluginPath, path.join(configDir, `tui${RUNTIME_ENTRY_EXTENSION}`));
|
|
655
|
+
fs.writeFileSync(path.join(configDir, "package.json"), `${JSON.stringify({
|
|
656
|
+
name: "cli-wechat-bridge-opencode-tui-route",
|
|
657
|
+
private: true,
|
|
658
|
+
type: "module",
|
|
659
|
+
}, null, 2)}\n`, "utf8");
|
|
660
|
+
this.tuiRoutePluginPath = configDir;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
this.tuiRoutePluginPath = pluginPath;
|
|
664
|
+
const configPath = path.join(configDir, "tui.json");
|
|
636
665
|
fs.writeFileSync(configPath, `${JSON.stringify({
|
|
637
666
|
plugin: [
|
|
638
667
|
[
|
|
@@ -644,7 +673,6 @@ export class OpenCodeServerAdapter {
|
|
|
644
673
|
],
|
|
645
674
|
],
|
|
646
675
|
}, null, 2)}\n`, "utf8");
|
|
647
|
-
this.tuiRouteConfigDir = configDir;
|
|
648
676
|
this.tuiRouteConfigPath = configPath;
|
|
649
677
|
}
|
|
650
678
|
acceptTuiRouteSocket(socket) {
|
|
@@ -759,6 +787,7 @@ export class OpenCodeServerAdapter {
|
|
|
759
787
|
const configDir = this.tuiRouteConfigDir;
|
|
760
788
|
this.tuiRouteConfigDir = null;
|
|
761
789
|
this.tuiRouteConfigPath = null;
|
|
790
|
+
this.tuiRoutePluginPath = null;
|
|
762
791
|
this.tuiRoutePort = 0;
|
|
763
792
|
this.tuiRouteToken = "";
|
|
764
793
|
this.tuiRouteStateReceived = false;
|
|
@@ -775,6 +804,50 @@ export class OpenCodeServerAdapter {
|
|
|
775
804
|
}
|
|
776
805
|
}
|
|
777
806
|
}
|
|
807
|
+
async probeOpenCodeVersion() {
|
|
808
|
+
const env = buildCliEnvironment(this.options.kind);
|
|
809
|
+
const target = resolveSpawnTarget(this.options.command, this.options.kind, { env });
|
|
810
|
+
const version = await new Promise((resolve, reject) => {
|
|
811
|
+
const child = spawnChildProcess(target.file, [...target.args, "--version"], {
|
|
812
|
+
cwd: this.options.cwd,
|
|
813
|
+
env,
|
|
814
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
815
|
+
windowsHide: true,
|
|
816
|
+
});
|
|
817
|
+
let output = "";
|
|
818
|
+
let settled = false;
|
|
819
|
+
const finish = (error) => {
|
|
820
|
+
if (settled)
|
|
821
|
+
return;
|
|
822
|
+
settled = true;
|
|
823
|
+
clearTimeout(timer);
|
|
824
|
+
if (error)
|
|
825
|
+
reject(error);
|
|
826
|
+
else
|
|
827
|
+
resolve(output);
|
|
828
|
+
};
|
|
829
|
+
const timer = setTimeout(() => {
|
|
830
|
+
if (child.pid != null) {
|
|
831
|
+
killProcessTreeSync(child.pid);
|
|
832
|
+
}
|
|
833
|
+
finish(new Error("Timed out checking the OpenCode CLI version."));
|
|
834
|
+
}, OPENCODE_VERSION_PROBE_TIMEOUT_MS);
|
|
835
|
+
child.stdout?.on("data", (chunk) => { output += chunk.toString("utf8"); });
|
|
836
|
+
child.stderr?.on("data", (chunk) => { output += chunk.toString("utf8"); });
|
|
837
|
+
child.once("error", (error) => finish(error));
|
|
838
|
+
child.once("exit", (code) => {
|
|
839
|
+
if (code === 0)
|
|
840
|
+
finish();
|
|
841
|
+
else
|
|
842
|
+
finish(new Error(`OpenCode --version exited with code ${code ?? "unknown"}.`));
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
const parsed = parseOpenCodeCliVersion(version);
|
|
846
|
+
if (!parsed || !this.isSupportedOpenCodeVersion(parsed)) {
|
|
847
|
+
throw new Error(`Unsupported OpenCode version ${parsed ?? (version.trim() || "unknown")}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
|
|
848
|
+
}
|
|
849
|
+
this.openCodeMajor = Number(parsed.split(".")[0]);
|
|
850
|
+
}
|
|
778
851
|
async startServerProcess() {
|
|
779
852
|
const env = buildCliEnvironment(this.options.kind);
|
|
780
853
|
this.serverPassword = randomBytes(24).toString("hex");
|
|
@@ -880,8 +953,9 @@ export class OpenCodeServerAdapter {
|
|
|
880
953
|
if ((this.options.extraCliArgs ?? []).includes("--pure")) {
|
|
881
954
|
throw new Error("OpenCode --pure cannot be used with the WeChat bridge because local session following requires the managed TUI plugin.");
|
|
882
955
|
}
|
|
883
|
-
const args =
|
|
884
|
-
|
|
956
|
+
const args = this.openCodeMajor >= 2
|
|
957
|
+
? ["--server", this.getServerUrl()]
|
|
958
|
+
: ["attach", this.getServerUrl(), "--dir", this.options.cwd];
|
|
885
959
|
const sessionId = this.activeSessionId;
|
|
886
960
|
if (sessionId && (await this.hasSession(sessionId))) {
|
|
887
961
|
args.push("--session", sessionId);
|
|
@@ -891,7 +965,37 @@ export class OpenCodeServerAdapter {
|
|
|
891
965
|
}
|
|
892
966
|
buildNativeClientEnv() {
|
|
893
967
|
const env = buildCliEnvironment(this.options.kind);
|
|
894
|
-
if (this.
|
|
968
|
+
if (this.openCodeMajor >= 2 && this.tuiRoutePluginPath) {
|
|
969
|
+
let existingConfig = {};
|
|
970
|
+
if (env.OPENCODE_CLI_CONFIG_CONTENT) {
|
|
971
|
+
try {
|
|
972
|
+
const parsed = JSON.parse(env.OPENCODE_CLI_CONFIG_CONTENT);
|
|
973
|
+
if (isRecord(parsed))
|
|
974
|
+
existingConfig = parsed;
|
|
975
|
+
}
|
|
976
|
+
catch (error) {
|
|
977
|
+
throw new Error(`OpenCode cannot merge the managed TUI plugin into invalid OPENCODE_CLI_CONFIG_CONTENT: ${describeUnknownError(error)}`, { cause: error });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const existingPlugins = Array.isArray(existingConfig.plugins)
|
|
981
|
+
? existingConfig.plugins
|
|
982
|
+
: [];
|
|
983
|
+
env.OPENCODE_CLI_CONFIG_CONTENT = JSON.stringify({
|
|
984
|
+
...existingConfig,
|
|
985
|
+
plugins: [
|
|
986
|
+
...existingPlugins,
|
|
987
|
+
{
|
|
988
|
+
package: this.tuiRoutePluginPath,
|
|
989
|
+
options: {
|
|
990
|
+
port: this.tuiRoutePort,
|
|
991
|
+
token: this.tuiRouteToken,
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
});
|
|
996
|
+
env.OPENCODE_PASSWORD = this.serverPassword ?? "";
|
|
997
|
+
}
|
|
998
|
+
else if (this.tuiRouteConfigDir && this.tuiRouteConfigPath) {
|
|
895
999
|
if (env.OPENCODE_CONFIG_DIR && env.OPENCODE_TUI_CONFIG) {
|
|
896
1000
|
throw new Error("OpenCode cannot inject the managed TUI session plugin while both OPENCODE_CONFIG_DIR and OPENCODE_TUI_CONFIG are set.");
|
|
897
1001
|
}
|
|
@@ -905,7 +1009,7 @@ export class OpenCodeServerAdapter {
|
|
|
905
1009
|
if (this.serverPassword) {
|
|
906
1010
|
env.OPENCODE_SERVER_PASSWORD = this.serverPassword;
|
|
907
1011
|
}
|
|
908
|
-
if (this.activeSessionId) {
|
|
1012
|
+
if (this.activeSessionId && this.openCodeMajor < 2) {
|
|
909
1013
|
env.OPENCODE_ROUTE = JSON.stringify({
|
|
910
1014
|
type: "session",
|
|
911
1015
|
sessionID: this.activeSessionId,
|
|
@@ -922,6 +1026,14 @@ export class OpenCodeServerAdapter {
|
|
|
922
1026
|
}
|
|
923
1027
|
async createSdkClient() {
|
|
924
1028
|
try {
|
|
1029
|
+
if (this.openCodeMajor >= 2) {
|
|
1030
|
+
this.client = await createOpenCodeV2CompatClient({
|
|
1031
|
+
baseUrl: `http://${OPENCODE_SERVER_HOST}:${this.serverPort}`,
|
|
1032
|
+
directory: this.options.cwd,
|
|
1033
|
+
headers: this.buildServerAuthHeaders(),
|
|
1034
|
+
});
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
925
1037
|
const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
|
|
926
1038
|
this.client = createOpencodeClient({
|
|
927
1039
|
baseUrl: `http://${OPENCODE_SERVER_HOST}:${this.serverPort}`,
|
|
@@ -931,7 +1043,8 @@ export class OpenCodeServerAdapter {
|
|
|
931
1043
|
});
|
|
932
1044
|
}
|
|
933
1045
|
catch (err) {
|
|
934
|
-
|
|
1046
|
+
const packageName = this.openCodeMajor >= 2 ? "@opencode/client" : "@opencode-ai/sdk";
|
|
1047
|
+
throw new Error(`Failed to load ${packageName}. Make sure it is installed: ${describeUnknownError(err)}`, { cause: err });
|
|
935
1048
|
}
|
|
936
1049
|
}
|
|
937
1050
|
async checkHealth() {
|
|
@@ -946,17 +1059,24 @@ export class OpenCodeServerAdapter {
|
|
|
946
1059
|
let lastError;
|
|
947
1060
|
while (Date.now() < deadline) {
|
|
948
1061
|
try {
|
|
949
|
-
const
|
|
1062
|
+
const healthPath = this.openCodeMajor >= 2 ? "/api/info" : "/global/health";
|
|
1063
|
+
const response = await fetch(`${baseUrl}${healthPath}`, {
|
|
950
1064
|
headers: this.buildServerAuthHeaders(),
|
|
951
1065
|
signal: AbortSignal.timeout(OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS),
|
|
952
1066
|
});
|
|
953
1067
|
if (response.ok) {
|
|
954
1068
|
const health = await response.json();
|
|
955
|
-
|
|
1069
|
+
const version = isRecord(health) && typeof health.version === "string"
|
|
1070
|
+
? health.version
|
|
1071
|
+
: undefined;
|
|
1072
|
+
const healthy = this.openCodeMajor >= 2
|
|
1073
|
+
? Boolean(version)
|
|
1074
|
+
: isRecord(health) && health.healthy === true;
|
|
1075
|
+
if (!healthy || !version) {
|
|
956
1076
|
lastError = new Error("OpenCode health check returned an invalid response.");
|
|
957
1077
|
}
|
|
958
|
-
else if (!this.isSupportedOpenCodeVersion(
|
|
959
|
-
throw new Error(`Unsupported OpenCode version ${
|
|
1078
|
+
else if (!this.isSupportedOpenCodeVersion(version)) {
|
|
1079
|
+
throw new Error(`Unsupported OpenCode version ${version}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
|
|
960
1080
|
}
|
|
961
1081
|
else {
|
|
962
1082
|
return;
|
|
@@ -1093,10 +1213,9 @@ export class OpenCodeServerAdapter {
|
|
|
1093
1213
|
return;
|
|
1094
1214
|
}
|
|
1095
1215
|
this.sseAbortController = new AbortController();
|
|
1096
|
-
const sseLoops =
|
|
1097
|
-
this.runSseLoop("event")
|
|
1098
|
-
this.runSseLoop("global-event")
|
|
1099
|
-
];
|
|
1216
|
+
const sseLoops = this.openCodeMajor >= 2
|
|
1217
|
+
? [this.runSseLoop("event")]
|
|
1218
|
+
: [this.runSseLoop("event"), this.runSseLoop("global-event")];
|
|
1100
1219
|
this.sseLoopPromise = Promise.all(sseLoops).then(() => undefined);
|
|
1101
1220
|
}
|
|
1102
1221
|
async runSseLoop(streamName) {
|
|
@@ -2739,6 +2858,16 @@ export class OpenCodeServerAdapter {
|
|
|
2739
2858
|
}
|
|
2740
2859
|
}
|
|
2741
2860
|
async sendVisibleSessionSelection(session) {
|
|
2861
|
+
if (this.openCodeMajor >= 2) {
|
|
2862
|
+
if (!this.tuiRouteSocket || this.tuiRouteSocket.destroyed) {
|
|
2863
|
+
throw new Error("The OpenCode 2 TUI route bridge is not connected.");
|
|
2864
|
+
}
|
|
2865
|
+
this.tuiRouteSocket.write(`${JSON.stringify({
|
|
2866
|
+
type: "select_session",
|
|
2867
|
+
sessionId: session.id,
|
|
2868
|
+
})}\n`);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2742
2871
|
if (!this.client?.tui) {
|
|
2743
2872
|
return;
|
|
2744
2873
|
}
|
|
@@ -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";
|
|
@@ -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;
|
|
@@ -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 { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureWorkspaceChannelDir, ensureChannelDataDir, } from "../wechat/channel-config.js";
|
|
3
9
|
import { buildInstanceId } from "../core/text-utils.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 fs from "node:fs";
|
|
2
8
|
import os from "node:os";
|
|
3
9
|
import path from "node:path";
|
|
@@ -1113,37 +1119,6 @@ export function isStrictApprovalModeEnabled(env = process.env) {
|
|
|
1113
1119
|
const value = env.CLI_BRIDGE_STRICT_APPROVAL?.trim().toLowerCase();
|
|
1114
1120
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
1115
1121
|
}
|
|
1116
|
-
// Outbound WeChat text size cap shared by the streaming OutputBatcher and the
|
|
1117
|
-
// final-reply forwarder. Long replies sent as a single sendmessage call can be
|
|
1118
|
-
// rejected by the server, which previously made long final replies vanish.
|
|
1119
|
-
export const WECHAT_TEXT_CHUNK_MAX_CHARS = 1_200;
|
|
1120
|
-
export function splitWechatTextIntoChunks(text, maxChars = WECHAT_TEXT_CHUNK_MAX_CHARS) {
|
|
1121
|
-
const normalized = text.trim();
|
|
1122
|
-
if (!normalized) {
|
|
1123
|
-
return [];
|
|
1124
|
-
}
|
|
1125
|
-
if (normalized.length <= maxChars) {
|
|
1126
|
-
return [normalized];
|
|
1127
|
-
}
|
|
1128
|
-
const chunks = [];
|
|
1129
|
-
let remaining = normalized;
|
|
1130
|
-
while (remaining.length > maxChars) {
|
|
1131
|
-
// Prefer breaking at a newline reasonably close to the cap so paragraphs
|
|
1132
|
-
// stay intact; fall back to a hard split.
|
|
1133
|
-
const window = remaining.slice(0, maxChars + 1);
|
|
1134
|
-
const newlineIndex = window.lastIndexOf("\n");
|
|
1135
|
-
const splitIndex = newlineIndex > maxChars / 2 ? newlineIndex : maxChars;
|
|
1136
|
-
const chunk = remaining.slice(0, splitIndex).trim();
|
|
1137
|
-
if (chunk) {
|
|
1138
|
-
chunks.push(chunk);
|
|
1139
|
-
}
|
|
1140
|
-
remaining = remaining.slice(splitIndex).trim();
|
|
1141
|
-
}
|
|
1142
|
-
if (remaining) {
|
|
1143
|
-
chunks.push(remaining);
|
|
1144
|
-
}
|
|
1145
|
-
return chunks;
|
|
1146
|
-
}
|
|
1147
1122
|
export function shouldDropStartupBacklogMessage(createdAtMs, bridgeStartedAtMs, graceMs = MESSAGE_START_GRACE_MS) {
|
|
1148
1123
|
// A missing or unparsable timestamp must not drop the message: treat it as
|
|
1149
1124
|
// fresh and let normal processing continue.
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
4
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
5
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
6
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
7
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
2
8
|
import crypto from "node:crypto";
|
|
3
9
|
import net from "node:net";
|
|
4
10
|
async function readStdin() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NativeTerminalControl, parseClaudeModelPicker, readClaudePermissionMode } from "./native-terminal-control.js";
|
|
1
|
+
import { NativeTerminalControl, parseClaudeModelPicker, parseClaudeModelSwitchConfirmation, readClaudePermissionMode } from "./native-terminal-control.js";
|
|
2
2
|
export class ClaudeNativeControls {
|
|
3
3
|
previousMode = null;
|
|
4
4
|
terminal;
|
|
@@ -83,7 +83,19 @@ export class ClaudeNativeControls {
|
|
|
83
83
|
throw new Error("This Claude version does not expose session-only model selection. Update Claude Code and try again.");
|
|
84
84
|
}
|
|
85
85
|
await this.terminal.submitMenu(undefined, "s");
|
|
86
|
-
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== null);
|
|
86
|
+
await this.terminal.waitFor(() => parseClaudeModelSwitchConfirmation(this.terminal.text) !== null || readClaudePermissionMode(this.terminal.text) !== null);
|
|
87
|
+
const confirmation = parseClaudeModelSwitchConfirmation(this.terminal.text);
|
|
88
|
+
if (confirmation) {
|
|
89
|
+
const targetLabel = target.label.toLowerCase();
|
|
90
|
+
const confirmedModel = confirmation.model.toLowerCase();
|
|
91
|
+
const matchesTarget = targetLabel.includes(confirmedModel)
|
|
92
|
+
|| (targetLabel.startsWith("default (recommended)") && confirmedModel.endsWith(" (default)"));
|
|
93
|
+
if (!confirmation.yesFocused || !matchesTarget) {
|
|
94
|
+
throw new Error("Claude displayed an unexpected model switch confirmation. Check the visible terminal before trying again.");
|
|
95
|
+
}
|
|
96
|
+
await this.terminal.key("\r");
|
|
97
|
+
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== null);
|
|
98
|
+
}
|
|
87
99
|
await this.terminal.openMenu("/model", "Select model");
|
|
88
100
|
const applied = parseClaudeModelPicker(this.terminal.text).find((row) => row.focused);
|
|
89
101
|
if (!applied || applied.label !== target.label || applied.index !== target.index)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
2
|
+
export class CodexVisibleThreadProxy {
|
|
3
|
+
server;
|
|
4
|
+
clients = new Set();
|
|
5
|
+
upstreams = new Set();
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.server = new WebSocketServer({
|
|
8
|
+
host: "127.0.0.1",
|
|
9
|
+
port: 0,
|
|
10
|
+
verifyClient: ({ req }) => req.headers.authorization === `Bearer ${options.token}`,
|
|
11
|
+
});
|
|
12
|
+
this.server.on("connection", (client) => {
|
|
13
|
+
options.onTrace?.("client_connected");
|
|
14
|
+
const upstream = new WebSocket(options.upstreamUrl, {
|
|
15
|
+
headers: { Authorization: `Bearer ${options.token}` },
|
|
16
|
+
});
|
|
17
|
+
this.clients.add(client);
|
|
18
|
+
this.upstreams.add(upstream);
|
|
19
|
+
const pendingRequests = new Map();
|
|
20
|
+
const queued = [];
|
|
21
|
+
let queuedBytes = 0;
|
|
22
|
+
client.on("message", (data, binary) => {
|
|
23
|
+
const bytes = Buffer.from(data);
|
|
24
|
+
if (!binary) {
|
|
25
|
+
try {
|
|
26
|
+
const request = JSON.parse(bytes.toString("utf8"));
|
|
27
|
+
if (request && typeof request === "object" && "method" in request) {
|
|
28
|
+
if (typeof request.method === "string" &&
|
|
29
|
+
(request.method.startsWith("thread/") || request.method === "turn/start")) {
|
|
30
|
+
const params = "params" in request && request.params && typeof request.params === "object"
|
|
31
|
+
? request.params : null;
|
|
32
|
+
const threadId = params && "threadId" in params && typeof params.threadId === "string"
|
|
33
|
+
? params.threadId.slice(0, 12) : "none";
|
|
34
|
+
options.onTrace?.(`request method=${request.method} thread=${threadId}`);
|
|
35
|
+
}
|
|
36
|
+
if (request.method === "turn/start" && "params" in request &&
|
|
37
|
+
request.params && typeof request.params === "object" &&
|
|
38
|
+
"threadId" in request.params && typeof request.params.threadId === "string") {
|
|
39
|
+
options.onVisibleTurn(request.params.threadId);
|
|
40
|
+
}
|
|
41
|
+
if ((request.method === "thread/resume" || request.method === "thread/start") &&
|
|
42
|
+
"id" in request && (typeof request.id === "number" || typeof request.id === "string")) {
|
|
43
|
+
pendingRequests.set(request.id, request.method);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Forward non-JSON messages unchanged.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (upstream.readyState === WebSocket.OPEN) {
|
|
52
|
+
upstream.send(bytes, { binary });
|
|
53
|
+
}
|
|
54
|
+
else if (upstream.readyState === WebSocket.CONNECTING && queuedBytes + bytes.length <= 1_048_576) {
|
|
55
|
+
queued.push({ data: bytes, binary });
|
|
56
|
+
queuedBytes += bytes.length;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
client.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
upstream.on("open", () => {
|
|
63
|
+
for (const message of queued)
|
|
64
|
+
upstream.send(message.data, { binary: message.binary });
|
|
65
|
+
queued.length = 0;
|
|
66
|
+
});
|
|
67
|
+
upstream.on("message", (data, binary) => {
|
|
68
|
+
if (!binary) {
|
|
69
|
+
try {
|
|
70
|
+
const response = JSON.parse(data.toString());
|
|
71
|
+
const pendingMethod = response && typeof response === "object" && "id" in response &&
|
|
72
|
+
(typeof response.id === "number" || typeof response.id === "string")
|
|
73
|
+
? pendingRequests.get(response.id) : undefined;
|
|
74
|
+
if (pendingMethod && response && typeof response === "object" && "id" in response &&
|
|
75
|
+
(typeof response.id === "number" || typeof response.id === "string")) {
|
|
76
|
+
pendingRequests.delete(response.id);
|
|
77
|
+
options.onTrace?.(`response method=${pendingMethod} success=${"result" in response}`);
|
|
78
|
+
}
|
|
79
|
+
if (pendingMethod && response && typeof response === "object" && "result" in response &&
|
|
80
|
+
response.result && typeof response.result === "object" &&
|
|
81
|
+
"thread" in response.result && response.result.thread &&
|
|
82
|
+
typeof response.result.thread === "object" && "id" in response.result.thread &&
|
|
83
|
+
typeof response.result.thread.id === "string" &&
|
|
84
|
+
!("parentThreadId" in response.result.thread &&
|
|
85
|
+
typeof response.result.thread.parentThreadId === "string") &&
|
|
86
|
+
!("ephemeral" in response.result.thread && response.result.thread.ephemeral === true)) {
|
|
87
|
+
const cwd = "cwd" in response.result.thread && typeof response.result.thread.cwd === "string"
|
|
88
|
+
? response.result.thread.cwd
|
|
89
|
+
: undefined;
|
|
90
|
+
options.onTrace?.(`opened thread=${response.result.thread.id.slice(0, 12)} cwd=${cwd ?? "unknown"}`);
|
|
91
|
+
options.onThreadOpened(response.result.thread.id, cwd);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// A malformed response must not interrupt the visible client.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (client.readyState === WebSocket.OPEN)
|
|
99
|
+
client.send(data, { binary });
|
|
100
|
+
});
|
|
101
|
+
client.on("close", () => upstream.terminate());
|
|
102
|
+
upstream.on("close", () => client.close());
|
|
103
|
+
client.on("error", () => upstream.terminate());
|
|
104
|
+
upstream.on("error", () => client.close());
|
|
105
|
+
client.on("close", () => {
|
|
106
|
+
this.clients.delete(client);
|
|
107
|
+
options.onTrace?.("client_disconnected");
|
|
108
|
+
});
|
|
109
|
+
upstream.on("close", () => this.upstreams.delete(upstream));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
static async start(options) {
|
|
113
|
+
const proxy = new CodexVisibleThreadProxy(options);
|
|
114
|
+
try {
|
|
115
|
+
await new Promise((resolve, reject) => {
|
|
116
|
+
proxy.server.once("listening", resolve);
|
|
117
|
+
proxy.server.once("error", reject);
|
|
118
|
+
});
|
|
119
|
+
return proxy;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
proxy.server.close();
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
get connected() {
|
|
127
|
+
return this.clients.size > 0;
|
|
128
|
+
}
|
|
129
|
+
get url() {
|
|
130
|
+
const address = this.server.address();
|
|
131
|
+
return `ws://127.0.0.1:${address.port}`;
|
|
132
|
+
}
|
|
133
|
+
async close() {
|
|
134
|
+
for (const client of this.clients)
|
|
135
|
+
client.terminate();
|
|
136
|
+
for (const upstream of this.upstreams)
|
|
137
|
+
upstream.terminate();
|
|
138
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -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 xterm from "@xterm/headless";
|
|
2
8
|
const { Terminal } = xterm;
|
|
3
9
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -239,6 +245,18 @@ export function parseClaudeModelPicker(text) {
|
|
|
239
245
|
return [{ index: Number(match[2]), label: match[3].replace(/[✓✔]/g, "").replace(/\s+/g, " ").trim(), current: /[✓✔]/.test(line), focused: /[❯>]/.test(match[1]) }];
|
|
240
246
|
});
|
|
241
247
|
}
|
|
248
|
+
export function parseClaudeModelSwitchConfirmation(text) {
|
|
249
|
+
const lines = text.split("\n");
|
|
250
|
+
const start = lines.findLastIndex((line) => line.trim() === "Switch model?");
|
|
251
|
+
if (start < 0)
|
|
252
|
+
return null;
|
|
253
|
+
const choices = lines.slice(start + 1);
|
|
254
|
+
const yes = choices.map((line) => /^\s*(❯|>)?\s*1\. Yes, switch to (.+?)\s*$/.exec(line)).find((match) => match);
|
|
255
|
+
const noIndex = choices.findIndex((line) => /^\s*(?:❯|>)?\s*2\. No, go back\s*$/.test(line));
|
|
256
|
+
if (!yes || noIndex < 0 || choices.slice(noIndex + 1).some((line) => /^\s*❯/.test(line)))
|
|
257
|
+
return null;
|
|
258
|
+
return { model: yes[2], yesFocused: Boolean(yes[1]) };
|
|
259
|
+
}
|
|
242
260
|
export function readClaudePermissionMode(text) {
|
|
243
261
|
const lines = text.split("\n");
|
|
244
262
|
const prompt = lines.findLastIndex((line) => /^\s*❯/.test(line));
|