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.
Files changed (80) hide show
  1. package/README.md +21 -7
  2. package/bin/_run-entry.mjs +6 -1
  3. package/bin/wechat-check-update.mjs +6 -1
  4. package/bin/wechat-claude.mjs +6 -1
  5. package/bin/wechat-codex.mjs +6 -1
  6. package/bin/wechat-daemon.mjs +6 -1
  7. package/bin/wechat-opencode.mjs +6 -1
  8. package/bin/wechat-pi.mjs +6 -1
  9. package/bin/wechat-setup.mjs +6 -1
  10. package/bin/wecom-check-update.mjs +6 -1
  11. package/bin/wecom-claude.mjs +6 -1
  12. package/bin/wecom-codex.mjs +6 -1
  13. package/bin/wecom-daemon.mjs +6 -1
  14. package/bin/wecom-opencode.mjs +6 -1
  15. package/bin/wecom-pi.mjs +6 -1
  16. package/bin/wecom-setup.mjs +6 -1
  17. package/dist/bridge/bridge-adapters.claude.js +6 -0
  18. package/dist/bridge/bridge-adapters.codex.js +95 -8
  19. package/dist/bridge/bridge-adapters.core.js +6 -0
  20. package/dist/bridge/bridge-adapters.opencode.js +148 -19
  21. package/dist/bridge/bridge-adapters.pi.js +6 -0
  22. package/dist/bridge/bridge-adapters.shared.js +6 -0
  23. package/dist/bridge/bridge-controller.js +6 -0
  24. package/dist/bridge/bridge-final-reply.js +6 -0
  25. package/dist/bridge/bridge-process-reaper.js +6 -0
  26. package/dist/bridge/bridge-state.js +6 -0
  27. package/dist/bridge/bridge-utils.js +6 -31
  28. package/dist/bridge/claude-hook.js +6 -0
  29. package/dist/bridge/claude-native-controls.js +14 -2
  30. package/dist/bridge/codex-visible-thread-proxy.js +140 -0
  31. package/dist/bridge/native-terminal-control.js +18 -0
  32. package/dist/bridge/opencode-v2-compat.js +450 -0
  33. package/dist/bridge/wechat-bridge.js +148 -241
  34. package/dist/bridge/wechat-forwarding.js +6 -0
  35. package/dist/bridge/wechat-outbound-queue.js +6 -0
  36. package/dist/channels/wechat/bridge-final-reply.js +13 -22
  37. package/dist/channels/wechat/index.js +6 -0
  38. package/dist/channels/wechat/wechat-driver.js +191 -0
  39. package/dist/channels/wechat/wechat-forwarding.js +18 -0
  40. package/dist/channels/wechat/wechat-outbound-queue.js +39 -0
  41. package/dist/channels/wecom/bridge-final-reply.js +6 -0
  42. package/dist/channels/wecom/setup-cli.js +6 -0
  43. package/dist/channels/wecom/setup.js +6 -0
  44. package/dist/channels/wecom/wecom-config.js +6 -0
  45. package/dist/channels/wecom/wecom-driver.js +106 -0
  46. package/dist/channels/wecom/wecom-media.js +6 -0
  47. package/dist/channels/wecom/wecom-transport.js +6 -0
  48. package/dist/commands/check-update.js +6 -0
  49. package/dist/companion/codex-remote-client.js +6 -0
  50. package/dist/companion/codex-visible-client-link.js +6 -0
  51. package/dist/companion/local-companion-link.js +6 -0
  52. package/dist/companion/local-companion-start.js +6 -0
  53. package/dist/companion/local-companion.js +6 -0
  54. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  55. package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
  56. package/dist/companion/pi-tui-bridge-extension.js +6 -0
  57. package/dist/core/bridge-control.js +2 -0
  58. package/dist/core/bridge-defer.js +25 -0
  59. package/dist/core/channel-driver.js +1 -0
  60. package/dist/core/conversation-routing.js +11 -1
  61. package/dist/core/direct-run.js +6 -0
  62. package/dist/core/inbound-message-claims.js +6 -0
  63. package/dist/core/outbound-target.js +32 -0
  64. package/dist/core/text-utils.js +6 -0
  65. package/dist/core/turn-coordinator.js +85 -0
  66. package/dist/daemon/daemon-link.js +6 -0
  67. package/dist/daemon/emoji-bindings.js +6 -0
  68. package/dist/daemon/wechat-daemon.js +451 -173
  69. package/dist/i18n/index.js +6 -0
  70. package/dist/i18n/messages-en.js +7 -1
  71. package/dist/i18n/messages-zh.js +7 -1
  72. package/dist/runtime/create-runtime-host.js +6 -0
  73. package/dist/utils/atomic-file.js +6 -0
  74. package/dist/utils/doctor.js +6 -0
  75. package/dist/utils/version-checker.js +6 -0
  76. package/dist/wechat/channel-config.js +6 -0
  77. package/dist/wechat/setup.js +6 -0
  78. package/dist/wechat/wechat-channel.js +6 -0
  79. package/dist/wechat/wechat-transport.js +73 -0
  80. 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 = 2;
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 workspaceDir = ensureWorkspaceChannelDir(this.options.cwd).workspaceDir;
630
- const configDir = fs.mkdtempSync(path.join(workspaceDir, "opencode-tui-route-"));
631
- const configPath = path.join(configDir, "tui.json");
632
- const pluginPath = path.join(MODULE_DIR, "..", "companion", `opencode-tui-bridge-plugin${RUNTIME_ENTRY_EXTENSION}`);
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 = ["attach", this.getServerUrl()];
884
- args.push("--dir", this.options.cwd);
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.tuiRouteConfigDir && this.tuiRouteConfigPath) {
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
- throw new Error(`Failed to load @opencode-ai/sdk. Make sure it is installed: ${describeUnknownError(err)}`, { cause: err });
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 response = await fetch(`${baseUrl}/global/health`, {
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
- if (!isRecord(health) || health.healthy !== true || typeof health.version !== "string") {
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(health.version)) {
959
- throw new Error(`Unsupported OpenCode version ${health.version}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
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));