cli-wechat-bridge 1.1.5 → 1.1.7

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 (52) hide show
  1. package/README.md +212 -232
  2. package/bin/_run-entry.mjs +20 -5
  3. package/bin/wecom-check-update.mjs +5 -0
  4. package/bin/wecom-claude.mjs +5 -0
  5. package/bin/wecom-codex.mjs +5 -0
  6. package/bin/wecom-daemon.mjs +5 -0
  7. package/bin/wecom-opencode.mjs +5 -0
  8. package/bin/wecom-pi.mjs +5 -0
  9. package/bin/wecom-setup.mjs +5 -0
  10. package/dist/bridge/bridge-adapters.claude.js +128 -4
  11. package/dist/bridge/bridge-adapters.codex.js +214 -23
  12. package/dist/bridge/bridge-adapters.core.js +2 -1
  13. package/dist/bridge/bridge-adapters.pi.js +1 -1
  14. package/dist/bridge/bridge-adapters.shared.js +2 -1
  15. package/dist/bridge/bridge-final-reply.js +2 -65
  16. package/dist/bridge/bridge-process-reaper.js +35 -2
  17. package/dist/bridge/bridge-state.js +11 -4
  18. package/dist/bridge/bridge-utils.js +4 -96
  19. package/dist/bridge/wechat-bridge.js +476 -206
  20. package/dist/bridge/wechat-forwarding.js +2 -58
  21. package/dist/bridge/wechat-outbound-queue.js +2 -0
  22. package/dist/channels/wechat/bridge-final-reply.js +65 -0
  23. package/dist/channels/wechat/channel-message.js +24 -0
  24. package/dist/channels/wechat/index.js +5 -0
  25. package/dist/channels/wechat/wechat-channel-port.js +67 -0
  26. package/dist/channels/wechat/wechat-forwarding.js +79 -0
  27. package/dist/channels/wechat/wechat-outbound-queue.js +79 -0
  28. package/dist/channels/wecom/bridge-final-reply.js +26 -0
  29. package/dist/channels/wecom/setup-cli.js +6 -0
  30. package/dist/channels/wecom/setup.js +176 -0
  31. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  32. package/dist/channels/wecom/wecom-config.js +66 -0
  33. package/dist/channels/wecom/wecom-media.js +141 -0
  34. package/dist/channels/wecom/wecom-message.js +149 -0
  35. package/dist/channels/wecom/wecom-transport.js +308 -0
  36. package/dist/companion/codex-remote-client.js +2 -1
  37. package/dist/companion/local-companion-start.js +49 -9
  38. package/dist/companion/local-companion.js +2 -1
  39. package/dist/core/bridge-control.js +32 -0
  40. package/dist/core/bridge-event-forwarder.js +50 -0
  41. package/dist/core/bridge-message-router.js +32 -0
  42. package/dist/core/channel-types.js +3 -0
  43. package/dist/core/direct-run.js +17 -0
  44. package/dist/core/inbound-message-claims.js +61 -0
  45. package/dist/core/text-utils.js +63 -0
  46. package/dist/daemon/daemon-link.js +3 -0
  47. package/dist/daemon/wechat-daemon.js +466 -172
  48. package/dist/i18n/messages-en.js +4 -2
  49. package/dist/i18n/messages-zh.js +4 -2
  50. package/dist/utils/doctor.js +30 -10
  51. package/dist/wechat/setup.js +113 -13
  52. package/package.json +117 -107
@@ -6,22 +6,37 @@ import { fileURLToPath } from "node:url";
6
6
 
7
7
  const BIN_DIR = path.dirname(fileURLToPath(import.meta.url));
8
8
  const PROJECT_DIR = path.resolve(BIN_DIR, "..");
9
- const MIN_NODE_MAJOR = 24;
9
+ export const MIN_NODE_VERSION = Object.freeze([22, 13, 0]);
10
+ export const MIN_NODE_VERSION_TEXT = MIN_NODE_VERSION.join(".");
11
+
12
+ export function isSupportedNodeVersion(version) {
13
+ const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version);
14
+ if (!match) {
15
+ return false;
16
+ }
17
+
18
+ const current = [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)];
19
+ for (let index = 0; index < MIN_NODE_VERSION.length; index += 1) {
20
+ if (current[index] !== MIN_NODE_VERSION[index]) {
21
+ return current[index] > MIN_NODE_VERSION[index];
22
+ }
23
+ }
24
+ return true;
25
+ }
10
26
 
11
27
  function ensureSupportedNodeVersion() {
12
28
  if (process.env.CLI_BRIDGE_SKIP_NODE_CHECK === "1") {
13
29
  return;
14
30
  }
15
31
 
16
- const major = Number(process.versions.node.split(".")[0]);
17
- if (Number.isFinite(major) && major >= MIN_NODE_MAJOR) {
32
+ if (isSupportedNodeVersion(process.versions.node)) {
18
33
  return;
19
34
  }
20
35
 
21
36
  process.stderr.write(
22
37
  [
23
- `[cli-wechat-bridge] Node.js >= ${MIN_NODE_MAJOR} is required, but you are running ${process.version}.`,
24
- `[cli-wechat-bridge] 需要 Node.js >= ${MIN_NODE_MAJOR},当前版本为 ${process.version}。`,
38
+ `[cli-wechat-bridge] Node.js >= ${MIN_NODE_VERSION_TEXT} is required, but you are running ${process.version}.`,
39
+ `[cli-wechat-bridge] 需要 Node.js >= ${MIN_NODE_VERSION_TEXT},当前版本为 ${process.version}。`,
25
40
  "Install the latest LTS from https://nodejs.org/ (or via nvm), then retry.",
26
41
  "Set CLI_BRIDGE_SKIP_NODE_CHECK=1 to bypass this check at your own risk.",
27
42
  "",
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/commands/check-update.js");
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "claude", "--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "codex", "--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/daemon/wechat-daemon.js", ["--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "opencode", "--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "pi", "--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/channels/wecom/setup-cli.js");
@@ -7,7 +7,8 @@ import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
7
7
  import { t } from "../i18n/index.js";
8
8
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
9
9
  import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, getClaudePermissionAutoResponse, getClaudeWechatOutboundAttachmentDenyMessage, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
10
- import { detectCliApproval, isThinkingForwardEnabled, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
10
+ import { detectCliApproval, isThinkingForwardEnabled, } from "./bridge-utils.js";
11
+ import { normalizeOutput, nowIso, truncatePreview } from "../core/text-utils.js";
11
12
  import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
12
13
  import * as shared from "./bridge-adapters.shared.js";
13
14
  const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, delay, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
@@ -17,6 +18,8 @@ const CLAUDE_COMPACT_DEDUP_MS = 2_000;
17
18
  const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
18
19
  const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
19
20
  const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
21
+ const CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS = [200, 500, 1_000];
22
+ const CLAUDE_INTERACTIVE_READY_TIMEOUT_MS = 10_000;
20
23
  const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
21
24
  const CLAUDE_SESSION_METADATA_READ_BYTES = 64 * 1024;
22
25
  const CLAUDE_MAX_SANITIZED_PROJECT_PATH = 200;
@@ -336,6 +339,12 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
336
339
  polledTranscriptSessionId = null;
337
340
  pendingResume = null;
338
341
  localResumeSessionEndObserved = false;
342
+ interactiveReady = true;
343
+ interactiveReadyTimer = null;
344
+ interactiveReadyWaiters = new Set();
345
+ remoteSubmitRetryTimer = null;
346
+ remoteSubmitRetryIndex = 0;
347
+ remoteSubmitText = null;
339
348
  constructor(options) {
340
349
  super(options);
341
350
  const shouldRestoreInitialSession = options.sessionStartMode !== "new";
@@ -363,6 +372,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
363
372
  if (this.pty) {
364
373
  return;
365
374
  }
375
+ this.beginInteractiveReadinessWait();
366
376
  this.startupOutputBuffer = "";
367
377
  this.hasAutoConfirmedWorkspaceTrustPrompt = false;
368
378
  ensureClaudeWorkspaceTrustAccepted(this.options.cwd);
@@ -384,11 +394,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
384
394
  this.state.activeRuntimeSessionId = undefined;
385
395
  }
386
396
  }
387
- await this.startHookServer();
388
397
  try {
398
+ await this.startHookServer();
389
399
  await super.start();
400
+ if (!this.interactiveReady) {
401
+ this.setStatus("starting", "Waiting for Claude interactive session...");
402
+ }
390
403
  }
391
404
  catch (error) {
405
+ this.rejectInteractiveReadiness(error instanceof Error ? error : new Error(String(error)));
392
406
  await this.stopHookServer();
393
407
  throw error;
394
408
  }
@@ -403,7 +417,18 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
403
417
  if (this.pendingApproval) {
404
418
  throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
405
419
  }
420
+ await this.waitForInteractiveReadiness();
421
+ if (!this.pty) {
422
+ throw new Error("claude adapter is not running.");
423
+ }
424
+ if (this.getState().status === "busy") {
425
+ throw new Error("claude is still working. Wait for the current reply or use /stop.");
426
+ }
427
+ if (this.pendingApproval) {
428
+ throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
429
+ }
406
430
  const normalizedText = normalizeOutput(text).trim();
431
+ this.clearRemoteSubmitRetry();
407
432
  this.pendingInjectedInputs.push({
408
433
  normalizedText,
409
434
  createdAtMs: Date.now(),
@@ -420,6 +445,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
420
445
  this.writeToPty(this.buildRemoteInputPayload(text));
421
446
  await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
422
447
  this.writeToPty("\r");
448
+ this.armRemoteSubmitRetry(normalizedText);
423
449
  this.armWechatWorkingNotice();
424
450
  }
425
451
  async listResumeSessions(limit = 10) {
@@ -529,6 +555,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
529
555
  return false;
530
556
  }
531
557
  this.clearWechatWorkingNotice(true);
558
+ this.clearRemoteSubmitRetry();
532
559
  this.pendingCliApprovalHints = null;
533
560
  this.flushPendingClaudeHookApprovals();
534
561
  if (this.pendingApproval) {
@@ -565,7 +592,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
565
592
  this.state.lastSessionSwitchAt = undefined;
566
593
  this.state.lastSessionSwitchSource = undefined;
567
594
  this.state.lastSessionSwitchReason = undefined;
568
- await super.reset();
595
+ await this.dispose();
596
+ await this.start();
569
597
  }
570
598
  async resolveApproval(action) {
571
599
  if (!this.pendingApproval) {
@@ -622,6 +650,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
622
650
  this.rejectPendingResume(new Error("Claude is shutting down during session switching."));
623
651
  this.detachLocalTerminal();
624
652
  this.clearWechatWorkingNotice(true);
653
+ this.clearRemoteSubmitRetry();
654
+ this.rejectInteractiveReadiness(new Error("Claude is shutting down."));
625
655
  this.pendingCliApprovalHints = null;
626
656
  this.flushPendingClaudeHookApprovals();
627
657
  this.stopTranscriptThinkingWatch();
@@ -719,8 +749,12 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
719
749
  handleExit(exitCode) {
720
750
  this.detachLocalTerminal();
721
751
  this.clearWechatWorkingNotice(true);
752
+ this.clearRemoteSubmitRetry();
722
753
  this.pendingCliApprovalHints = null;
723
754
  this.rejectPendingResume(new Error("Claude exited before the requested session switch completed."));
755
+ if (!this.recoveringInvalidResume) {
756
+ this.rejectInteractiveReadiness(new Error("Claude exited before its interactive session became ready."));
757
+ }
724
758
  void this.stopHookServer();
725
759
  if (this.recoveringInvalidResume && !this.shuttingDown) {
726
760
  this.clearCompletionTimer();
@@ -995,6 +1029,91 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
995
1029
  }
996
1030
  return `${CLAUDE_BRACKETED_PASTE_START}${normalizedText}${CLAUDE_BRACKETED_PASTE_END}`;
997
1031
  }
1032
+ beginInteractiveReadinessWait() {
1033
+ this.clearInteractiveReadinessTimer();
1034
+ this.interactiveReady = false;
1035
+ this.interactiveReadyTimer = setTimeout(() => {
1036
+ this.interactiveReadyTimer = null;
1037
+ this.markInteractiveReady();
1038
+ }, CLAUDE_INTERACTIVE_READY_TIMEOUT_MS);
1039
+ this.interactiveReadyTimer.unref?.();
1040
+ }
1041
+ waitForInteractiveReadiness() {
1042
+ if (this.interactiveReady) {
1043
+ return Promise.resolve();
1044
+ }
1045
+ return new Promise((resolve, reject) => {
1046
+ this.interactiveReadyWaiters.add({ resolve, reject });
1047
+ });
1048
+ }
1049
+ markInteractiveReady() {
1050
+ if (this.interactiveReady) {
1051
+ return;
1052
+ }
1053
+ this.interactiveReady = true;
1054
+ this.clearInteractiveReadinessTimer();
1055
+ for (const waiter of this.interactiveReadyWaiters) {
1056
+ waiter.resolve();
1057
+ }
1058
+ this.interactiveReadyWaiters.clear();
1059
+ if (this.state.status === "starting" && !this.hasAcceptedInput) {
1060
+ this.setStatus("idle", "Claude interactive session is ready.");
1061
+ }
1062
+ }
1063
+ rejectInteractiveReadiness(error) {
1064
+ this.clearInteractiveReadinessTimer();
1065
+ for (const waiter of this.interactiveReadyWaiters) {
1066
+ waiter.reject(error);
1067
+ }
1068
+ this.interactiveReadyWaiters.clear();
1069
+ this.interactiveReady = true;
1070
+ }
1071
+ clearInteractiveReadinessTimer() {
1072
+ if (!this.interactiveReadyTimer) {
1073
+ return;
1074
+ }
1075
+ clearTimeout(this.interactiveReadyTimer);
1076
+ this.interactiveReadyTimer = null;
1077
+ }
1078
+ armRemoteSubmitRetry(normalizedText) {
1079
+ this.clearRemoteSubmitRetry();
1080
+ this.remoteSubmitText = normalizedText;
1081
+ this.remoteSubmitRetryIndex = 0;
1082
+ this.scheduleRemoteSubmitRetry();
1083
+ }
1084
+ scheduleRemoteSubmitRetry() {
1085
+ const delayMs = CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS[this.remoteSubmitRetryIndex];
1086
+ if (delayMs === undefined || !this.remoteSubmitText) {
1087
+ return;
1088
+ }
1089
+ this.remoteSubmitRetryTimer = setTimeout(() => {
1090
+ this.remoteSubmitRetryTimer = null;
1091
+ if (!this.remoteSubmitText ||
1092
+ !this.pty ||
1093
+ this.state.status !== "busy" ||
1094
+ !this.pendingInjectedInputs.some((input) => input.normalizedText === this.remoteSubmitText)) {
1095
+ this.clearRemoteSubmitRetry();
1096
+ return;
1097
+ }
1098
+ this.writeToPty("\r");
1099
+ this.remoteSubmitRetryIndex += 1;
1100
+ if (this.remoteSubmitRetryIndex >= CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS.length) {
1101
+ this.clearRemoteSubmitRetry();
1102
+ this.emitClaudeNotice("Claude did not confirm the WeChat message submission. Press Enter in the local Claude terminal to retry.", "warning");
1103
+ return;
1104
+ }
1105
+ this.scheduleRemoteSubmitRetry();
1106
+ }, delayMs);
1107
+ this.remoteSubmitRetryTimer.unref?.();
1108
+ }
1109
+ clearRemoteSubmitRetry() {
1110
+ if (this.remoteSubmitRetryTimer) {
1111
+ clearTimeout(this.remoteSubmitRetryTimer);
1112
+ this.remoteSubmitRetryTimer = null;
1113
+ }
1114
+ this.remoteSubmitRetryIndex = 0;
1115
+ this.remoteSubmitText = null;
1116
+ }
998
1117
  // Fallback heuristic for older Claude Code versions that lack PostCompact hooks.
999
1118
  // The structured PostCompact hook event (handled in handleClaudeHookEnvelope) is
1000
1119
  // the reliable signal; this regex match serves as a best-effort fallback.
@@ -1043,6 +1162,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1043
1162
  }
1044
1163
  this.clearCompletionTimer();
1045
1164
  this.clearWechatWorkingNotice(true);
1165
+ this.clearRemoteSubmitRetry();
1046
1166
  this.pendingCliApprovalHints = null;
1047
1167
  this.flushPendingClaudeHookApprovals();
1048
1168
  this.pendingApproval = null;
@@ -1078,6 +1198,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1078
1198
  const completedPreview = this.currentPreview;
1079
1199
  this.clearCompletionTimer();
1080
1200
  this.clearWechatWorkingNotice(true);
1201
+ this.clearRemoteSubmitRetry();
1081
1202
  this.pendingCliApprovalHints = null;
1082
1203
  this.flushPendingClaudeHookApprovals();
1083
1204
  this.pendingApproval = null;
@@ -1167,6 +1288,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1167
1288
  }
1168
1289
  }
1169
1290
  handleClaudeSessionStart(payload) {
1291
+ this.markInteractiveReady();
1170
1292
  if (!payload.session_id) {
1171
1293
  return;
1172
1294
  }
@@ -1289,6 +1411,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1289
1411
  }
1290
1412
  const injectedIndex = findInjectedClaudePromptIndex(prompt, this.pendingInjectedInputs);
1291
1413
  if (injectedIndex >= 0) {
1414
+ this.clearRemoteSubmitRetry();
1292
1415
  this.pendingInjectedInputs.splice(injectedIndex, 1);
1293
1416
  return;
1294
1417
  }
@@ -1331,7 +1454,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1331
1454
  this.state.lastSessionSwitchReason = undefined;
1332
1455
  this.emitClaudeNotice(`Saved Claude conversation ${failedResumeConversationId} is no longer available. Starting a fresh Claude session.`, "warning");
1333
1456
  try {
1334
- await super.reset();
1457
+ await this.reset();
1335
1458
  }
1336
1459
  catch (error) {
1337
1460
  // recoverFromInvalidResume is invoked fire-and-forget (void) and there is
@@ -1440,6 +1563,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1440
1563
  this.currentPreview === "(idle)") {
1441
1564
  return;
1442
1565
  }
1566
+ this.clearRemoteSubmitRetry();
1443
1567
  this.clearWechatWorkingNotice(true);
1444
1568
  this.pendingCliApprovalHints = null;
1445
1569
  this.lastAutoApprovedPayload = null;