cli-wechat-bridge 1.1.6 → 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 (34) hide show
  1. package/README.md +163 -198
  2. package/bin/wecom-check-update.mjs +5 -0
  3. package/bin/wecom-claude.mjs +5 -0
  4. package/bin/wecom-codex.mjs +5 -0
  5. package/bin/wecom-daemon.mjs +5 -0
  6. package/bin/wecom-opencode.mjs +5 -0
  7. package/bin/wecom-pi.mjs +5 -0
  8. package/bin/wecom-setup.mjs +5 -0
  9. package/dist/bridge/bridge-adapters.claude.js +126 -3
  10. package/dist/bridge/bridge-process-reaper.js +35 -2
  11. package/dist/bridge/bridge-state.js +10 -3
  12. package/dist/bridge/wechat-bridge.js +277 -44
  13. package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
  14. package/dist/channels/wecom/bridge-final-reply.js +26 -0
  15. package/dist/channels/wecom/setup-cli.js +6 -0
  16. package/dist/channels/wecom/setup.js +176 -0
  17. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  18. package/dist/channels/wecom/wecom-config.js +66 -0
  19. package/dist/channels/wecom/wecom-media.js +141 -0
  20. package/dist/channels/wecom/wecom-message.js +149 -0
  21. package/dist/channels/wecom/wecom-transport.js +308 -0
  22. package/dist/companion/codex-remote-client.js +2 -1
  23. package/dist/companion/local-companion-start.js +49 -9
  24. package/dist/companion/local-companion.js +2 -1
  25. package/dist/core/channel-types.js +3 -1
  26. package/dist/core/direct-run.js +17 -0
  27. package/dist/core/inbound-message-claims.js +61 -0
  28. package/dist/daemon/daemon-link.js +3 -0
  29. package/dist/daemon/wechat-daemon.js +265 -36
  30. package/dist/i18n/messages-en.js +2 -0
  31. package/dist/i18n/messages-zh.js +2 -0
  32. package/dist/utils/doctor.js +30 -10
  33. package/dist/wechat/setup.js +113 -13
  34. package/package.json +20 -10
@@ -18,6 +18,8 @@ const CLAUDE_COMPACT_DEDUP_MS = 2_000;
18
18
  const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
19
19
  const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
20
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;
21
23
  const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
22
24
  const CLAUDE_SESSION_METADATA_READ_BYTES = 64 * 1024;
23
25
  const CLAUDE_MAX_SANITIZED_PROJECT_PATH = 200;
@@ -337,6 +339,12 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
337
339
  polledTranscriptSessionId = null;
338
340
  pendingResume = null;
339
341
  localResumeSessionEndObserved = false;
342
+ interactiveReady = true;
343
+ interactiveReadyTimer = null;
344
+ interactiveReadyWaiters = new Set();
345
+ remoteSubmitRetryTimer = null;
346
+ remoteSubmitRetryIndex = 0;
347
+ remoteSubmitText = null;
340
348
  constructor(options) {
341
349
  super(options);
342
350
  const shouldRestoreInitialSession = options.sessionStartMode !== "new";
@@ -364,6 +372,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
364
372
  if (this.pty) {
365
373
  return;
366
374
  }
375
+ this.beginInteractiveReadinessWait();
367
376
  this.startupOutputBuffer = "";
368
377
  this.hasAutoConfirmedWorkspaceTrustPrompt = false;
369
378
  ensureClaudeWorkspaceTrustAccepted(this.options.cwd);
@@ -385,11 +394,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
385
394
  this.state.activeRuntimeSessionId = undefined;
386
395
  }
387
396
  }
388
- await this.startHookServer();
389
397
  try {
398
+ await this.startHookServer();
390
399
  await super.start();
400
+ if (!this.interactiveReady) {
401
+ this.setStatus("starting", "Waiting for Claude interactive session...");
402
+ }
391
403
  }
392
404
  catch (error) {
405
+ this.rejectInteractiveReadiness(error instanceof Error ? error : new Error(String(error)));
393
406
  await this.stopHookServer();
394
407
  throw error;
395
408
  }
@@ -404,7 +417,18 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
404
417
  if (this.pendingApproval) {
405
418
  throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
406
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
+ }
407
430
  const normalizedText = normalizeOutput(text).trim();
431
+ this.clearRemoteSubmitRetry();
408
432
  this.pendingInjectedInputs.push({
409
433
  normalizedText,
410
434
  createdAtMs: Date.now(),
@@ -421,6 +445,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
421
445
  this.writeToPty(this.buildRemoteInputPayload(text));
422
446
  await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
423
447
  this.writeToPty("\r");
448
+ this.armRemoteSubmitRetry(normalizedText);
424
449
  this.armWechatWorkingNotice();
425
450
  }
426
451
  async listResumeSessions(limit = 10) {
@@ -530,6 +555,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
530
555
  return false;
531
556
  }
532
557
  this.clearWechatWorkingNotice(true);
558
+ this.clearRemoteSubmitRetry();
533
559
  this.pendingCliApprovalHints = null;
534
560
  this.flushPendingClaudeHookApprovals();
535
561
  if (this.pendingApproval) {
@@ -566,7 +592,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
566
592
  this.state.lastSessionSwitchAt = undefined;
567
593
  this.state.lastSessionSwitchSource = undefined;
568
594
  this.state.lastSessionSwitchReason = undefined;
569
- await super.reset();
595
+ await this.dispose();
596
+ await this.start();
570
597
  }
571
598
  async resolveApproval(action) {
572
599
  if (!this.pendingApproval) {
@@ -623,6 +650,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
623
650
  this.rejectPendingResume(new Error("Claude is shutting down during session switching."));
624
651
  this.detachLocalTerminal();
625
652
  this.clearWechatWorkingNotice(true);
653
+ this.clearRemoteSubmitRetry();
654
+ this.rejectInteractiveReadiness(new Error("Claude is shutting down."));
626
655
  this.pendingCliApprovalHints = null;
627
656
  this.flushPendingClaudeHookApprovals();
628
657
  this.stopTranscriptThinkingWatch();
@@ -720,8 +749,12 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
720
749
  handleExit(exitCode) {
721
750
  this.detachLocalTerminal();
722
751
  this.clearWechatWorkingNotice(true);
752
+ this.clearRemoteSubmitRetry();
723
753
  this.pendingCliApprovalHints = null;
724
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
+ }
725
758
  void this.stopHookServer();
726
759
  if (this.recoveringInvalidResume && !this.shuttingDown) {
727
760
  this.clearCompletionTimer();
@@ -996,6 +1029,91 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
996
1029
  }
997
1030
  return `${CLAUDE_BRACKETED_PASTE_START}${normalizedText}${CLAUDE_BRACKETED_PASTE_END}`;
998
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
+ }
999
1117
  // Fallback heuristic for older Claude Code versions that lack PostCompact hooks.
1000
1118
  // The structured PostCompact hook event (handled in handleClaudeHookEnvelope) is
1001
1119
  // the reliable signal; this regex match serves as a best-effort fallback.
@@ -1044,6 +1162,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1044
1162
  }
1045
1163
  this.clearCompletionTimer();
1046
1164
  this.clearWechatWorkingNotice(true);
1165
+ this.clearRemoteSubmitRetry();
1047
1166
  this.pendingCliApprovalHints = null;
1048
1167
  this.flushPendingClaudeHookApprovals();
1049
1168
  this.pendingApproval = null;
@@ -1079,6 +1198,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1079
1198
  const completedPreview = this.currentPreview;
1080
1199
  this.clearCompletionTimer();
1081
1200
  this.clearWechatWorkingNotice(true);
1201
+ this.clearRemoteSubmitRetry();
1082
1202
  this.pendingCliApprovalHints = null;
1083
1203
  this.flushPendingClaudeHookApprovals();
1084
1204
  this.pendingApproval = null;
@@ -1168,6 +1288,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1168
1288
  }
1169
1289
  }
1170
1290
  handleClaudeSessionStart(payload) {
1291
+ this.markInteractiveReady();
1171
1292
  if (!payload.session_id) {
1172
1293
  return;
1173
1294
  }
@@ -1290,6 +1411,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1290
1411
  }
1291
1412
  const injectedIndex = findInjectedClaudePromptIndex(prompt, this.pendingInjectedInputs);
1292
1413
  if (injectedIndex >= 0) {
1414
+ this.clearRemoteSubmitRetry();
1293
1415
  this.pendingInjectedInputs.splice(injectedIndex, 1);
1294
1416
  return;
1295
1417
  }
@@ -1332,7 +1454,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1332
1454
  this.state.lastSessionSwitchReason = undefined;
1333
1455
  this.emitClaudeNotice(`Saved Claude conversation ${failedResumeConversationId} is no longer available. Starting a fresh Claude session.`, "warning");
1334
1456
  try {
1335
- await super.reset();
1457
+ await this.reset();
1336
1458
  }
1337
1459
  catch (error) {
1338
1460
  // recoverFromInvalidResume is invoked fire-and-forget (void) and there is
@@ -1441,6 +1563,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1441
1563
  this.currentPreview === "(idle)") {
1442
1564
  return;
1443
1565
  }
1566
+ this.clearRemoteSubmitRetry();
1444
1567
  this.clearWechatWorkingNotice(true);
1445
1568
  this.pendingCliApprovalHints = null;
1446
1569
  this.lastAutoApprovedPayload = null;
@@ -113,7 +113,7 @@ function normalizeBridgeProcessRecord(value) {
113
113
  }
114
114
  return record;
115
115
  }
116
- export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
116
+ function parseWindowsProcessProbeRecords(stdout) {
117
117
  const trimmed = stdout.trim();
118
118
  if (!trimmed) {
119
119
  return [];
@@ -128,7 +128,13 @@ export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = proces
128
128
  const values = Array.isArray(parsed) ? parsed : [parsed];
129
129
  return values
130
130
  .map(normalizeBridgeProcessRecord)
131
- .filter((record) => Boolean(record))
131
+ .filter((record) => Boolean(record));
132
+ }
133
+ export function parseWindowsProcessRecordProbeOutput(stdout, currentPid = process.pid) {
134
+ return (parseWindowsProcessProbeRecords(stdout).find((record) => record.pid !== currentPid) ?? null);
135
+ }
136
+ export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
137
+ return parseWindowsProcessProbeRecords(stdout)
132
138
  .filter((record) => record.pid !== currentPid && isWechatBridgeCommandLine(record.commandLine));
133
139
  }
134
140
  export function parsePosixBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
@@ -195,10 +201,37 @@ export function listPeerBridgeProcesses(currentPid = process.pid) {
195
201
  ? listWindowsBridgeProcesses(currentPid)
196
202
  : listPosixBridgeProcesses(currentPid);
197
203
  }
204
+ function getWindowsProcessRecordByPid(pid, currentPid = process.pid) {
205
+ const probe = spawnSync("powershell.exe", [
206
+ "-NoLogo",
207
+ "-NoProfile",
208
+ "-NonInteractive",
209
+ "-ExecutionPolicy",
210
+ "Bypass",
211
+ "-Command",
212
+ [
213
+ "$ErrorActionPreference='Stop';",
214
+ `Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}'`,
215
+ "| Select-Object ProcessId,ParentProcessId,Name,CommandLine",
216
+ "| ConvertTo-Json -Compress",
217
+ ].join(" "),
218
+ ], {
219
+ encoding: "utf8",
220
+ windowsHide: true,
221
+ timeout: 8_000,
222
+ });
223
+ if (probe.status !== 0 || typeof probe.stdout !== "string") {
224
+ return null;
225
+ }
226
+ return parseWindowsProcessRecordProbeOutput(probe.stdout, currentPid);
227
+ }
198
228
  export function getProcessRecordByPid(pid, currentPid = process.pid) {
199
229
  if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) {
200
230
  return null;
201
231
  }
232
+ if (process.platform === "win32") {
233
+ return getWindowsProcessRecordByPid(pid, currentPid);
234
+ }
202
235
  return listAllProcessesRaw(currentPid).find((record) => record.pid === pid) ?? null;
203
236
  }
204
237
  export function listWechatDaemonProcesses(params = {}) {
@@ -79,6 +79,9 @@ export function normalizeBridgeLockPayload(value) {
79
79
  startedAt: record.startedAt,
80
80
  lifecycle: record.lifecycle === "companion_bound" ? "companion_bound" : "persistent",
81
81
  legacyLifecycleFallback: hasExplicitLifecycle ? undefined : true,
82
+ ...(record.channelId === "wechat" || record.channelId === "wecom"
83
+ ? { channelId: record.channelId }
84
+ : {}),
82
85
  };
83
86
  }
84
87
  export function readBridgeLockFile() {
@@ -148,11 +151,13 @@ function buildLockConflictError(lock) {
148
151
  * pid is alive but belongs to something else, and "unknown" when the command
149
152
  * line could not be resolved (probe failure, restricted permissions).
150
153
  */
151
- export function classifyLockHolderProcess(pid) {
152
- if (!isPidAlive(pid)) {
154
+ export function classifyLockHolderProcess(pid, dependencies = {}) {
155
+ const isProcessAlive = dependencies.isPidAlive ?? isPidAlive;
156
+ const getProcessRecord = dependencies.getProcessRecordByPid ?? getProcessRecordByPid;
157
+ if (!isProcessAlive(pid)) {
153
158
  return "unknown";
154
159
  }
155
- const record = getProcessRecordByPid(pid);
160
+ const record = getProcessRecord(pid);
156
161
  if (!record) {
157
162
  return "unknown";
158
163
  }
@@ -200,6 +205,7 @@ export class BridgeStateStore {
200
205
  cwd: options.cwd,
201
206
  startedAt: new Date(this.bridgeStartedAtMs).toISOString(),
202
207
  lifecycle: options.lifecycle,
208
+ channelId: options.channelId,
203
209
  };
204
210
  this.acquireLock();
205
211
  const shouldRestoreSessionState = options.sessionStartMode !== "new";
@@ -231,6 +237,7 @@ export class BridgeStateStore {
231
237
  resumeConversationId: persistedResumeConversationId,
232
238
  transcriptPath: persistedTranscriptPath,
233
239
  lastActivityAt: persisted?.lastActivityAt,
240
+ channelId: options.channelId,
234
241
  pendingConfirmation: null,
235
242
  pendingUserInput: null,
236
243
  };