@unlinearity/cli-wechat-bridge 1.1.3 → 1.1.4
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 +98 -135
- package/bin/_run-entry.mjs +12 -0
- package/bin/wechat-claude-start.mjs +7 -2
- package/bin/wechat-claude.mjs +1 -1
- package/bin/wechat-codex-start.mjs +7 -2
- package/bin/wechat-codex.mjs +1 -1
- package/bin/wechat-opencode-start.mjs +7 -2
- package/bin/wechat-opencode.mjs +1 -1
- package/bin/wechat-pi-start.mjs +7 -2
- package/bin/wechat-pi.mjs +1 -1
- package/dist/bridge/bridge-adapters.claude.js +21 -1
- package/dist/bridge/bridge-adapters.codex.js +27 -4
- package/dist/bridge/bridge-adapters.core.js +76 -4
- package/dist/bridge/bridge-adapters.js +0 -3
- package/dist/bridge/bridge-adapters.opencode.js +130 -21
- package/dist/bridge/bridge-adapters.pi.js +12 -2
- package/dist/bridge/bridge-adapters.shared.js +13 -100
- package/dist/bridge/bridge-final-reply.js +17 -2
- package/dist/bridge/bridge-process-reaper.js +14 -4
- package/dist/bridge/bridge-state.js +41 -6
- package/dist/bridge/bridge-utils.js +37 -224
- package/dist/bridge/wechat-bridge.js +75 -130
- package/dist/bridge/wechat-forwarding.js +58 -0
- package/dist/companion/codex-remote-client.js +20 -6
- package/dist/companion/local-companion-link.js +7 -2
- package/dist/companion/local-companion-start.js +45 -13
- package/dist/companion/local-companion.js +6 -3
- package/dist/daemon/daemon-link.js +25 -3
- package/dist/daemon/wechat-daemon.js +148 -54
- package/dist/i18n/index.js +0 -3
- package/dist/i18n/messages-en.js +2 -3
- package/dist/i18n/messages-zh.js +2 -3
- package/dist/utils/atomic-file.js +15 -0
- package/dist/utils/doctor.js +23 -11
- package/dist/wechat/channel-config.js +0 -2
- package/dist/wechat/setup.js +5 -11
- package/dist/wechat/wechat-transport.js +14 -8
- package/package.json +5 -8
- package/scripts/ensure-node-pty-permissions.mjs +73 -0
- package/bin/wechat-bridge-claude.mjs +0 -5
- package/bin/wechat-bridge-codex.mjs +0 -5
- package/bin/wechat-bridge-opencode.mjs +0 -5
- package/bin/wechat-bridge-pi.mjs +0 -5
- package/bin/wechat-bridge-shell.mjs +0 -5
- package/bin/wechat-bridge.mjs +0 -5
- package/dist/bridge/bridge-adapters.shell.js +0 -363
package/dist/utils/doctor.js
CHANGED
|
@@ -7,7 +7,7 @@ import { normalizeBridgeLockPayload, shouldAutoReclaimBridgeLock, } from "../bri
|
|
|
7
7
|
import { readLocalCompanionEndpoint as readWorkspaceEndpoint, } from "../companion/local-companion-link.js";
|
|
8
8
|
import { isDaemonEndpointAlive, } from "../daemon/daemon-link.js";
|
|
9
9
|
import { t } from "../i18n/index.js";
|
|
10
|
-
import { DEFAULT_BASE_URL, resolveChannelDataDir } from "../wechat/channel-config.js";
|
|
10
|
+
import { DEFAULT_BASE_URL, getWorkspaceAdapterEndpointFile, getWorkspaceChannelPaths, resolveChannelDataDir, } from "../wechat/channel-config.js";
|
|
11
11
|
const STATE_OK = "[ok]";
|
|
12
12
|
const STATE_WARN = "[warn]";
|
|
13
13
|
const STATE_FAIL = "[fail]";
|
|
@@ -174,8 +174,7 @@ function isBridgeAdapterKind(value) {
|
|
|
174
174
|
return (value === "codex" ||
|
|
175
175
|
value === "claude" ||
|
|
176
176
|
value === "opencode" ||
|
|
177
|
-
value === "pi"
|
|
178
|
-
value === "shell");
|
|
177
|
+
value === "pi");
|
|
179
178
|
}
|
|
180
179
|
export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process.cwd()) {
|
|
181
180
|
let cwd = fallbackCwd;
|
|
@@ -417,8 +416,6 @@ function chooseCliChecks(options) {
|
|
|
417
416
|
return [{ name: "opencode", label: "OpenCode", optional: false }];
|
|
418
417
|
case "pi":
|
|
419
418
|
return [{ name: "pi", label: "Pi", optional: false }];
|
|
420
|
-
case "shell":
|
|
421
|
-
return [];
|
|
422
419
|
default:
|
|
423
420
|
return [];
|
|
424
421
|
}
|
|
@@ -432,11 +429,6 @@ function chooseCliChecks(options) {
|
|
|
432
429
|
}
|
|
433
430
|
function appendCliLines(lines, options, deps) {
|
|
434
431
|
const clis = chooseCliChecks(options);
|
|
435
|
-
if (options.mode === "bridge" && options.adapter === "shell") {
|
|
436
|
-
section(lines, msg("section.adapterCli"));
|
|
437
|
-
lines.push(row(STATE_OK, msg("label.shell"), msg("adapterCli.shellNative")));
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
432
|
if (clis.length === 0) {
|
|
441
433
|
return;
|
|
442
434
|
}
|
|
@@ -519,6 +511,18 @@ async function appendEndpointSummary(lines, label, endpoint, options, lock, deps
|
|
|
519
511
|
lines.push(detail(msg("issue", { issue })));
|
|
520
512
|
}
|
|
521
513
|
}
|
|
514
|
+
function endpointFileState(filePath) {
|
|
515
|
+
if (!fs.existsSync(filePath)) {
|
|
516
|
+
return "missing";
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
520
|
+
return "readable";
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return "corrupted";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
522
526
|
async function appendWorkspaceEndpointLines(lines, options, lock, deps) {
|
|
523
527
|
const adapters = chooseEndpointAdapters(options, lock);
|
|
524
528
|
const seenEndpoints = new Set();
|
|
@@ -527,12 +531,20 @@ async function appendWorkspaceEndpointLines(lines, options, lock, deps) {
|
|
|
527
531
|
if (endpoint) {
|
|
528
532
|
seenEndpoints.add(`${endpoint.kind}:${endpoint.instanceId}:${endpoint.port}`);
|
|
529
533
|
}
|
|
534
|
+
else if (endpointFileState(getWorkspaceAdapterEndpointFile(options.cwd, adapter)) ===
|
|
535
|
+
"corrupted") {
|
|
536
|
+
// A torn or otherwise unparsable endpoint file must not read as a
|
|
537
|
+
// healthy "none" — surface it so the operator knows to clear it.
|
|
538
|
+
lines.push(row(STATE_WARN, msg("label.workspaceEndpoint", { adapter }), msg("endpoint.corrupted")));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
530
541
|
await appendEndpointSummary(lines, msg("label.workspaceEndpoint", { adapter }), endpoint, { ...options, adapter }, lock, deps);
|
|
531
542
|
}
|
|
532
543
|
const legacyEndpoint = deps.readLocalCompanionEndpoint(options.cwd);
|
|
533
544
|
const legacyLabel = msg("label.legacyEndpoint");
|
|
534
545
|
if (!legacyEndpoint) {
|
|
535
|
-
|
|
546
|
+
const legacyFileState = endpointFileState(getWorkspaceChannelPaths(options.cwd).endpointFile);
|
|
547
|
+
lines.push(row(legacyFileState === "corrupted" ? STATE_WARN : STATE_OK, legacyLabel, legacyFileState === "corrupted" ? msg("endpoint.corrupted") : msg("none")));
|
|
536
548
|
return;
|
|
537
549
|
}
|
|
538
550
|
const legacyKey = `${legacyEndpoint.kind}:${legacyEndpoint.instanceId}:${legacyEndpoint.port}`;
|
|
@@ -16,7 +16,6 @@ export const CHANNEL_DATA_DIR = resolveChannelDataDir();
|
|
|
16
16
|
export const CREDENTIALS_FILE = path.join(CHANNEL_DATA_DIR, "account.json");
|
|
17
17
|
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
|
-
export const BRIDGE_STATE_FILE = path.join(CHANNEL_DATA_DIR, "bridge-state.json");
|
|
20
19
|
export const BRIDGE_LOG_FILE = path.join(CHANNEL_DATA_DIR, "bridge.log");
|
|
21
20
|
// Hard cap for bridge.log. Long-running daemons otherwise grow it without bound.
|
|
22
21
|
export const BRIDGE_LOG_MAX_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
@@ -48,7 +47,6 @@ export function appendBoundedLog(filePath, line) {
|
|
|
48
47
|
}
|
|
49
48
|
export const BRIDGE_LOCK_FILE = path.join(CHANNEL_DATA_DIR, "bridge.lock.json");
|
|
50
49
|
export const DAEMON_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "daemon-endpoint.json");
|
|
51
|
-
export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-panel-endpoint.json");
|
|
52
50
|
export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
|
|
53
51
|
export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-message-claims");
|
|
54
52
|
export const INBOUND_ATTACHMENTS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-attachments");
|
package/dist/wechat/setup.js
CHANGED
|
@@ -166,17 +166,11 @@ function printPostLoginHelp(log) {
|
|
|
166
166
|
log("This WeChat account is now authorized for the bridge.");
|
|
167
167
|
log("");
|
|
168
168
|
log("Start from any project directory with one of:");
|
|
169
|
-
log(" wechat-codex
|
|
170
|
-
log(" wechat-claude
|
|
171
|
-
log(" wechat-opencode
|
|
172
|
-
log(" wechat-pi
|
|
173
|
-
log(" wechat-
|
|
174
|
-
log("");
|
|
175
|
-
log("Manual two-terminal mode is also available:");
|
|
176
|
-
log(" wechat-bridge-codex + wechat-codex");
|
|
177
|
-
log(" wechat-bridge-claude + wechat-claude");
|
|
178
|
-
log(" wechat-bridge-opencode + wechat-opencode");
|
|
179
|
-
log(" wechat-bridge-pi + wechat-pi");
|
|
169
|
+
log(" wechat-codex");
|
|
170
|
+
log(" wechat-claude");
|
|
171
|
+
log(" wechat-opencode");
|
|
172
|
+
log(" wechat-pi");
|
|
173
|
+
log(" wechat-daemon");
|
|
180
174
|
log("");
|
|
181
175
|
log("Run wechat-setup again any time you need to refresh the login.");
|
|
182
176
|
}
|
|
@@ -3,6 +3,7 @@ import { createCipheriv, createDecipheriv } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { CONTEXT_CACHE_FILE, CREDENTIALS_FILE, ensureChannelDataDir, INBOUND_ATTACHMENTS_DIR, INBOUND_MESSAGE_CLAIMS_DIR, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
|
|
6
|
+
import { writeJsonFileAtomic } from "../utils/atomic-file.js";
|
|
6
7
|
export const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
7
8
|
const CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
8
9
|
const CHANNEL_VERSION = "0.3.0";
|
|
@@ -10,6 +11,7 @@ const RECENT_MESSAGE_CACHE_SIZE = 500;
|
|
|
10
11
|
const BYTES_PER_MB = 1024 * 1024;
|
|
11
12
|
const SEND_TIMEOUT_MS = 15_000;
|
|
12
13
|
const INBOUND_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
14
|
+
const CDN_UPLOAD_TIMEOUT_MS = 60_000;
|
|
13
15
|
const CDN_MAX_RETRIES = 3;
|
|
14
16
|
const ERROR_CAUSE_DEPTH_LIMIT = 4;
|
|
15
17
|
const INBOUND_MESSAGE_CLAIM_TTL_MS = 10 * 60 * 1000;
|
|
@@ -305,13 +307,7 @@ export function classifyWechatTransportError(error) {
|
|
|
305
307
|
return { kind: "unknown", retryable: false };
|
|
306
308
|
}
|
|
307
309
|
function writeJsonFile(filePath, value) {
|
|
308
|
-
|
|
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
|
+
writeJsonFileAtomic(filePath, value);
|
|
315
311
|
}
|
|
316
312
|
function randomWechatUin() {
|
|
317
313
|
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
@@ -343,8 +339,11 @@ async function apiFetch(params) {
|
|
|
343
339
|
body: params.body,
|
|
344
340
|
signal: controller.signal,
|
|
345
341
|
});
|
|
346
|
-
|
|
342
|
+
// Keep the abort timer armed until the body has been fully read: a server
|
|
343
|
+
// that sends headers and then stalls would otherwise leave res.text()
|
|
344
|
+
// pending forever, freezing the long-poll loop with no timeout at all.
|
|
347
345
|
const text = await res.text();
|
|
346
|
+
clearTimeout(timer);
|
|
348
347
|
if (!res.ok) {
|
|
349
348
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
350
349
|
}
|
|
@@ -495,12 +494,18 @@ async function uploadBufferToCdn(params) {
|
|
|
495
494
|
let downloadParam;
|
|
496
495
|
let lastError;
|
|
497
496
|
for (let attempt = 1; attempt <= CDN_MAX_RETRIES; attempt += 1) {
|
|
497
|
+
// Abort guard mirrors the download path: without it a stalled CDN upload
|
|
498
|
+
// hangs the send queue forever (no timeout, no signal).
|
|
499
|
+
const controller = new AbortController();
|
|
500
|
+
const timer = setTimeout(() => controller.abort(), CDN_UPLOAD_TIMEOUT_MS);
|
|
498
501
|
try {
|
|
499
502
|
const res = await fetch(cdnUrl, {
|
|
500
503
|
method: "POST",
|
|
501
504
|
headers: { "Content-Type": "application/octet-stream" },
|
|
502
505
|
body: new Uint8Array(ciphertext),
|
|
506
|
+
signal: controller.signal,
|
|
503
507
|
});
|
|
508
|
+
clearTimeout(timer);
|
|
504
509
|
if (res.status >= 400 && res.status < 500) {
|
|
505
510
|
const errMsg = res.headers.get("x-error-message") ?? (await res.text());
|
|
506
511
|
throw new Error(`CDN client error ${res.status}: ${errMsg}`);
|
|
@@ -516,6 +521,7 @@ async function uploadBufferToCdn(params) {
|
|
|
516
521
|
break;
|
|
517
522
|
}
|
|
518
523
|
catch (err) {
|
|
524
|
+
clearTimeout(timer);
|
|
519
525
|
lastError = err;
|
|
520
526
|
if (err instanceof Error && err.message.includes("client error")) {
|
|
521
527
|
throw err;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlinearity/cli-wechat-bridge",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode,
|
|
3
|
+
"version": "1.1.4",
|
|
4
|
+
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and Pi over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin/",
|
|
@@ -34,18 +34,12 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"bin": {
|
|
37
|
-
"wechat-bridge": "bin/wechat-bridge.mjs",
|
|
38
|
-
"wechat-bridge-codex": "bin/wechat-bridge-codex.mjs",
|
|
39
37
|
"wechat-codex": "bin/wechat-codex.mjs",
|
|
40
38
|
"wechat-codex-start": "bin/wechat-codex-start.mjs",
|
|
41
39
|
"wechat-claude": "bin/wechat-claude.mjs",
|
|
42
40
|
"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
41
|
"wechat-opencode": "bin/wechat-opencode.mjs",
|
|
47
42
|
"wechat-opencode-start": "bin/wechat-opencode-start.mjs",
|
|
48
|
-
"wechat-bridge-pi": "bin/wechat-bridge-pi.mjs",
|
|
49
43
|
"wechat-pi": "bin/wechat-pi.mjs",
|
|
50
44
|
"wechat-pi-start": "bin/wechat-pi-start.mjs",
|
|
51
45
|
"wechat-daemon": "bin/wechat-daemon.mjs",
|
|
@@ -72,5 +66,8 @@
|
|
|
72
66
|
"engines": {
|
|
73
67
|
"bun": ">=1.0.0",
|
|
74
68
|
"node": ">=24.0.0"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"postinstall": "node scripts/ensure-node-pty-permissions.mjs"
|
|
75
72
|
}
|
|
76
73
|
}
|
|
@@ -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
|
+
}
|
package/bin/wechat-bridge-pi.mjs
DELETED