cli-wechat-bridge 1.1.1 → 1.1.3
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 +60 -43
- package/bin/_run-entry.mjs +24 -0
- package/bin/wechat-bridge-pi.mjs +5 -0
- package/bin/wechat-pi-start.mjs +5 -0
- package/bin/wechat-pi.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +2 -0
- package/dist/bridge/bridge-adapters.claude.js +18 -0
- package/dist/bridge/bridge-adapters.codex.js +83 -2
- package/dist/bridge/bridge-adapters.core.js +36 -2
- package/dist/bridge/bridge-adapters.js +5 -0
- package/dist/bridge/bridge-adapters.opencode.js +278 -32
- package/dist/bridge/bridge-adapters.pi.js +697 -0
- package/dist/bridge/bridge-adapters.shared.js +44 -46
- package/dist/bridge/bridge-final-reply.js +8 -4
- package/dist/bridge/bridge-process-reaper.js +58 -3
- package/dist/bridge/bridge-state.js +15 -6
- package/dist/bridge/bridge-utils.js +129 -30
- package/dist/bridge/claude-hook.js +7 -0
- package/dist/bridge/claude-hooks.js +10 -2
- package/dist/bridge/wechat-bridge.js +20 -8
- package/dist/commands/check-update.js +15 -4
- package/dist/companion/local-companion-link.js +6 -1
- package/dist/companion/local-companion-start.js +29 -8
- package/dist/companion/local-companion.js +3 -3
- package/dist/companion/pi-tui-bridge-extension.js +256 -0
- package/dist/daemon/wechat-daemon.js +126 -25
- package/dist/i18n/messages-en.js +12 -2
- package/dist/i18n/messages-zh.js +12 -2
- package/dist/utils/doctor.js +85 -2
- package/dist/utils/version-checker.js +77 -44
- package/dist/wechat/channel-config.js +28 -0
- package/dist/wechat/setup.js +2 -0
- package/dist/wechat/wechat-transport.js +49 -13
- package/package.json +120 -100
- package/scripts/ensure-node-pty-permissions.mjs +73 -0
|
@@ -18,6 +18,34 @@ export const SYNC_BUF_FILE = path.join(CHANNEL_DATA_DIR, "sync_buf.txt");
|
|
|
18
18
|
export const CONTEXT_CACHE_FILE = path.join(CHANNEL_DATA_DIR, "context_tokens.json");
|
|
19
19
|
export const BRIDGE_STATE_FILE = path.join(CHANNEL_DATA_DIR, "bridge-state.json");
|
|
20
20
|
export const BRIDGE_LOG_FILE = path.join(CHANNEL_DATA_DIR, "bridge.log");
|
|
21
|
+
// Hard cap for bridge.log. Long-running daemons otherwise grow it without bound.
|
|
22
|
+
export const BRIDGE_LOG_MAX_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
23
|
+
/**
|
|
24
|
+
* Append a line to a log file, trimming it down to the tail half whenever it
|
|
25
|
+
* exceeds BRIDGE_LOG_MAX_BYTES. Trimming is best-effort under concurrent
|
|
26
|
+
* writers (a few lines may be lost during a trim) but keeps the file bounded.
|
|
27
|
+
*/
|
|
28
|
+
export function appendBoundedLog(filePath, line) {
|
|
29
|
+
try {
|
|
30
|
+
const stat = fs.statSync(filePath);
|
|
31
|
+
if (stat.size > BRIDGE_LOG_MAX_BYTES) {
|
|
32
|
+
const keepSize = Math.floor(BRIDGE_LOG_MAX_BYTES / 2);
|
|
33
|
+
const fd = fs.openSync(filePath, "r");
|
|
34
|
+
try {
|
|
35
|
+
const tail = Buffer.alloc(keepSize);
|
|
36
|
+
const bytesRead = fs.readSync(fd, tail, 0, keepSize, stat.size - keepSize);
|
|
37
|
+
fs.writeFileSync(filePath, tail.subarray(0, bytesRead));
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
fs.closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// File missing or unreadable: fall through to a plain append.
|
|
46
|
+
}
|
|
47
|
+
fs.appendFileSync(filePath, line);
|
|
48
|
+
}
|
|
21
49
|
export const BRIDGE_LOCK_FILE = path.join(CHANNEL_DATA_DIR, "bridge.lock.json");
|
|
22
50
|
export const DAEMON_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "daemon-endpoint.json");
|
|
23
51
|
export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-panel-endpoint.json");
|
package/dist/wechat/setup.js
CHANGED
|
@@ -169,12 +169,14 @@ function printPostLoginHelp(log) {
|
|
|
169
169
|
log(" wechat-codex-start");
|
|
170
170
|
log(" wechat-claude-start");
|
|
171
171
|
log(" wechat-opencode-start");
|
|
172
|
+
log(" wechat-pi-start");
|
|
172
173
|
log(" wechat-bridge-shell");
|
|
173
174
|
log("");
|
|
174
175
|
log("Manual two-terminal mode is also available:");
|
|
175
176
|
log(" wechat-bridge-codex + wechat-codex");
|
|
176
177
|
log(" wechat-bridge-claude + wechat-claude");
|
|
177
178
|
log(" wechat-bridge-opencode + wechat-opencode");
|
|
179
|
+
log(" wechat-bridge-pi + wechat-pi");
|
|
178
180
|
log("");
|
|
179
181
|
log("Run wechat-setup again any time you need to refresh the login.");
|
|
180
182
|
}
|
|
@@ -306,7 +306,12 @@ export function classifyWechatTransportError(error) {
|
|
|
306
306
|
}
|
|
307
307
|
function writeJsonFile(filePath, value) {
|
|
308
308
|
ensureChannelDataDir();
|
|
309
|
-
|
|
309
|
+
const data = JSON.stringify(value, null, 2);
|
|
310
|
+
// Atomic write via temp file + rename, so a crash mid-write cannot leave a
|
|
311
|
+
// truncated/half-written JSON that would break readers on the next launch.
|
|
312
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
313
|
+
fs.writeFileSync(tempPath, data, "utf-8");
|
|
314
|
+
fs.renameSync(tempPath, filePath);
|
|
310
315
|
}
|
|
311
316
|
function randomWechatUin() {
|
|
312
317
|
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
@@ -445,7 +450,15 @@ async function getUploadUrl(account, params) {
|
|
|
445
450
|
token: account.token,
|
|
446
451
|
timeoutMs: SEND_TIMEOUT_MS,
|
|
447
452
|
});
|
|
448
|
-
|
|
453
|
+
try {
|
|
454
|
+
return JSON.parse(raw);
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
// A 200-OK but non-JSON body (e.g. an HTML error page, empty body) would
|
|
458
|
+
// otherwise throw an unclassified SyntaxError that bypasses transport error
|
|
459
|
+
// classification/retry. Surface it as a meaningful error instead.
|
|
460
|
+
throw new Error(`getUploadUrl returned a non-JSON response (${raw.length} bytes): ${String(error)}`, { cause: error });
|
|
461
|
+
}
|
|
449
462
|
}
|
|
450
463
|
function buildCdnUploadUrl(cdnBaseUrl, uploadParam, filekey) {
|
|
451
464
|
return `${cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`;
|
|
@@ -678,11 +691,25 @@ export function buildInboundMessageClaimPath(messageKey, claimsDir = INBOUND_MES
|
|
|
678
691
|
return path.join(claimsDir, fileName);
|
|
679
692
|
}
|
|
680
693
|
export function clearInboundMessageClaims(claimsDir = INBOUND_MESSAGE_CLAIMS_DIR) {
|
|
694
|
+
// Delete claim files individually from a directory snapshot rather than
|
|
695
|
+
// `rmSync(claimsDir, { recursive })`. A recursive rmdir can remove a file a
|
|
696
|
+
// concurrent process just created via tryClaimInboundMessage, silently
|
|
697
|
+
// invalidating its claim and letting the same message be double-processed.
|
|
698
|
+
// Per-file removal only deletes files present at snapshot time.
|
|
699
|
+
let entries;
|
|
681
700
|
try {
|
|
682
|
-
fs.
|
|
701
|
+
entries = fs.readdirSync(claimsDir);
|
|
683
702
|
}
|
|
684
703
|
catch {
|
|
685
|
-
//
|
|
704
|
+
return; // directory does not exist; nothing to clear
|
|
705
|
+
}
|
|
706
|
+
for (const entry of entries) {
|
|
707
|
+
try {
|
|
708
|
+
fs.rmSync(path.join(claimsDir, entry), { force: true });
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// best effort
|
|
712
|
+
}
|
|
686
713
|
}
|
|
687
714
|
}
|
|
688
715
|
export function tryClaimInboundMessage(messageKey, options = {}) {
|
|
@@ -719,7 +746,11 @@ export function tryClaimInboundMessage(messageKey, options = {}) {
|
|
|
719
746
|
? error.code
|
|
720
747
|
: "";
|
|
721
748
|
if (code !== "EEXIST") {
|
|
722
|
-
|
|
749
|
+
// Real persistence failure (permissions, disk full, missing dir, ...).
|
|
750
|
+
// Returning true here would claim success WITHOUT writing the claim file,
|
|
751
|
+
// letting concurrent processes all think they own this message and
|
|
752
|
+
// double-process it. Fail closed so the message is retried instead.
|
|
753
|
+
return false;
|
|
723
754
|
}
|
|
724
755
|
}
|
|
725
756
|
try {
|
|
@@ -849,23 +880,28 @@ export class WeChatTransport {
|
|
|
849
880
|
if (!extracted.text && extracted.attachments.length === 0) {
|
|
850
881
|
continue;
|
|
851
882
|
}
|
|
852
|
-
const messageKey = buildMessageKey(rawMessage);
|
|
853
|
-
if (!this.rememberMessage(messageKey)) {
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
883
|
const senderId = rawMessage.from_user_id ?? "unknown";
|
|
860
884
|
if (rawMessage.context_token) {
|
|
861
885
|
this.cacheContextToken(senderId, rawMessage.context_token);
|
|
862
886
|
}
|
|
887
|
+
// Filter pre-start backlog BEFORE claiming/remembering the message so a
|
|
888
|
+
// skipped message is not permanently locked away from other processes.
|
|
889
|
+
// A missing create_time_ms is treated as fresh rather than dropped.
|
|
863
890
|
const createdAtMs = rawMessage.create_time_ms ?? 0;
|
|
864
891
|
if (typeof options.minCreatedAtMs === "number" &&
|
|
865
|
-
|
|
892
|
+
Number.isFinite(createdAtMs) &&
|
|
893
|
+
createdAtMs > 0 &&
|
|
894
|
+
createdAtMs < options.minCreatedAtMs) {
|
|
866
895
|
ignoredBacklogCount += 1;
|
|
867
896
|
continue;
|
|
868
897
|
}
|
|
898
|
+
const messageKey = buildMessageKey(rawMessage);
|
|
899
|
+
if (!this.rememberMessage(messageKey)) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
869
905
|
const { attachments, failureLines } = await this.downloadInboundAttachments(extracted.attachments, rawMessage);
|
|
870
906
|
const text = appendInboundAttachmentFailureText(extracted.text, failureLines);
|
|
871
907
|
messages.push({
|
package/package.json
CHANGED
|
@@ -1,100 +1,120 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cli-wechat-bridge",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"files": [
|
|
7
|
-
"bin/",
|
|
8
|
-
"dist/",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"wechat-
|
|
38
|
-
"wechat-codex
|
|
39
|
-
"wechat-
|
|
40
|
-
"wechat-
|
|
41
|
-
"wechat-
|
|
42
|
-
"wechat-
|
|
43
|
-
"wechat-bridge-
|
|
44
|
-
"wechat-
|
|
45
|
-
"wechat-opencode
|
|
46
|
-
"wechat-
|
|
47
|
-
"wechat-
|
|
48
|
-
"wechat-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"bridge:
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"node": "
|
|
99
|
-
|
|
100
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "cli-wechat-bridge",
|
|
3
|
+
"version": "1.1.3",
|
|
4
|
+
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, Pi, and PowerShell over WeChat",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin/",
|
|
8
|
+
"dist/",
|
|
9
|
+
"scripts/ensure-node-pty-permissions.mjs",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE.txt"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"wechat",
|
|
15
|
+
"cli",
|
|
16
|
+
"bridge",
|
|
17
|
+
"codex",
|
|
18
|
+
"claude",
|
|
19
|
+
"opencode",
|
|
20
|
+
"pi",
|
|
21
|
+
"terminal"
|
|
22
|
+
],
|
|
23
|
+
"author": "UNLINEARITY",
|
|
24
|
+
"license": "AGPL-3.0-or-later",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/UNLINEARITY/CLI-WeChat-Bridge.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/UNLINEARITY/CLI-WeChat-Bridge/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/UNLINEARITY/CLI-WeChat-Bridge#readme",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"bin": {
|
|
37
|
+
"wechat-bridge": "bin/wechat-bridge.mjs",
|
|
38
|
+
"wechat-bridge-codex": "bin/wechat-bridge-codex.mjs",
|
|
39
|
+
"wechat-codex": "bin/wechat-codex.mjs",
|
|
40
|
+
"wechat-codex-start": "bin/wechat-codex-start.mjs",
|
|
41
|
+
"wechat-claude": "bin/wechat-claude.mjs",
|
|
42
|
+
"wechat-claude-start": "bin/wechat-claude-start.mjs",
|
|
43
|
+
"wechat-bridge-claude": "bin/wechat-bridge-claude.mjs",
|
|
44
|
+
"wechat-bridge-shell": "bin/wechat-bridge-shell.mjs",
|
|
45
|
+
"wechat-bridge-opencode": "bin/wechat-bridge-opencode.mjs",
|
|
46
|
+
"wechat-opencode": "bin/wechat-opencode.mjs",
|
|
47
|
+
"wechat-opencode-start": "bin/wechat-opencode-start.mjs",
|
|
48
|
+
"wechat-bridge-pi": "bin/wechat-bridge-pi.mjs",
|
|
49
|
+
"wechat-pi": "bin/wechat-pi.mjs",
|
|
50
|
+
"wechat-pi-start": "bin/wechat-pi-start.mjs",
|
|
51
|
+
"wechat-daemon": "bin/wechat-daemon.mjs",
|
|
52
|
+
"wechat-setup": "bin/wechat-setup.mjs",
|
|
53
|
+
"wechat-check-update": "bin/wechat-check-update.mjs"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"postinstall": "node scripts/ensure-node-pty-permissions.mjs",
|
|
57
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
58
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
59
|
+
"prepack": "npm run build",
|
|
60
|
+
"lint": "eslint bin src test",
|
|
61
|
+
"lint:fix": "eslint bin src test --fix",
|
|
62
|
+
"typecheck:src": "tsc -p tsconfig.typecheck.src.json --noEmit --pretty false",
|
|
63
|
+
"quality": "npm run lint && npm run typecheck:src && bun test test && npm run build",
|
|
64
|
+
"smoke:global": "node scripts/smoke-global-install.mjs",
|
|
65
|
+
"publish:dual": "node scripts/publish-dual.mjs",
|
|
66
|
+
"setup": "node --no-warnings --experimental-strip-types src/wechat/setup.ts",
|
|
67
|
+
"start": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts",
|
|
68
|
+
"check": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts --check",
|
|
69
|
+
"bridge": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts",
|
|
70
|
+
"daemon": "node --no-warnings --experimental-strip-types src/daemon/wechat-daemon.ts",
|
|
71
|
+
"bridge:codex": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter codex",
|
|
72
|
+
"codex:panel": "node --no-warnings --experimental-strip-types src/companion/codex-remote-client.ts",
|
|
73
|
+
"codex:start": "node --no-warnings --experimental-strip-types src/companion/local-companion-start.ts",
|
|
74
|
+
"claude:start": "node --no-warnings --experimental-strip-types src/companion/local-companion-start.ts --adapter claude",
|
|
75
|
+
"claude:companion": "node --no-warnings --experimental-strip-types src/companion/local-companion.ts --adapter claude",
|
|
76
|
+
"bridge:claude": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter claude",
|
|
77
|
+
"bridge:shell": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter shell",
|
|
78
|
+
"bridge:opencode": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter opencode",
|
|
79
|
+
"opencode:panel": "node --no-warnings --experimental-strip-types src/companion/local-companion.ts --adapter opencode",
|
|
80
|
+
"opencode:start": "node --no-warnings --experimental-strip-types src/companion/local-companion-start.ts --adapter opencode",
|
|
81
|
+
"bridge:pi": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter pi",
|
|
82
|
+
"pi:companion": "node --no-warnings --experimental-strip-types src/companion/local-companion.ts --adapter pi",
|
|
83
|
+
"pi:start": "node --no-warnings --experimental-strip-types src/companion/local-companion-start.ts --adapter pi",
|
|
84
|
+
"bridge:bun": "bun src/bridge/wechat-bridge.ts",
|
|
85
|
+
"test": "bun test test",
|
|
86
|
+
"test:bridge": "bun test test/bridge",
|
|
87
|
+
"test:companion": "bun test test/companion",
|
|
88
|
+
"test:wechat": "bun test test/wechat",
|
|
89
|
+
"test:watch": "bun test --watch test"
|
|
90
|
+
},
|
|
91
|
+
"dependencies": {
|
|
92
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
93
|
+
"@opencode-ai/sdk": "^1.18.0",
|
|
94
|
+
"node-pty": "^1.1.0",
|
|
95
|
+
"qrcode-terminal": "^0.12.0"
|
|
96
|
+
},
|
|
97
|
+
"overrides": {
|
|
98
|
+
"@hono/node-server": "2.1.1",
|
|
99
|
+
"body-parser": "2.3.0",
|
|
100
|
+
"brace-expansion": "5.0.9",
|
|
101
|
+
"express-rate-limit": "8.6.2",
|
|
102
|
+
"fast-uri": "3.1.5",
|
|
103
|
+
"hono": "4.13.3",
|
|
104
|
+
"ip-address": "10.5.0",
|
|
105
|
+
"path-to-regexp": "8.4.2",
|
|
106
|
+
"qs": "6.15.3"
|
|
107
|
+
},
|
|
108
|
+
"devDependencies": {
|
|
109
|
+
"@eslint/js": "^10.0.1",
|
|
110
|
+
"@types/bun": "latest",
|
|
111
|
+
"@types/node": "^24.12.4",
|
|
112
|
+
"eslint": "^10.3.0",
|
|
113
|
+
"typescript": "^6.0.2",
|
|
114
|
+
"typescript-eslint": "^8.59.3"
|
|
115
|
+
},
|
|
116
|
+
"engines": {
|
|
117
|
+
"bun": ">=1.0.0",
|
|
118
|
+
"node": ">=24.0.0"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +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
|
+
}
|