@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.
Files changed (46) hide show
  1. package/README.md +98 -135
  2. package/bin/_run-entry.mjs +12 -0
  3. package/bin/wechat-claude-start.mjs +7 -2
  4. package/bin/wechat-claude.mjs +1 -1
  5. package/bin/wechat-codex-start.mjs +7 -2
  6. package/bin/wechat-codex.mjs +1 -1
  7. package/bin/wechat-opencode-start.mjs +7 -2
  8. package/bin/wechat-opencode.mjs +1 -1
  9. package/bin/wechat-pi-start.mjs +7 -2
  10. package/bin/wechat-pi.mjs +1 -1
  11. package/dist/bridge/bridge-adapters.claude.js +21 -1
  12. package/dist/bridge/bridge-adapters.codex.js +27 -4
  13. package/dist/bridge/bridge-adapters.core.js +76 -4
  14. package/dist/bridge/bridge-adapters.js +0 -3
  15. package/dist/bridge/bridge-adapters.opencode.js +130 -21
  16. package/dist/bridge/bridge-adapters.pi.js +12 -2
  17. package/dist/bridge/bridge-adapters.shared.js +13 -100
  18. package/dist/bridge/bridge-final-reply.js +17 -2
  19. package/dist/bridge/bridge-process-reaper.js +14 -4
  20. package/dist/bridge/bridge-state.js +41 -6
  21. package/dist/bridge/bridge-utils.js +37 -224
  22. package/dist/bridge/wechat-bridge.js +75 -130
  23. package/dist/bridge/wechat-forwarding.js +58 -0
  24. package/dist/companion/codex-remote-client.js +20 -6
  25. package/dist/companion/local-companion-link.js +7 -2
  26. package/dist/companion/local-companion-start.js +45 -13
  27. package/dist/companion/local-companion.js +6 -3
  28. package/dist/daemon/daemon-link.js +25 -3
  29. package/dist/daemon/wechat-daemon.js +148 -54
  30. package/dist/i18n/index.js +0 -3
  31. package/dist/i18n/messages-en.js +2 -3
  32. package/dist/i18n/messages-zh.js +2 -3
  33. package/dist/utils/atomic-file.js +15 -0
  34. package/dist/utils/doctor.js +23 -11
  35. package/dist/wechat/channel-config.js +0 -2
  36. package/dist/wechat/setup.js +5 -11
  37. package/dist/wechat/wechat-transport.js +14 -8
  38. package/package.json +5 -8
  39. package/scripts/ensure-node-pty-permissions.mjs +73 -0
  40. package/bin/wechat-bridge-claude.mjs +0 -5
  41. package/bin/wechat-bridge-codex.mjs +0 -5
  42. package/bin/wechat-bridge-opencode.mjs +0 -5
  43. package/bin/wechat-bridge-pi.mjs +0 -5
  44. package/bin/wechat-bridge-shell.mjs +0 -5
  45. package/bin/wechat-bridge.mjs +0 -5
  46. package/dist/bridge/bridge-adapters.shell.js +0 -363
@@ -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
- lines.push(row(STATE_OK, legacyLabel, msg("none")));
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");
@@ -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-start");
170
- log(" wechat-claude-start");
171
- log(" wechat-opencode-start");
172
- log(" wechat-pi-start");
173
- log(" wechat-bridge-shell");
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
- ensureChannelDataDir();
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
- clearTimeout(timer);
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.3",
4
- "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, Pi, and PowerShell over WeChat",
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
+ }
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runJsEntry } from "./_run-entry.mjs";
4
-
5
- runJsEntry("dist/bridge/wechat-bridge.js", ["--adapter", "claude"]);
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runJsEntry } from "./_run-entry.mjs";
4
-
5
- runJsEntry("dist/bridge/wechat-bridge.js", ["--adapter", "codex"]);
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runJsEntry } from "./_run-entry.mjs";
4
-
5
- runJsEntry("dist/bridge/wechat-bridge.js", ["--adapter", "opencode"]);
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runJsEntry } from "./_run-entry.mjs";
4
-
5
- runJsEntry("dist/bridge/wechat-bridge.js", ["--adapter", "pi"]);
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runJsEntry } from "./_run-entry.mjs";
4
-
5
- runJsEntry("dist/bridge/wechat-bridge.js", ["--adapter", "shell"]);
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runJsEntry } from "./_run-entry.mjs";
4
-
5
- runJsEntry("dist/bridge/wechat-bridge.js");