@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +467 -475
  3. package/bin/_run-entry.mjs +29 -6
  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 +161 -4
  20. package/dist/bridge/bridge-adapters.codex.js +118 -7
  21. package/dist/bridge/bridge-adapters.core.js +25 -1
  22. package/dist/bridge/bridge-adapters.opencode.js +156 -1
  23. package/dist/bridge/bridge-adapters.pi.js +45 -0
  24. package/dist/bridge/bridge-adapters.shared.js +6 -0
  25. package/dist/bridge/bridge-controller.js +6 -0
  26. package/dist/bridge/bridge-final-reply.js +6 -0
  27. package/dist/bridge/bridge-process-reaper.js +41 -2
  28. package/dist/bridge/bridge-state.js +16 -3
  29. package/dist/bridge/bridge-utils.js +6 -31
  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 +380 -171
  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 +49 -3
  41. package/dist/channels/wecom/bridge-final-reply.js +32 -0
  42. package/dist/channels/wecom/setup-cli.js +12 -0
  43. package/dist/channels/wecom/setup.js +182 -0
  44. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  45. package/dist/channels/wecom/wecom-config.js +72 -0
  46. package/dist/channels/wecom/wecom-driver.js +106 -0
  47. package/dist/channels/wecom/wecom-media.js +147 -0
  48. package/dist/channels/wecom/wecom-message.js +149 -0
  49. package/dist/channels/wecom/wecom-transport.js +314 -0
  50. package/dist/commands/check-update.js +6 -0
  51. package/dist/companion/codex-remote-client.js +30 -3
  52. package/dist/companion/codex-visible-client-link.js +107 -0
  53. package/dist/companion/local-companion-link.js +6 -0
  54. package/dist/companion/local-companion-start.js +62 -11
  55. package/dist/companion/local-companion.js +25 -1
  56. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  57. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  58. package/dist/core/bridge-control.js +10 -0
  59. package/dist/core/bridge-defer.js +25 -0
  60. package/dist/core/channel-driver.js +1 -0
  61. package/dist/core/channel-types.js +3 -1
  62. package/dist/core/conversation-routing.js +58 -0
  63. package/dist/core/direct-run.js +23 -0
  64. package/dist/core/inbound-message-claims.js +67 -0
  65. package/dist/core/outbound-target.js +32 -0
  66. package/dist/core/text-utils.js +6 -0
  67. package/dist/core/turn-coordinator.js +85 -0
  68. package/dist/daemon/daemon-link.js +9 -0
  69. package/dist/daemon/emoji-bindings.js +6 -0
  70. package/dist/daemon/wechat-daemon.js +675 -122
  71. package/dist/i18n/index.js +6 -0
  72. package/dist/i18n/messages-en.js +9 -1
  73. package/dist/i18n/messages-zh.js +9 -1
  74. package/dist/runtime/create-runtime-host.js +6 -0
  75. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  76. package/dist/runtime/runtime-types.js +3 -0
  77. package/dist/utils/atomic-file.js +6 -0
  78. package/dist/utils/doctor.js +36 -10
  79. package/dist/utils/version-checker.js +17 -11
  80. package/dist/wechat/channel-config.js +6 -0
  81. package/dist/wechat/setup.js +119 -13
  82. package/dist/wechat/wechat-channel.js +6 -0
  83. package/dist/wechat/wechat-transport.js +169 -58
  84. package/package.json +13 -3
  85. 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 crypto from "node:crypto";
2
8
  import { createCipheriv, createDecipheriv } from "node:crypto";
3
9
  import fs from "node:fs";
@@ -332,6 +338,15 @@ async function apiFetch(params) {
332
338
  const url = new URL(params.endpoint, base).toString();
333
339
  const controller = new AbortController();
334
340
  const timer = setTimeout(() => controller.abort(), params.timeoutMs);
341
+ const abortFromCaller = () => controller.abort(params.signal?.reason);
342
+ if (params.signal) {
343
+ if (params.signal.aborted) {
344
+ abortFromCaller();
345
+ }
346
+ else {
347
+ params.signal.addEventListener("abort", abortFromCaller, { once: true });
348
+ }
349
+ }
335
350
  try {
336
351
  const res = await fetch(url, {
337
352
  method: "POST",
@@ -353,6 +368,9 @@ async function apiFetch(params) {
353
368
  clearTimeout(timer);
354
369
  throw err;
355
370
  }
371
+ finally {
372
+ params.signal?.removeEventListener("abort", abortFromCaller);
373
+ }
356
374
  }
357
375
  function encryptAesEcb(plaintext, key) {
358
376
  const cipher = createCipheriv("aes-128-ecb", key, null);
@@ -806,12 +824,15 @@ function buildInboundAttachmentFilePath(params) {
806
824
  const uniquePrefix = `${timestamp.replace(/[:.]/g, "-")}-${crypto.randomBytes(4).toString("hex")}`;
807
825
  return path.join(directory, `${uniquePrefix}-${safeFileName}`);
808
826
  }
827
+ const WECHAT_TYPING_TIMEOUT_MS = 8_000;
809
828
  export class WeChatTransport {
810
829
  logger;
811
830
  recentMessageKeys = new Set();
812
831
  recentMessageOrder = [];
813
832
  contextTokenCache;
814
833
  syncBuffer = "";
834
+ stopped = false;
835
+ activePollControllers = new Set();
815
836
  constructor(logger) {
816
837
  this.logger = logger;
817
838
  migrateLegacyChannelFiles((message) => this.logger.log(message));
@@ -855,73 +876,96 @@ export class WeChatTransport {
855
876
  ? "Reset sync state and cleared cached context tokens."
856
877
  : "Reset sync state.";
857
878
  }
879
+ stop() {
880
+ if (this.stopped) {
881
+ return;
882
+ }
883
+ this.stopped = true;
884
+ for (const controller of this.activePollControllers) {
885
+ controller.abort();
886
+ }
887
+ this.activePollControllers.clear();
888
+ }
858
889
  async pollMessages(options = {}) {
890
+ if (this.stopped) {
891
+ return { messages: [], ignoredBacklogCount: 0 };
892
+ }
859
893
  const timeoutMs = options.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
860
894
  const account = this.requireAccount();
861
- let response = await this.getUpdates(account, timeoutMs);
862
- if (isWechatSyncSessionTimeout(response) && this.syncBuffer) {
863
- this.logger.log("WeChat sync session timed out. Clearing local sync cursor and retrying once.");
864
- this.clearSyncBuffer();
865
- response = await this.getUpdates(account, timeoutMs);
866
- }
867
- if (isWechatSyncSessionTimeout(response)) {
868
- throw new Error('WeChat session timed out. Run "wechat-setup" to log in again.');
869
- }
870
- const isError = (response.ret !== undefined && response.ret !== 0) ||
871
- (response.errcode !== undefined && response.errcode !== 0);
872
- if (isError) {
873
- throw new Error(`getUpdates failed: ret=${response.ret} errcode=${response.errcode} errmsg=${response.errmsg ?? ""}`);
874
- }
875
- if (response.get_updates_buf) {
876
- this.syncBuffer = response.get_updates_buf;
877
- this.saveSyncBuffer(this.syncBuffer);
878
- }
879
- const messages = [];
880
- let ignoredBacklogCount = 0;
881
- for (const rawMessage of response.msgs ?? []) {
882
- if (rawMessage.message_type !== MSG_TYPE_USER) {
883
- continue;
895
+ const controller = new AbortController();
896
+ this.activePollControllers.add(controller);
897
+ try {
898
+ let response = await this.getUpdates(account, timeoutMs, controller.signal);
899
+ if (isWechatSyncSessionTimeout(response) && this.syncBuffer && !this.stopped) {
900
+ this.logger.log("WeChat sync session timed out. Clearing local sync cursor and retrying once.");
901
+ this.clearSyncBuffer();
902
+ response = await this.getUpdates(account, timeoutMs, controller.signal);
884
903
  }
885
- const extracted = extractInboundMessageContent(rawMessage);
886
- if (!extracted.text && extracted.attachments.length === 0) {
887
- continue;
904
+ if (this.stopped) {
905
+ return { messages: [], ignoredBacklogCount: 0 };
888
906
  }
889
- const senderId = rawMessage.from_user_id ?? "unknown";
890
- if (rawMessage.context_token) {
891
- this.cacheContextToken(senderId, rawMessage.context_token);
907
+ if (isWechatSyncSessionTimeout(response)) {
908
+ throw new Error('WeChat session timed out. Run "wechat-setup" to log in again.');
892
909
  }
893
- // Filter pre-start backlog BEFORE claiming/remembering the message so a
894
- // skipped message is not permanently locked away from other processes.
895
- // A missing create_time_ms is treated as fresh rather than dropped.
896
- const createdAtMs = rawMessage.create_time_ms ?? 0;
897
- if (typeof options.minCreatedAtMs === "number" &&
898
- Number.isFinite(createdAtMs) &&
899
- createdAtMs > 0 &&
900
- createdAtMs < options.minCreatedAtMs) {
901
- ignoredBacklogCount += 1;
902
- continue;
910
+ const isError = (response.ret !== undefined && response.ret !== 0) ||
911
+ (response.errcode !== undefined && response.errcode !== 0);
912
+ if (isError) {
913
+ throw new Error(`getUpdates failed: ret=${response.ret} errcode=${response.errcode} errmsg=${response.errmsg ?? ""}`);
903
914
  }
904
- const messageKey = buildMessageKey(rawMessage);
905
- if (!this.rememberMessage(messageKey)) {
906
- continue;
915
+ if (response.get_updates_buf) {
916
+ this.syncBuffer = response.get_updates_buf;
917
+ this.saveSyncBuffer(this.syncBuffer);
907
918
  }
908
- if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
909
- continue;
919
+ const messages = [];
920
+ let ignoredBacklogCount = 0;
921
+ for (const rawMessage of response.msgs ?? []) {
922
+ if (rawMessage.message_type !== MSG_TYPE_USER) {
923
+ continue;
924
+ }
925
+ const extracted = extractInboundMessageContent(rawMessage);
926
+ if (!extracted.text && extracted.attachments.length === 0) {
927
+ continue;
928
+ }
929
+ const senderId = rawMessage.from_user_id ?? "unknown";
930
+ if (rawMessage.context_token) {
931
+ this.cacheContextToken(senderId, rawMessage.context_token);
932
+ }
933
+ // Filter pre-start backlog BEFORE claiming/remembering the message so a
934
+ // skipped message is not permanently locked away from other processes.
935
+ // A missing create_time_ms is treated as fresh rather than dropped.
936
+ const createdAtMs = rawMessage.create_time_ms ?? 0;
937
+ if (typeof options.minCreatedAtMs === "number" &&
938
+ Number.isFinite(createdAtMs) &&
939
+ createdAtMs > 0 &&
940
+ createdAtMs < options.minCreatedAtMs) {
941
+ ignoredBacklogCount += 1;
942
+ continue;
943
+ }
944
+ const messageKey = buildMessageKey(rawMessage);
945
+ if (!this.rememberMessage(messageKey)) {
946
+ continue;
947
+ }
948
+ if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
949
+ continue;
950
+ }
951
+ const { attachments, failureLines } = await this.downloadInboundAttachments(extracted.attachments, rawMessage);
952
+ const text = appendInboundAttachmentFailureText(extracted.text, failureLines);
953
+ messages.push({
954
+ senderId,
955
+ sender: normalizeSender(senderId),
956
+ sessionId: rawMessage.session_id ?? "",
957
+ text,
958
+ attachments,
959
+ contextToken: rawMessage.context_token,
960
+ createdAt: formatTimestamp(rawMessage.create_time_ms),
961
+ createdAtMs,
962
+ });
910
963
  }
911
- const { attachments, failureLines } = await this.downloadInboundAttachments(extracted.attachments, rawMessage);
912
- const text = appendInboundAttachmentFailureText(extracted.text, failureLines);
913
- messages.push({
914
- senderId,
915
- sender: normalizeSender(senderId),
916
- sessionId: rawMessage.session_id ?? "",
917
- text,
918
- attachments,
919
- contextToken: rawMessage.context_token,
920
- createdAt: formatTimestamp(rawMessage.create_time_ms),
921
- createdAtMs,
922
- });
964
+ return { messages, ignoredBacklogCount };
965
+ }
966
+ finally {
967
+ this.activePollControllers.delete(controller);
923
968
  }
924
- return { messages, ignoredBacklogCount };
925
969
  }
926
970
  async downloadInboundAttachments(descriptors, rawMessage) {
927
971
  const attachments = [];
@@ -963,6 +1007,72 @@ export class WeChatTransport {
963
1007
  }
964
1008
  return { attachments, failureLines };
965
1009
  }
1010
+ /** Typing status values for {@link sendTyping}. 1 = typing, 2 = cancel. */
1011
+ static TYPING_STATUS = { TYPING: 1, CANCEL: 2 };
1012
+ /**
1013
+ * Fetch the per-user typing ticket from ilink/bot/getconfig.
1014
+ * Returns "" when the endpoint fails or reports no ticket.
1015
+ */
1016
+ async fetchTypingTicket(recipientId) {
1017
+ const account = this.requireAccount();
1018
+ const resolvedRecipientId = recipientId?.trim() ||
1019
+ [...this.contextTokenCache.keys()].at(-1);
1020
+ if (!resolvedRecipientId) {
1021
+ return "";
1022
+ }
1023
+ try {
1024
+ const raw = await apiFetch({
1025
+ baseUrl: account.baseUrl,
1026
+ endpoint: "ilink/bot/getconfig",
1027
+ body: JSON.stringify({
1028
+ ilink_user_id: resolvedRecipientId,
1029
+ ...(this.contextTokenCache.has(resolvedRecipientId)
1030
+ ? { context_token: this.contextTokenCache.get(resolvedRecipientId) }
1031
+ : {}),
1032
+ }),
1033
+ token: account.token,
1034
+ timeoutMs: WECHAT_TYPING_TIMEOUT_MS,
1035
+ });
1036
+ const resp = JSON.parse(raw);
1037
+ return resp.ret === 0 ? (resp.typing_ticket ?? "") : "";
1038
+ }
1039
+ catch {
1040
+ return "";
1041
+ }
1042
+ }
1043
+ /**
1044
+ * Send or cancel a "typing…" indicator. Failures are logged only; the
1045
+ * indicator is best-effort and never blocks message flow.
1046
+ */
1047
+ async sendTyping(recipientId, typingTicket, status) {
1048
+ if (!typingTicket) {
1049
+ return false;
1050
+ }
1051
+ const account = this.requireAccount();
1052
+ try {
1053
+ const raw = await apiFetch({
1054
+ baseUrl: account.baseUrl,
1055
+ endpoint: "ilink/bot/sendtyping",
1056
+ body: JSON.stringify({
1057
+ ilink_user_id: recipientId,
1058
+ typing_ticket: typingTicket,
1059
+ status,
1060
+ }),
1061
+ token: account.token,
1062
+ timeoutMs: WECHAT_TYPING_TIMEOUT_MS,
1063
+ });
1064
+ const resp = JSON.parse(raw);
1065
+ if (resp.ret !== undefined && resp.ret !== 0) {
1066
+ this.logger.logError(`sendTyping ret=${resp.ret} errmsg=${resp.errmsg ?? "(none)"}`);
1067
+ return false;
1068
+ }
1069
+ return true;
1070
+ }
1071
+ catch (error) {
1072
+ this.logger.logError(`sendTyping failed: ${error instanceof Error ? error.message : String(error)}`);
1073
+ return false;
1074
+ }
1075
+ }
966
1076
  async sendText(senderId, text) {
967
1077
  const trimmed = text.trim();
968
1078
  if (!trimmed) {
@@ -1166,7 +1276,7 @@ export class WeChatTransport {
1166
1276
  }
1167
1277
  return stat;
1168
1278
  }
1169
- async getUpdates(account, timeoutMs) {
1279
+ async getUpdates(account, timeoutMs, signal) {
1170
1280
  try {
1171
1281
  const raw = await apiFetch({
1172
1282
  baseUrl: account.baseUrl,
@@ -1177,6 +1287,7 @@ export class WeChatTransport {
1177
1287
  }),
1178
1288
  token: account.token,
1179
1289
  timeoutMs,
1290
+ signal,
1180
1291
  });
1181
1292
  return JSON.parse(raw);
1182
1293
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unlinearity/cli-wechat-bridge",
3
- "version": "1.1.6",
4
- "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and Pi over WeChat",
3
+ "version": "1.2.0",
4
+ "description": "CLI bridge for Codex, Claude Code, OpenCode, and Pi over WeChat and WeCom",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "bin/",
@@ -12,6 +12,7 @@
12
12
  ],
13
13
  "keywords": [
14
14
  "wechat",
15
+ "wecom",
15
16
  "cli",
16
17
  "bridge",
17
18
  "codex",
@@ -40,11 +41,20 @@
40
41
  "wechat-pi": "bin/wechat-pi.mjs",
41
42
  "wechat-daemon": "bin/wechat-daemon.mjs",
42
43
  "wechat-setup": "bin/wechat-setup.mjs",
43
- "wechat-check-update": "bin/wechat-check-update.mjs"
44
+ "wechat-check-update": "bin/wechat-check-update.mjs",
45
+ "wecom-codex": "bin/wecom-codex.mjs",
46
+ "wecom-claude": "bin/wecom-claude.mjs",
47
+ "wecom-opencode": "bin/wecom-opencode.mjs",
48
+ "wecom-pi": "bin/wecom-pi.mjs",
49
+ "wecom-daemon": "bin/wecom-daemon.mjs",
50
+ "wecom-setup": "bin/wecom-setup.mjs",
51
+ "wecom-check-update": "bin/wecom-check-update.mjs"
44
52
  },
45
53
  "dependencies": {
46
54
  "@modelcontextprotocol/sdk": "^1.30.0",
47
55
  "@opencode-ai/sdk": "^1.18.0",
56
+ "@wecom/aibot-node-sdk": "1.0.7",
57
+ "@xterm/headless": "^6.0.0",
48
58
  "node-pty": "^1.1.0",
49
59
  "qrcode-terminal": "^0.12.0"
50
60
  },
@@ -1,73 +1,73 @@
1
- #!/usr/bin/env node
2
-
3
- // Ensure the bundled node-pty `spawn-helper` is executable.
4
- //
5
- // node-pty ships its prebuilt `spawn-helper` binaries with mode 0644 (no exec
6
- // bit) in the published tarball. On macOS/Linux that makes `posix_spawnp`
7
- // fail when node-pty tries to fork a PTY, so the bridge silently drops to the
8
- // non-PTY fallback. The Claude adapter relies on PTY interactive mode, so the
9
- // fallback surfaces to users as:
10
- //
11
- // Error: Input must be provided ... when using --print
12
- //
13
- // Restoring the exec bit after install fixes PTY spawning. This runs on
14
- // postinstall and is idempotent; it never fails the install.
15
-
16
- import fs from "node:fs";
17
- import path from "node:path";
18
- import { createRequire } from "node:module";
19
-
20
- if (process.platform === "win32") {
21
- process.exit(0);
22
- }
23
-
24
- const require = createRequire(import.meta.url);
25
-
26
- let nodePtyDir;
27
- try {
28
- nodePtyDir = path.dirname(require.resolve("node-pty/package.json"));
29
- } catch {
30
- // node-pty is optional in unsupported environments; nothing to fix.
31
- process.exit(0);
32
- }
33
-
34
- const candidates = [
35
- "prebuilds/darwin-arm64/spawn-helper",
36
- "prebuilds/darwin-x64/spawn-helper",
37
- "prebuilds/linux-x64/spawn-helper",
38
- "prebuilds/linux-arm64/spawn-helper",
39
- "build/Release/spawn-helper",
40
- ];
41
-
42
- let fixed = 0;
43
- for (const relative of candidates) {
44
- const helperPath = path.join(nodePtyDir, relative);
45
- let stats;
46
- try {
47
- stats = fs.statSync(helperPath);
48
- } catch {
49
- continue;
50
- }
51
- if (!stats.isFile()) {
52
- continue;
53
- }
54
- if ((stats.mode & 0o111) !== 0) {
55
- continue;
56
- }
57
- try {
58
- fs.chmodSync(helperPath, stats.mode | 0o755);
59
- fixed += 1;
60
- } catch (error) {
61
- console.warn(
62
- `[cli-wechat-bridge] Could not make node-pty spawn-helper executable: ${helperPath}\n` +
63
- ` ${error instanceof Error ? error.message : String(error)}\n` +
64
- " PTY mode may be unavailable; run --doctor for details.",
65
- );
66
- }
67
- }
68
-
69
- if (fixed > 0) {
70
- console.log(
71
- `[cli-wechat-bridge] Restored executable bit on ${fixed} node-pty spawn-helper binary(ies).`,
72
- );
73
- }
1
+ #!/usr/bin/env node
2
+
3
+ // Ensure the bundled node-pty `spawn-helper` is executable.
4
+ //
5
+ // node-pty ships its prebuilt `spawn-helper` binaries with mode 0644 (no exec
6
+ // bit) in the published tarball. On macOS/Linux that makes `posix_spawnp`
7
+ // fail when node-pty tries to fork a PTY, so the bridge silently drops to the
8
+ // non-PTY fallback. The Claude adapter relies on PTY interactive mode, so the
9
+ // fallback surfaces to users as:
10
+ //
11
+ // Error: Input must be provided ... when using --print
12
+ //
13
+ // Restoring the exec bit after install fixes PTY spawning. This runs on
14
+ // postinstall and is idempotent; it never fails the install.
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { createRequire } from "node:module";
19
+
20
+ if (process.platform === "win32") {
21
+ process.exit(0);
22
+ }
23
+
24
+ const require = createRequire(import.meta.url);
25
+
26
+ let nodePtyDir;
27
+ try {
28
+ nodePtyDir = path.dirname(require.resolve("node-pty/package.json"));
29
+ } catch {
30
+ // node-pty is optional in unsupported environments; nothing to fix.
31
+ process.exit(0);
32
+ }
33
+
34
+ const candidates = [
35
+ "prebuilds/darwin-arm64/spawn-helper",
36
+ "prebuilds/darwin-x64/spawn-helper",
37
+ "prebuilds/linux-x64/spawn-helper",
38
+ "prebuilds/linux-arm64/spawn-helper",
39
+ "build/Release/spawn-helper",
40
+ ];
41
+
42
+ let fixed = 0;
43
+ for (const relative of candidates) {
44
+ const helperPath = path.join(nodePtyDir, relative);
45
+ let stats;
46
+ try {
47
+ stats = fs.statSync(helperPath);
48
+ } catch {
49
+ continue;
50
+ }
51
+ if (!stats.isFile()) {
52
+ continue;
53
+ }
54
+ if ((stats.mode & 0o111) !== 0) {
55
+ continue;
56
+ }
57
+ try {
58
+ fs.chmodSync(helperPath, stats.mode | 0o755);
59
+ fixed += 1;
60
+ } catch (error) {
61
+ console.warn(
62
+ `[cli-wechat-bridge] Could not make node-pty spawn-helper executable: ${helperPath}\n` +
63
+ ` ${error instanceof Error ? error.message : String(error)}\n` +
64
+ " PTY mode may be unavailable; run --doctor for details.",
65
+ );
66
+ }
67
+ }
68
+
69
+ if (fixed > 0) {
70
+ console.log(
71
+ `[cli-wechat-bridge] Restored executable bit on ${fixed} node-pty spawn-helper binary(ies).`,
72
+ );
73
+ }