@unlinearity/cli-wechat-bridge 1.1.5 → 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.
Files changed (89) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +434 -427
  3. package/bin/_run-entry.mjs +47 -9
  4. package/bin/wechat-check-update.mjs +6 -1
  5. package/bin/wechat-claude.mjs +6 -1
  6. package/bin/wechat-codex.mjs +6 -1
  7. package/bin/wechat-daemon.mjs +6 -1
  8. package/bin/wechat-opencode.mjs +6 -1
  9. package/bin/wechat-pi.mjs +6 -1
  10. package/bin/wechat-setup.mjs +6 -1
  11. package/bin/wecom-check-update.mjs +10 -0
  12. package/bin/wecom-claude.mjs +10 -0
  13. package/bin/wecom-codex.mjs +10 -0
  14. package/bin/wecom-daemon.mjs +10 -0
  15. package/bin/wecom-opencode.mjs +10 -0
  16. package/bin/wecom-pi.mjs +10 -0
  17. package/bin/wecom-setup.mjs +10 -0
  18. package/dist/bridge/adapter-control.js +58 -0
  19. package/dist/bridge/bridge-adapters.claude.js +163 -5
  20. package/dist/bridge/bridge-adapters.codex.js +331 -29
  21. package/dist/bridge/bridge-adapters.core.js +27 -2
  22. package/dist/bridge/bridge-adapters.opencode.js +156 -1
  23. package/dist/bridge/bridge-adapters.pi.js +46 -1
  24. package/dist/bridge/bridge-adapters.shared.js +8 -1
  25. package/dist/bridge/bridge-controller.js +6 -0
  26. package/dist/bridge/bridge-final-reply.js +8 -65
  27. package/dist/bridge/bridge-process-reaper.js +41 -2
  28. package/dist/bridge/bridge-state.js +17 -4
  29. package/dist/bridge/bridge-utils.js +10 -127
  30. package/dist/bridge/claude-hook.js +6 -0
  31. package/dist/bridge/claude-native-controls.js +133 -0
  32. package/dist/bridge/native-terminal-control.js +259 -0
  33. package/dist/bridge/wechat-bridge.js +520 -274
  34. package/dist/bridge/wechat-forwarding.js +8 -58
  35. package/dist/bridge/wechat-outbound-queue.js +8 -0
  36. package/dist/channels/wechat/bridge-final-reply.js +56 -0
  37. package/dist/channels/wechat/channel-message.js +24 -0
  38. package/dist/channels/wechat/index.js +11 -0
  39. package/dist/channels/wechat/wechat-channel-port.js +67 -0
  40. package/dist/channels/wechat/wechat-driver.js +191 -0
  41. package/dist/channels/wechat/wechat-forwarding.js +97 -0
  42. package/dist/channels/wechat/wechat-outbound-queue.js +118 -0
  43. package/dist/channels/wecom/bridge-final-reply.js +32 -0
  44. package/dist/channels/wecom/setup-cli.js +12 -0
  45. package/dist/channels/wecom/setup.js +182 -0
  46. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  47. package/dist/channels/wecom/wecom-config.js +72 -0
  48. package/dist/channels/wecom/wecom-driver.js +106 -0
  49. package/dist/channels/wecom/wecom-media.js +147 -0
  50. package/dist/channels/wecom/wecom-message.js +149 -0
  51. package/dist/channels/wecom/wecom-transport.js +314 -0
  52. package/dist/commands/check-update.js +6 -0
  53. package/dist/companion/codex-remote-client.js +30 -3
  54. package/dist/companion/codex-visible-client-link.js +107 -0
  55. package/dist/companion/local-companion-link.js +6 -0
  56. package/dist/companion/local-companion-start.js +62 -11
  57. package/dist/companion/local-companion.js +25 -1
  58. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  59. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  60. package/dist/core/bridge-control.js +42 -0
  61. package/dist/core/bridge-defer.js +25 -0
  62. package/dist/core/bridge-event-forwarder.js +50 -0
  63. package/dist/core/bridge-message-router.js +32 -0
  64. package/dist/core/channel-driver.js +1 -0
  65. package/dist/core/channel-types.js +3 -0
  66. package/dist/core/conversation-routing.js +58 -0
  67. package/dist/core/direct-run.js +23 -0
  68. package/dist/core/inbound-message-claims.js +67 -0
  69. package/dist/core/outbound-target.js +32 -0
  70. package/dist/core/text-utils.js +69 -0
  71. package/dist/core/turn-coordinator.js +85 -0
  72. package/dist/daemon/daemon-link.js +9 -0
  73. package/dist/daemon/emoji-bindings.js +6 -0
  74. package/dist/daemon/wechat-daemon.js +827 -209
  75. package/dist/i18n/index.js +6 -0
  76. package/dist/i18n/messages-en.js +11 -3
  77. package/dist/i18n/messages-zh.js +11 -3
  78. package/dist/runtime/create-runtime-host.js +6 -0
  79. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  80. package/dist/runtime/runtime-types.js +3 -0
  81. package/dist/utils/atomic-file.js +6 -0
  82. package/dist/utils/doctor.js +36 -10
  83. package/dist/utils/version-checker.js +17 -11
  84. package/dist/wechat/channel-config.js +6 -0
  85. package/dist/wechat/setup.js +119 -13
  86. package/dist/wechat/wechat-channel.js +6 -0
  87. package/dist/wechat/wechat-transport.js +169 -58
  88. package/package.json +14 -4
  89. 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 { messages as zhMessages } from "./messages-zh.js";
2
8
  import { messages as enMessages } from "./messages-en.js";
3
9
  const catalogs = {
@@ -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
  export const messages = {
2
8
  // === Bridge welcome & control ===
3
9
  "bridge.welcome": "WeChat Bridge ready ({adapter}).\nCWD: {cwd}",
@@ -11,7 +17,7 @@ export const messages = {
11
17
  "bridge.fatalError": "Bridge error: {message}",
12
18
  "bridge.backlogIgnored": "Ignored {count} backlog message(s) sent before the bridge started (they will not be processed).\nIf you actually sent them after the bridge started, your system clock is likely skewed by more than {graceSeconds}s — sync the clock and restart the bridge.",
13
19
  // === Daemon welcome & control ===
14
- "daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
20
+ "daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status, /model, /plan, /all <prompt>\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
15
21
  "daemon.noActiveAdapter": "No active terminal is selected. Send /codex, /claude, /opencode, or /pi to start one.",
16
22
  "daemon.switchResult.new": "Started a new visible CLI.",
17
23
  "daemon.switchResult.reused": "Reused the existing visible CLI.",
@@ -25,7 +31,7 @@ export const messages = {
25
31
  // === Hook health check ===
26
32
  "hook.healthCheck.warning": "[Warning] No hook events received from Claude after 15s.\nThe hook system may not be working — Claude output will not reach WeChat.",
27
33
  "hook.healthCheck.logHint": "\nCheck: {logPath}",
28
- "hook.healthCheck.fixes": "\nCommon fixes:\n- Ensure Node.js >= 22.6.0: node --version\n- Reinstall: npm install -g cli-wechat-bridge@latest\n- Check firewall: allow localhost TCP connections",
34
+ "hook.healthCheck.fixes": "\nCommon fixes:\n- Ensure Node.js >= 22.13.0: node --version\n- Reinstall: npm install -g cli-wechat-bridge@latest\n- Check firewall: allow localhost TCP connections",
29
35
  // === PTY fallback ===
30
36
  "pty.fallback.warning": "[Warning] PTY unavailable — using fallback mode (TERM=dumb).\nCore features (messaging, approvals, session management) work normally; terminal rendering (colors, progress bars) may be degraded.\nFix: Linux install build-essential python3 | Windows run npm rebuild node-pty | macOS run xcode-select --install\nRun --doctor for detailed environment info.\n",
31
37
  // === Spawn diagnostic ===
@@ -36,7 +42,7 @@ export const messages = {
36
42
  "spawn.diagnostic.linuxBuildTools": "- Linux requires build tools for node-pty: sudo apt install build-essential python3\n- Then reinstall: npm install -g cli-wechat-bridge@latest",
37
43
  "spawn.diagnostic.notFound": "- The command \"{target}\" was not found on PATH.\n- Verify it is installed and accessible from your terminal.",
38
44
  "spawn.diagnostic.generic": "- Reinstall: npm install -g cli-wechat-bridge@latest",
39
- "spawn.diagnostic.nodeVersion": "- Ensure Node.js >= 22.6.0: node --version",
45
+ "spawn.diagnostic.nodeVersion": "- Ensure Node.js >= 22.13.0: node --version",
40
46
  "spawn.diagnostic.winFull": "- Ensure Windows 10 build 18309+ (run 'winver' to check)\n- Install Visual C++ Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe\n- Run: npm rebuild node-pty\n- Try running as Administrator",
41
47
  // === Emoji bindings ===
42
48
  "binding.usage": "Invalid format.\nUsage: /bind [emoji] command\nExample: /bind 🚀 deploy --prod",
@@ -59,6 +65,7 @@ export const messages = {
59
65
  "doctor.label.codePage": "Console code page",
60
66
  "doctor.codePage.nonAsciiWarning": "{codePage} (not UTF-8 and the data directory path contains non-ASCII characters; hook scripts from older versions break in this environment — upgrade to the latest release)",
61
67
  "doctor.label.connectivity": "WeChat connectivity",
68
+ "doctor.label.wecomConnectivity": "WeCom connectivity",
62
69
  "doctor.connectivity.ok": "reachable ({baseUrl})",
63
70
  "doctor.connectivity.unreachable": "cannot reach {baseUrl} (check network and firewall)",
64
71
  "doctor.connectivity.proxyHint": "cannot reach {baseUrl}. Proxy {proxy} detected, but Node fetch ignores proxy environment variables by default; if your network requires the proxy, set NODE_USE_ENV_PROXY=1 and retry.",
@@ -82,6 +89,7 @@ export const messages = {
82
89
  "doctor.dataDirMissing": "(will be created on first run)",
83
90
  "doctor.credentialsFound": "found",
84
91
  "doctor.credentialsMissing": "not found (run wechat-setup first)",
92
+ "doctor.credentialsMissingWecom": "not found (run wecom-setup first)",
85
93
  "doctor.mode.bridge": "standalone bridge",
86
94
  "doctor.mode.generic": "generic",
87
95
  "doctor.notSpecified": "(not specified)",
@@ -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
  export const messages = {
2
8
  // === 桥接欢迎 & 控制 ===
3
9
  "bridge.welcome": "微信桥接就绪 ({adapter})。\n工作目录: {cwd}",
@@ -11,7 +17,7 @@ export const messages = {
11
17
  "bridge.fatalError": "桥接错误: {message}",
12
18
  "bridge.backlogIgnored": "已忽略 {count} 条桥接启动前的积压消息(不会自动处理)。\n如果你是在桥接启动后才发送的消息却看到此提示,说明本机系统时钟可能不准(偏差超过 {graceSeconds} 秒),请校准时间后重启桥接。",
13
19
  // === Daemon 欢迎 & 控制 ===
14
- "daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
20
+ "daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status, /model, /plan, /all <prompt>\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
15
21
  "daemon.noActiveAdapter": "未选择活跃终端。发送 /codex、/claude、/opencode 或 /pi 启动一个。",
16
22
  "daemon.switchResult.new": "已启动新的可见 CLI。",
17
23
  "daemon.switchResult.reused": "已复用现有的可见 CLI。",
@@ -25,7 +31,7 @@ export const messages = {
25
31
  // === Hook 健康检查 ===
26
32
  "hook.healthCheck.warning": "[警告] 15 秒内未收到 Claude hook 事件。\nhook 系统可能未正常工作 — Claude 的输出将无法到达微信。",
27
33
  "hook.healthCheck.logHint": "\n查看日志: {logPath}",
28
- "hook.healthCheck.fixes": "\n常见修复方法:\n- 确保 Node.js >= 22.6.0: node --version\n- 重新安装: npm install -g cli-wechat-bridge@latest\n- 检查防火墙: 允许 localhost TCP 连接",
34
+ "hook.healthCheck.fixes": "\n常见修复方法:\n- 确保 Node.js >= 22.13.0: node --version\n- 重新安装: npm install -g cli-wechat-bridge@latest\n- 检查防火墙: 允许 localhost TCP 连接",
29
35
  // === PTY 回退 ===
30
36
  "pty.fallback.warning": "[警告] PTY 不可用 — 已切换到回退模式(TERM=dumb)。\n核心功能(消息收发、审批、会话管理)正常工作;终端渲染(颜色、进度条)可能受限。\n修复方法: Linux 安装 build-essential python3 | Windows 运行 npm rebuild node-pty | macOS 运行 xcode-select --install\n运行 --doctor 查看详细环境信息。\n",
31
37
  // === 启动诊断 ===
@@ -36,7 +42,7 @@ export const messages = {
36
42
  "spawn.diagnostic.linuxBuildTools": "- Linux 需要编译工具来构建 node-pty: sudo apt install build-essential python3\n- 然后重新安装: npm install -g cli-wechat-bridge@latest",
37
43
  "spawn.diagnostic.notFound": "- 命令 \"{target}\" 未在 PATH 中找到。\n- 请确认已安装并可从终端访问。",
38
44
  "spawn.diagnostic.generic": "- 重新安装: npm install -g cli-wechat-bridge@latest",
39
- "spawn.diagnostic.nodeVersion": "- 确保 Node.js >= 22.6.0: node --version",
45
+ "spawn.diagnostic.nodeVersion": "- 确保 Node.js >= 22.13.0: node --version",
40
46
  "spawn.diagnostic.winFull": "- 确保 Windows 10 build 18309+(运行 winver 查看)\n- 安装 Visual C++ Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe\n- 运行: npm rebuild node-pty\n- 尝试以管理员身份运行",
41
47
  // === Emoji 绑定 ===
42
48
  "binding.usage": "格式错误。\n用法: /bind [表情] 命令\n示例: /bind 🚀 deploy --prod",
@@ -59,6 +65,7 @@ export const messages = {
59
65
  "doctor.label.codePage": "控制台代码页",
60
66
  "doctor.codePage.nonAsciiWarning": "{codePage}(非 UTF-8,且数据目录路径含非 ASCII 字符;旧版本的 hook 脚本在此环境下会乱码失效,请升级到最新版本)",
61
67
  "doctor.label.connectivity": "微信服务可达性",
68
+ "doctor.label.wecomConnectivity": "企业微信服务可达性",
62
69
  "doctor.connectivity.ok": "可达 ({baseUrl})",
63
70
  "doctor.connectivity.unreachable": "无法访问 {baseUrl}(检查网络与防火墙)",
64
71
  "doctor.connectivity.proxyHint": "无法访问 {baseUrl}。检测到代理 {proxy},但 Node fetch 默认不使用代理环境变量;若必须经代理出网,请设置 NODE_USE_ENV_PROXY=1 后重试。",
@@ -82,6 +89,7 @@ export const messages = {
82
89
  "doctor.dataDirMissing": "(首次运行会创建)",
83
90
  "doctor.credentialsFound": "已找到",
84
91
  "doctor.credentialsMissing": "未找到 (先运行 wechat-setup)",
92
+ "doctor.credentialsMissingWecom": "未找到 (先运行 wecom-setup)",
85
93
  "doctor.mode.bridge": "独立桥接",
86
94
  "doctor.mode.generic": "通用",
87
95
  "doctor.notSpecified": "(未指定)",
@@ -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 { createBridgeAdapter } from "../bridge/bridge-adapters.js";
2
8
  import { CodexPtyAdapter } from "../bridge/bridge-adapters.codex.js";
3
9
  import { LegacyAdapterRuntime } from "./legacy-adapter-runtime.js";
@@ -25,6 +25,21 @@ export class LegacyAdapterRuntime {
25
25
  }
26
26
  await this.adapter.createSession();
27
27
  }
28
+ async listModels() {
29
+ if (!this.adapter.listModels)
30
+ throw new Error("Model listing is not supported by this companion.");
31
+ return this.adapter.listModels();
32
+ }
33
+ async selectModel(modelId) {
34
+ if (!this.adapter.selectModel)
35
+ throw new Error("Model selection is not supported by this companion.");
36
+ return this.adapter.selectModel(modelId);
37
+ }
38
+ async setPlanMode(enabled) {
39
+ if (!this.adapter.setPlanMode)
40
+ throw new Error("Plan mode is not supported by this companion.");
41
+ return this.adapter.setPlanMode(enabled);
42
+ }
28
43
  async interrupt() {
29
44
  return await this.adapter.interrupt();
30
45
  }
@@ -3,3 +3,6 @@ export const CODEX_REMOTE_AUTH_TOKEN_ENV = "WECHAT_BRIDGE_CODEX_REMOTE_AUTH_TOKE
3
3
  export function hasLocalClientEndpointProvider(runtime) {
4
4
  return typeof runtime.getLocalClientEndpoint === "function";
5
5
  }
6
+ export function hasVisibleClientSessionPreparer(runtime) {
7
+ return typeof runtime.prepareVisibleClientSession === "function";
8
+ }
@@ -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 { ensureChannelDataDir } from "../wechat/channel-config.js";
3
9
  /**
@@ -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 { execFileSync } from "node:child_process";
2
8
  import fs from "node:fs";
3
9
  import net from "node:net";
@@ -74,11 +80,11 @@ function defaultGetWindowsCodePage() {
74
80
  return null;
75
81
  }
76
82
  }
77
- async function defaultFetchServerDate() {
83
+ async function defaultFetchServerDate(baseUrl = DEFAULT_BASE_URL) {
78
84
  const controller = new AbortController();
79
85
  const timer = setTimeout(() => controller.abort(), SERVER_DATE_TIMEOUT_MS);
80
86
  try {
81
- const res = await fetch(DEFAULT_BASE_URL, {
87
+ const res = await fetch(baseUrl, {
82
88
  method: "GET",
83
89
  signal: controller.signal,
84
90
  });
@@ -179,6 +185,7 @@ function isBridgeAdapterKind(value) {
179
185
  export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process.cwd()) {
180
186
  let cwd = fallbackCwd;
181
187
  let adapter;
188
+ let channelId = "wechat";
182
189
  for (let i = 0; i < argv.length; i += 1) {
183
190
  const arg = argv[i];
184
191
  const next = argv[i + 1];
@@ -195,11 +202,17 @@ export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process
195
202
  i += 1;
196
203
  continue;
197
204
  }
205
+ if (arg === "--channel" && (next === "wechat" || next === "wecom")) {
206
+ channelId = next;
207
+ i += 1;
208
+ continue;
209
+ }
198
210
  }
199
211
  return {
200
212
  mode,
201
213
  cwd: path.resolve(cwd),
202
214
  adapter,
215
+ ...(channelId === "wecom" ? { channelId } : {}),
203
216
  };
204
217
  }
205
218
  function statusTag(status) {
@@ -582,10 +595,15 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
582
595
  }
583
596
  }
584
597
  const proxyUrl = pickProxyEnvValue(deps.env);
585
- const serverDate = await deps.fetchServerDate();
598
+ const connectivityUrl = doctorOptions.channelId === "wecom"
599
+ ? "https://open.work.weixin.qq.com"
600
+ : DEFAULT_BASE_URL;
601
+ const serverDate = await deps.fetchServerDate(connectivityUrl);
586
602
  if (serverDate) {
587
- lines.push(row(STATE_OK, msg("label.connectivity"), msg("connectivity.ok", {
588
- baseUrl: DEFAULT_BASE_URL,
603
+ lines.push(row(STATE_OK, msg(doctorOptions.channelId === "wecom"
604
+ ? "label.wecomConnectivity"
605
+ : "label.connectivity"), msg("connectivity.ok", {
606
+ baseUrl: connectivityUrl,
589
607
  })));
590
608
  const skewMs = deps.now() - serverDate.getTime();
591
609
  const skewSeconds = Math.round(Math.abs(skewMs) / 1000);
@@ -595,9 +613,11 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
595
613
  : msg("clock.skewWarning", { skewSeconds })));
596
614
  }
597
615
  else {
598
- lines.push(row(STATE_WARN, msg("label.connectivity"), proxyUrl && deps.env.NODE_USE_ENV_PROXY !== "1"
599
- ? msg("connectivity.proxyHint", { baseUrl: DEFAULT_BASE_URL, proxy: proxyUrl })
600
- : msg("connectivity.unreachable", { baseUrl: DEFAULT_BASE_URL })));
616
+ lines.push(row(STATE_WARN, msg(doctorOptions.channelId === "wecom"
617
+ ? "label.wecomConnectivity"
618
+ : "label.connectivity"), proxyUrl && deps.env.NODE_USE_ENV_PROXY !== "1"
619
+ ? msg("connectivity.proxyHint", { baseUrl: connectivityUrl, proxy: proxyUrl })
620
+ : msg("connectivity.unreachable", { baseUrl: connectivityUrl })));
601
621
  }
602
622
  try {
603
623
  await deps.loadNodePty();
@@ -612,9 +632,15 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
612
632
  const dataDir = deps.resolveDataDir();
613
633
  const dataDirExists = deps.exists(dataDir);
614
634
  lines.push(row(dataDirExists ? STATE_OK : STATE_FAIL, msg("label.dataDir"), dataDirExists ? dataDir : `${dataDir} ${msg("dataDirMissing")}`));
615
- const credFile = path.join(dataDir, "account.json");
635
+ const credFile = doctorOptions.channelId === "wecom"
636
+ ? path.join(dataDir, "wecom", "account.json")
637
+ : path.join(dataDir, "account.json");
616
638
  const credExists = deps.exists(credFile);
617
- lines.push(row(credExists ? STATE_OK : STATE_FAIL, msg("label.credentials"), credExists ? msg("credentialsFound") : msg("credentialsMissing")));
639
+ lines.push(row(credExists ? STATE_OK : STATE_FAIL, msg("label.credentials"), credExists
640
+ ? msg("credentialsFound")
641
+ : msg(doctorOptions.channelId === "wecom"
642
+ ? "credentialsMissingWecom"
643
+ : "credentialsMissing")));
618
644
  section(lines, msg("section.runtime"));
619
645
  lines.push(fieldRow(msg("label.mode"), formatMode(doctorOptions.mode)));
620
646
  lines.push(fieldRow(msg("label.cwd"), doctorOptions.cwd));
@@ -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 { CHANNEL_DATA_DIR, ensureChannelDataDir } from "../wechat/channel-config.js";
@@ -179,16 +185,16 @@ export async function checkForUpdate(forceCheck = false) {
179
185
  */
180
186
  export function formatUpdateMessage(versionInfo) {
181
187
  const { current, latest } = versionInfo;
182
- return `
183
- [Update Available] Version ${latest} is available (current: ${current})
184
-
185
- Update instructions:
186
- cd CLI-WeChat-Bridge
187
- git pull
188
- bun install
189
- npm install -g .
190
-
191
- For more information:
192
- https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
188
+ return `
189
+ [Update Available] Version ${latest} is available (current: ${current})
190
+
191
+ Update instructions:
192
+ cd CLI-WeChat-Bridge
193
+ git pull
194
+ bun install
195
+ npm install -g .
196
+
197
+ For more information:
198
+ https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
193
199
  `;
194
200
  }
@@ -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 os from "node:os";
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env bun
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
  /**
3
9
  * CLI WeChat Bridge setup.
4
10
  *
@@ -11,6 +17,7 @@ import fs from "node:fs";
11
17
  import readline from "node:readline";
12
18
  import { BOT_TYPE, CONTEXT_CACHE_FILE, CREDENTIALS_FILE, DEFAULT_BASE_URL, ensureChannelDataDir, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
13
19
  import { isWechatSyncSessionTimeout } from "./wechat-transport.js";
20
+ import { isDirectModuleRun } from "../core/direct-run.js";
14
21
  const CHANNEL_VERSION = "0.3.0";
15
22
  export function loadExistingCredentials() {
16
23
  try {
@@ -130,18 +137,74 @@ async function askYesNo(prompt) {
130
137
  rl.close();
131
138
  }
132
139
  }
133
- async function printQRCode(qrContent, write) {
140
+ // eslint-disable-next-line no-control-regex
141
+ const ANSI_SGR_RE = /\u001b\[[0-9;]*m/g;
142
+ function resolveLoginTerminal() {
143
+ const columns = process.stdout.columns;
144
+ return {
145
+ platform: process.platform,
146
+ isTTY: process.stdout.isTTY === true,
147
+ columns: typeof columns === "number" && Number.isFinite(columns) && columns > 0
148
+ ? columns
149
+ : undefined,
150
+ };
151
+ }
152
+ export function getQRCodeVisibleWidth(qr) {
153
+ return Math.max(0, ...qr
154
+ .replace(/\r/g, "")
155
+ .split("\n")
156
+ .map((line) => Array.from(line.replace(ANSI_SGR_RE, "")).length));
157
+ }
158
+ export function selectQRCodePresentation(params) {
159
+ if (!params.isTTY ||
160
+ typeof params.columns !== "number" ||
161
+ !Number.isFinite(params.columns) ||
162
+ params.columns <= 0 ||
163
+ params.renderedWidth + 1 > params.columns) {
164
+ return "url_only";
165
+ }
166
+ return "qr_with_url";
167
+ }
168
+ export function formatQRCodeLoginInstruction(presentation) {
169
+ return presentation === "qr_with_url"
170
+ ? "Scan the QR code above with WeChat, then confirm the login on your phone. If scanning fails, open the browser URL above.\n"
171
+ : "The QR code was not rendered in this terminal. Open the browser URL above, then confirm the login in WeChat.\n";
172
+ }
173
+ async function generateQRCode(qrContent, options) {
174
+ const qrterm = await import("qrcode-terminal");
175
+ return await new Promise((resolve, reject) => {
176
+ try {
177
+ qrterm.default.generate(qrContent, options, resolve);
178
+ }
179
+ catch (error) {
180
+ reject(error);
181
+ }
182
+ });
183
+ }
184
+ export async function printQRCode(qrContent, write, terminal = resolveLoginTerminal(), qrMode = "small", qrGenerator = generateQRCode) {
185
+ if (!terminal.isTTY) {
186
+ return "url_only";
187
+ }
134
188
  try {
135
- const qrterm = await import("qrcode-terminal");
136
- await new Promise((resolve) => {
137
- qrterm.default.generate(qrContent, { small: true }, (qr) => {
138
- write(`${qr}\n`);
139
- resolve();
140
- });
189
+ const qr = await qrGenerator(qrContent, {
190
+ small: qrMode === "small",
141
191
  });
192
+ const presentation = selectQRCodePresentation({
193
+ isTTY: terminal.isTTY,
194
+ columns: terminal.columns,
195
+ renderedWidth: getQRCodeVisibleWidth(qr),
196
+ });
197
+ if (presentation === "qr_with_url") {
198
+ write(`${qr}\n`);
199
+ }
200
+ write(presentation === "qr_with_url"
201
+ ? `If scanning fails, open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`
202
+ : `Open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`);
203
+ return presentation;
142
204
  }
143
205
  catch {
144
- write(`Open this QR code URL in a browser: ${qrContent}\n\n`);
206
+ write(`The QR code could not be rendered. Open this short-lived WeChat login URL in a browser. Do not share it:\n${qrContent}\n\n`);
207
+ return "url_only";
145
208
  }
146
209
  }
147
210
  function saveCredentials(account) {
@@ -174,16 +237,54 @@ function printPostLoginHelp(log) {
174
237
  log("");
175
238
  log("Run wechat-setup again any time you need to refresh the login.");
176
239
  }
240
+ export function parseWechatSetupCliArgs(argv) {
241
+ let qrMode = "small";
242
+ for (let index = 0; index < argv.length; index += 1) {
243
+ const arg = argv[index];
244
+ if (arg === "--help" || arg === "-h") {
245
+ return { help: true, qrMode };
246
+ }
247
+ if (arg === "--qr-mode") {
248
+ const value = argv[index + 1];
249
+ if (value !== "small" && value !== "normal") {
250
+ throw new Error("--qr-mode requires small or normal.");
251
+ }
252
+ qrMode = value;
253
+ index += 1;
254
+ continue;
255
+ }
256
+ if (arg === "--qr-mode=small" || arg === "--qr-mode=normal") {
257
+ qrMode = arg.endsWith("=small") ? "small" : "normal";
258
+ continue;
259
+ }
260
+ throw new Error(`Unknown wechat-setup option: ${arg}`);
261
+ }
262
+ return { help: false, qrMode };
263
+ }
264
+ export function formatWechatSetupUsage() {
265
+ return [
266
+ "Usage: wechat-setup [--qr-mode <small|normal>]",
267
+ "",
268
+ "Log in to WeChat and save bridge credentials.",
269
+ "QR mode defaults to small; use --qr-mode normal when the terminal renders small QR blocks incorrectly.",
270
+ "",
271
+ ].join("\n");
272
+ }
177
273
  export async function runWechatLogin(options = {}) {
178
274
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
179
275
  const log = options.log ?? ((message) => console.log(message));
180
276
  const write = options.write ?? ((message) => process.stdout.write(message));
181
277
  const timeoutMs = options.timeoutMs ?? 480_000;
182
278
  const pollIntervalMs = options.pollIntervalMs ?? 1_000;
279
+ const terminal = options.terminal ?? resolveLoginTerminal();
280
+ const qrMode = options.qrMode ?? "small";
281
+ if (!terminal.isTTY) {
282
+ throw new Error("WeChat login requires an interactive terminal. Run wechat-setup directly in PowerShell, Command Prompt, or a terminal window.");
283
+ }
183
284
  log("Fetching WeChat login QR code...\n");
184
285
  const qrResp = await fetchQRCode(baseUrl);
185
- await printQRCode(qrResp.qrcode_img_content, write);
186
- log("Scan the QR code above with WeChat, then confirm the login on your phone.\n");
286
+ const presentation = await printQRCode(qrResp.qrcode_img_content, write, terminal, qrMode);
287
+ log(formatQRCodeLoginInstruction(presentation));
187
288
  const deadline = Date.now() + timeoutMs;
188
289
  let scannedPrinted = false;
189
290
  while (Date.now() < deadline) {
@@ -252,7 +353,12 @@ export async function ensureWechatCredentials(options = {}) {
252
353
  const login = options.login ?? (() => runWechatLogin(options));
253
354
  return login();
254
355
  }
255
- async function main() {
356
+ async function main(argv = process.argv.slice(2)) {
357
+ const cliOptions = parseWechatSetupCliArgs(argv);
358
+ if (cliOptions.help) {
359
+ console.log(formatWechatSetupUsage());
360
+ return;
361
+ }
256
362
  migrateLegacyChannelFiles((message) => console.log(message));
257
363
  const existing = loadExistingCredentials();
258
364
  if (existing) {
@@ -266,10 +372,10 @@ async function main() {
266
372
  return;
267
373
  }
268
374
  }
269
- await runWechatLogin({ requireUserId: true });
375
+ await runWechatLogin({ requireUserId: true, qrMode: cliOptions.qrMode });
270
376
  printPostLoginHelp((message) => console.log(message));
271
377
  }
272
- const isDirectRun = Boolean(import.meta.main);
378
+ const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
273
379
  if (isDirectRun) {
274
380
  main().catch((err) => {
275
381
  const message = err instanceof Error ? err.message : String(err);
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env bun
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
  /**
3
9
  * WeChat MCP server.
4
10
  *