@unlinearity/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.
Files changed (34) hide show
  1. package/README.md +60 -43
  2. package/bin/_run-entry.mjs +24 -0
  3. package/bin/wechat-bridge-pi.mjs +5 -0
  4. package/bin/wechat-pi-start.mjs +5 -0
  5. package/bin/wechat-pi.mjs +5 -0
  6. package/dist/bridge/bridge-adapter-common.js +2 -0
  7. package/dist/bridge/bridge-adapters.claude.js +18 -0
  8. package/dist/bridge/bridge-adapters.codex.js +83 -2
  9. package/dist/bridge/bridge-adapters.core.js +36 -2
  10. package/dist/bridge/bridge-adapters.js +5 -0
  11. package/dist/bridge/bridge-adapters.opencode.js +278 -32
  12. package/dist/bridge/bridge-adapters.pi.js +697 -0
  13. package/dist/bridge/bridge-adapters.shared.js +44 -46
  14. package/dist/bridge/bridge-final-reply.js +8 -4
  15. package/dist/bridge/bridge-process-reaper.js +58 -3
  16. package/dist/bridge/bridge-state.js +15 -6
  17. package/dist/bridge/bridge-utils.js +129 -30
  18. package/dist/bridge/claude-hook.js +7 -0
  19. package/dist/bridge/claude-hooks.js +10 -2
  20. package/dist/bridge/wechat-bridge.js +20 -8
  21. package/dist/commands/check-update.js +15 -4
  22. package/dist/companion/local-companion-link.js +6 -1
  23. package/dist/companion/local-companion-start.js +29 -8
  24. package/dist/companion/local-companion.js +3 -3
  25. package/dist/companion/pi-tui-bridge-extension.js +256 -0
  26. package/dist/daemon/wechat-daemon.js +126 -25
  27. package/dist/i18n/messages-en.js +12 -2
  28. package/dist/i18n/messages-zh.js +12 -2
  29. package/dist/utils/doctor.js +85 -2
  30. package/dist/utils/version-checker.js +77 -44
  31. package/dist/wechat/channel-config.js +28 -0
  32. package/dist/wechat/setup.js +2 -0
  33. package/dist/wechat/wechat-transport.js +49 -13
  34. package/package.json +20 -4
@@ -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");
@@ -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
- fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
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
- return JSON.parse(raw);
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.rmSync(claimsDir, { recursive: true, force: true });
701
+ entries = fs.readdirSync(claimsDir);
683
702
  }
684
703
  catch {
685
- // Best effort cleanup.
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
- return true;
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
- (!Number.isFinite(createdAtMs) || createdAtMs < options.minCreatedAtMs)) {
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,11 +1,12 @@
1
1
  {
2
2
  "name": "@unlinearity/cli-wechat-bridge",
3
- "version": "1.1.1",
4
- "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
3
+ "version": "1.1.3",
4
+ "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, Pi, and PowerShell over WeChat",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "bin/",
8
8
  "dist/",
9
+ "scripts/ensure-node-pty-permissions.mjs",
9
10
  "README.md",
10
11
  "LICENSE.txt"
11
12
  ],
@@ -16,6 +17,7 @@
16
17
  "codex",
17
18
  "claude",
18
19
  "opencode",
20
+ "pi",
19
21
  "terminal"
20
22
  ],
21
23
  "author": "UNLINEARITY",
@@ -43,16 +45,30 @@
43
45
  "wechat-bridge-opencode": "bin/wechat-bridge-opencode.mjs",
44
46
  "wechat-opencode": "bin/wechat-opencode.mjs",
45
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",
46
51
  "wechat-daemon": "bin/wechat-daemon.mjs",
47
52
  "wechat-setup": "bin/wechat-setup.mjs",
48
53
  "wechat-check-update": "bin/wechat-check-update.mjs"
49
54
  },
50
55
  "dependencies": {
51
- "@modelcontextprotocol/sdk": "^1.27.1",
52
- "@opencode-ai/sdk": "^1.3.2",
56
+ "@modelcontextprotocol/sdk": "^1.30.0",
57
+ "@opencode-ai/sdk": "^1.18.0",
53
58
  "node-pty": "^1.1.0",
54
59
  "qrcode-terminal": "^0.12.0"
55
60
  },
61
+ "overrides": {
62
+ "@hono/node-server": "2.1.1",
63
+ "body-parser": "2.3.0",
64
+ "brace-expansion": "5.0.9",
65
+ "express-rate-limit": "8.6.2",
66
+ "fast-uri": "3.1.5",
67
+ "hono": "4.13.3",
68
+ "ip-address": "10.5.0",
69
+ "path-to-regexp": "8.4.2",
70
+ "qs": "6.15.3"
71
+ },
56
72
  "engines": {
57
73
  "bun": ">=1.0.0",
58
74
  "node": ">=24.0.0"