@unlinearity/cli-wechat-bridge 1.0.0 → 1.0.2

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 (81) hide show
  1. package/bin/_run-entry.mjs +2 -8
  2. package/bin/wechat-bridge-claude.mjs +2 -2
  3. package/bin/wechat-bridge-codex.mjs +2 -2
  4. package/bin/wechat-bridge-opencode.mjs +2 -2
  5. package/bin/wechat-bridge-shell.mjs +2 -2
  6. package/bin/wechat-bridge.mjs +2 -2
  7. package/bin/wechat-check-update.mjs +2 -2
  8. package/bin/wechat-claude-start.mjs +2 -2
  9. package/bin/wechat-claude.mjs +2 -2
  10. package/bin/wechat-codex-start.mjs +2 -2
  11. package/bin/wechat-codex.mjs +2 -2
  12. package/bin/wechat-opencode-start.mjs +2 -2
  13. package/bin/wechat-opencode.mjs +2 -2
  14. package/bin/wechat-setup.mjs +5 -0
  15. package/dist/bridge/bridge-adapters.claude.js +829 -0
  16. package/dist/bridge/bridge-adapters.codex.js +2206 -0
  17. package/dist/bridge/bridge-adapters.core.js +668 -0
  18. package/dist/bridge/bridge-adapters.js +26 -0
  19. package/dist/bridge/bridge-adapters.opencode.js +1777 -0
  20. package/dist/bridge/bridge-adapters.shared.js +1098 -0
  21. package/dist/bridge/bridge-adapters.shell.js +363 -0
  22. package/dist/bridge/bridge-controller.js +44 -0
  23. package/dist/bridge/bridge-final-reply.js +31 -0
  24. package/dist/bridge/bridge-process-reaper.js +348 -0
  25. package/dist/bridge/bridge-state.js +356 -0
  26. package/dist/bridge/bridge-types.js +1 -0
  27. package/dist/bridge/bridge-utils.js +1150 -0
  28. package/dist/bridge/claude-hook.js +82 -0
  29. package/dist/bridge/claude-hooks.js +267 -0
  30. package/dist/bridge/wechat-bridge.js +904 -0
  31. package/dist/commands/check-update.js +30 -0
  32. package/dist/companion/codex-panel-link.js +72 -0
  33. package/dist/companion/codex-panel.js +179 -0
  34. package/dist/companion/codex-remote-client.js +117 -0
  35. package/dist/companion/local-companion-link.js +210 -0
  36. package/dist/companion/local-companion-start.js +372 -0
  37. package/dist/companion/local-companion.js +411 -0
  38. package/dist/media/media-types.js +1 -0
  39. package/dist/runtime/create-runtime-host.js +12 -0
  40. package/dist/runtime/legacy-adapter-runtime.js +40 -0
  41. package/dist/runtime/runtime-types.js +5 -0
  42. package/dist/utils/version-checker.js +161 -0
  43. package/dist/wechat/channel-config.js +103 -0
  44. package/dist/wechat/setup.js +283 -0
  45. package/dist/wechat/standalone-bot.js +356 -0
  46. package/dist/wechat/wechat-channel.js +481 -0
  47. package/dist/wechat/wechat-transport.js +887 -0
  48. package/package.json +6 -2
  49. package/src/bridge/bridge-adapters.claude.ts +0 -1047
  50. package/src/bridge/bridge-adapters.codex.ts +0 -2733
  51. package/src/bridge/bridge-adapters.core.ts +0 -877
  52. package/src/bridge/bridge-adapters.opencode.ts +0 -2390
  53. package/src/bridge/bridge-adapters.shared.ts +0 -1647
  54. package/src/bridge/bridge-adapters.shell.ts +0 -456
  55. package/src/bridge/bridge-adapters.ts +0 -30
  56. package/src/bridge/bridge-controller.ts +0 -58
  57. package/src/bridge/bridge-final-reply.ts +0 -56
  58. package/src/bridge/bridge-process-reaper.ts +0 -417
  59. package/src/bridge/bridge-state.ts +0 -520
  60. package/src/bridge/bridge-types.ts +0 -218
  61. package/src/bridge/bridge-utils.ts +0 -1524
  62. package/src/bridge/claude-hook.ts +0 -92
  63. package/src/bridge/claude-hooks.ts +0 -376
  64. package/src/bridge/wechat-bridge.ts +0 -1305
  65. package/src/commands/check-update.ts +0 -46
  66. package/src/companion/codex-panel-link.ts +0 -151
  67. package/src/companion/codex-panel.ts +0 -225
  68. package/src/companion/codex-remote-client.ts +0 -178
  69. package/src/companion/local-companion-link.ts +0 -338
  70. package/src/companion/local-companion-start.ts +0 -520
  71. package/src/companion/local-companion.ts +0 -532
  72. package/src/media/media-types.ts +0 -11
  73. package/src/runtime/create-runtime-host.ts +0 -16
  74. package/src/runtime/legacy-adapter-runtime.ts +0 -55
  75. package/src/runtime/runtime-types.ts +0 -51
  76. package/src/utils/version-checker.ts +0 -197
  77. package/src/wechat/channel-config.ts +0 -141
  78. package/src/wechat/setup.ts +0 -232
  79. package/src/wechat/standalone-bot.ts +0 -483
  80. package/src/wechat/wechat-channel.ts +0 -608
  81. package/src/wechat/wechat-transport.ts +0 -1290
@@ -1,1305 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import path from "node:path";
4
-
5
- import {
6
- resolveDefaultAdapterCommand,
7
- } from "./bridge-adapters.ts";
8
- import { delay } from "./bridge-adapters.shared.ts";
9
- import { BridgeController } from "./bridge-controller.ts";
10
- import { forwardWechatFinalReply } from "./bridge-final-reply.ts";
11
- import { migrateLegacyChannelFiles } from "../wechat/channel-config.ts";
12
- import { BridgeStateStore } from "./bridge-state.ts";
13
- import { reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses } from "./bridge-process-reaper.ts";
14
- import { createRuntimeHost } from "../runtime/create-runtime-host.ts";
15
- import type {
16
- ApprovalRequest,
17
- BridgeAdapter,
18
- BridgeAdapterKind,
19
- BridgeEvent,
20
- BridgeLifecycleMode,
21
- BridgeTurnOrigin,
22
- BridgeWorkerStatus,
23
- PendingApproval,
24
- } from "./bridge-types.ts";
25
- import {
26
- buildWechatInboundPrompt,
27
- buildOneTimeCode,
28
- formatApprovalMessage,
29
- formatPendingApprovalReminder,
30
- formatDuration,
31
- formatMirroredUserInputMessage,
32
- formatSessionSwitchMessage,
33
- formatStatusReport,
34
- formatTaskFailedMessage,
35
- MESSAGE_START_GRACE_MS,
36
- nowIso,
37
- OutputBatcher,
38
- parseWechatControlCommand,
39
- truncatePreview,
40
- } from "./bridge-utils.ts";
41
- import {
42
- classifyWechatTransportError,
43
- DEFAULT_LONG_POLL_TIMEOUT_MS,
44
- WeChatTransport,
45
- describeWechatTransportError,
46
- type InboundWechatMessage,
47
- } from "../wechat/wechat-transport.ts";
48
- import {
49
- checkForUpdate,
50
- formatUpdateMessage,
51
- } from "../utils/version-checker.ts";
52
-
53
- type BridgeCliOptions = {
54
- adapter: BridgeAdapterKind;
55
- command: string;
56
- cwd: string;
57
- profile?: string;
58
- lifecycle: BridgeLifecycleMode;
59
- };
60
-
61
- type ActiveTask = {
62
- startedAt: number;
63
- inputPreview: string;
64
- };
65
-
66
- type DeferredInboundMessage = {
67
- message: InboundWechatMessage;
68
- };
69
-
70
- type WechatSendContext =
71
- | "message"
72
- | "notice"
73
- | "approval_required"
74
- | "mirrored_user_input"
75
- | "session_switched"
76
- | "thread_switched"
77
- | "task_failed"
78
- | "fatal_error"
79
- | "inbound_error";
80
-
81
- const POLL_RETRY_BASE_MS = 1_000;
82
- const POLL_RETRY_MAX_MS = 30_000;
83
- const PARENT_PROCESS_POLL_MS = 5_000;
84
-
85
- function log(message: string): void {
86
- process.stderr.write(`[wechat-bridge] ${message}\n`);
87
- }
88
-
89
- function logError(message: string): void {
90
- process.stderr.write(`[wechat-bridge] ERROR: ${message}\n`);
91
- }
92
-
93
- function computePollRetryDelayMs(consecutiveFailures: number): number {
94
- const normalizedFailures = Math.max(1, consecutiveFailures);
95
- const exponent = Math.min(normalizedFailures - 1, 5);
96
- return Math.min(POLL_RETRY_MAX_MS, POLL_RETRY_BASE_MS * 2 ** exponent);
97
- }
98
-
99
- function isPidAlive(pid: number): boolean {
100
- if (!Number.isInteger(pid) || pid <= 0) {
101
- return false;
102
- }
103
-
104
- try {
105
- process.kill(pid, 0);
106
- return true;
107
- } catch {
108
- return false;
109
- }
110
- }
111
-
112
- export function formatUserFacingBridgeFatalError(message: string): string {
113
- return `Bridge error: ${message.replace(/\s+Recent app-server log:.*$/s, "").trim()}`;
114
- }
115
-
116
- export function shouldForwardBridgeEventToWechat(
117
- adapter: BridgeAdapterKind,
118
- eventType: BridgeEvent["type"],
119
- options: {
120
- text?: string;
121
- } = {},
122
- ): boolean {
123
- if (adapter !== "opencode") {
124
- return true;
125
- }
126
-
127
- switch (eventType) {
128
- case "stdout":
129
- case "stderr":
130
- case "thread_switched":
131
- return false;
132
- case "notice":
133
- return /^OpenCode local draft:\s*/i.test(options.text ?? "");
134
- case "mirrored_user_input":
135
- return true;
136
- default:
137
- return true;
138
- }
139
- }
140
-
141
- export function formatUserFacingInboundError(params: {
142
- adapter: BridgeAdapterKind;
143
- cwd?: string;
144
- errorText: string;
145
- isUserFacingShellRejection: boolean;
146
- }): string {
147
- const { adapter, cwd, errorText, isUserFacingShellRejection } = params;
148
- if (isUserFacingShellRejection) {
149
- return errorText;
150
- }
151
-
152
- if (
153
- adapter === "opencode" &&
154
- /opencode companion is not connected/i.test(errorText)
155
- ) {
156
- return cwd
157
- ? `OpenCode companion is not connected for bridge workspace:\n${cwd}\nRun "wechat-opencode" in that directory to reconnect the current local terminal, or run "wechat-bridge-opencode" and then "wechat-opencode" in your target project to replace this bridge.`
158
- : 'OpenCode companion is not connected. Start "wechat-opencode" in this directory to reconnect it, then retry.';
159
- }
160
-
161
- return `Bridge error: ${errorText}`;
162
- }
163
-
164
- export function formatWechatSendFailureLogEntry(params: {
165
- context: WechatSendContext;
166
- recipientId: string;
167
- error: unknown;
168
- }): string {
169
- return `wechat_send_failed: context=${params.context} recipient=${params.recipientId} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
170
- }
171
-
172
- export function shouldWatchParentProcess(options: {
173
- startupParentPid: number;
174
- attachedToTerminal: boolean;
175
- lifecycle: BridgeLifecycleMode;
176
- }): boolean {
177
- return (
178
- options.startupParentPid > 1 &&
179
- (options.attachedToTerminal || options.lifecycle === "companion_bound")
180
- );
181
- }
182
-
183
- function toPendingApproval(request: ApprovalRequest | PendingApproval): PendingApproval {
184
- if (typeof (request as PendingApproval).code === "string") {
185
- return request as PendingApproval;
186
- }
187
-
188
- return {
189
- ...request,
190
- code: buildOneTimeCode(),
191
- createdAt: nowIso(),
192
- };
193
- }
194
-
195
- export function shouldDeferCodexInboundMessage(params: {
196
- adapter: BridgeAdapterKind;
197
- status: BridgeWorkerStatus;
198
- activeTurnOrigin?: BridgeTurnOrigin;
199
- hasPendingConfirmation: boolean;
200
- hasSystemCommand: boolean;
201
- }): boolean {
202
- return (
203
- params.adapter === "codex" &&
204
- !params.hasPendingConfirmation &&
205
- !params.hasSystemCommand &&
206
- params.activeTurnOrigin === "local" &&
207
- (params.status === "busy" || params.status === "awaiting_approval")
208
- );
209
- }
210
-
211
- export function canDrainDeferredCodexInboundQueue(params: {
212
- adapter: BridgeAdapterKind;
213
- deferredCount: number;
214
- status: BridgeWorkerStatus;
215
- activeTurnId?: string;
216
- hasPendingConfirmation: boolean;
217
- hasPendingApproval: boolean;
218
- hasActiveTask: boolean;
219
- }): boolean {
220
- return (
221
- params.adapter === "codex" &&
222
- params.deferredCount > 0 &&
223
- !params.hasPendingConfirmation &&
224
- !params.hasPendingApproval &&
225
- !params.hasActiveTask &&
226
- !params.activeTurnId &&
227
- params.status !== "busy" &&
228
- params.status !== "awaiting_approval"
229
- );
230
- }
231
-
232
- export function formatDeferredCodexInboundQueueMessage(queuePosition: number): string {
233
- return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
234
- }
235
-
236
- export function isRetryableDeferredCodexDrainError(errorText: string): boolean {
237
- return /still working|approval request is pending|waiting for local terminal input/i.test(
238
- errorText,
239
- );
240
- }
241
-
242
- export function parseCliArgs(argv: string[]): BridgeCliOptions {
243
- let adapter: BridgeAdapterKind | null = null;
244
- let commandOverride: string | undefined;
245
- let cwd = process.cwd();
246
- let profile: string | undefined;
247
- let lifecycle: BridgeLifecycleMode = "persistent";
248
-
249
- for (let i = 0; i < argv.length; i += 1) {
250
- const arg = argv[i];
251
- const next = argv[i + 1];
252
-
253
- switch (arg) {
254
- case "--adapter":
255
- if (!next || !["codex", "claude", "opencode", "shell"].includes(next)) {
256
- throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
257
- }
258
- adapter = next as BridgeAdapterKind;
259
- i += 1;
260
- break;
261
- case "--cmd":
262
- if (!next) {
263
- throw new Error("--cmd requires a value");
264
- }
265
- commandOverride = next;
266
- i += 1;
267
- break;
268
- case "--cwd":
269
- if (!next) {
270
- throw new Error("--cwd requires a value");
271
- }
272
- cwd = path.resolve(next);
273
- i += 1;
274
- break;
275
- case "--profile":
276
- if (!next) {
277
- throw new Error("--profile requires a value");
278
- }
279
- profile = next;
280
- i += 1;
281
- break;
282
- case "--lifecycle":
283
- if (!next || !["persistent", "companion_bound"].includes(next)) {
284
- throw new Error(`Invalid lifecycle: ${next ?? "(missing)"}`);
285
- }
286
- lifecycle = next as BridgeLifecycleMode;
287
- i += 1;
288
- break;
289
- case "--shutdown-on-parent-exit":
290
- lifecycle = "companion_bound";
291
- break;
292
- case "--help":
293
- case "-h":
294
- printUsageAndExit();
295
- break;
296
- default:
297
- throw new Error(`Unknown argument: ${arg}`);
298
- }
299
- }
300
-
301
- if (!adapter) {
302
- throw new Error("Missing required --adapter <codex|claude|opencode|shell>");
303
- }
304
-
305
- const defaultCommand = resolveDefaultAdapterCommand(adapter);
306
- return {
307
- adapter,
308
- command: commandOverride ?? defaultCommand,
309
- cwd,
310
- profile,
311
- lifecycle,
312
- };
313
- }
314
-
315
- function printUsageAndExit(): never {
316
- process.stdout.write(
317
- [
318
- "Usage: wechat-bridge --adapter <codex|claude|opencode|shell> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>]",
319
- "",
320
- "Examples:",
321
- " wechat-bridge-codex",
322
- " wechat-bridge-claude --cwd ~/work/my-project",
323
- " wechat-bridge-opencode --cwd ~/work/my-project",
324
- " wechat-bridge-shell --cmd pwsh # headless shell executor for non-interactive commands/scripts",
325
- " wechat-bridge-shell --cmd bash # headless shell executor for non-interactive commands/scripts",
326
- " wechat-bridge-codex --lifecycle companion_bound",
327
- " bun run bridge:codex # repo-local development entrypoint",
328
- " bun run bridge:opencode # repo-local development entrypoint",
329
- "",
330
- ].join("\n"),
331
- );
332
- process.exit(0);
333
- }
334
-
335
- async function main(): Promise<void> {
336
- migrateLegacyChannelFiles(log);
337
-
338
- // 非阻塞地检查更新(不影响启动速度)
339
- setTimeout(async () => {
340
- try {
341
- const versionInfo = await checkForUpdate();
342
- if (versionInfo?.hasUpdate) {
343
- log(formatUpdateMessage(versionInfo));
344
- }
345
- } catch (error) {
346
- // 静默失败,不影响正常使用
347
- }
348
- }, 3000); // 延迟3秒,确保不影响启动
349
-
350
- const options = parseCliArgs(process.argv.slice(2));
351
- const transport = new WeChatTransport({ log, logError });
352
-
353
- const credentials = transport.getCredentials();
354
- if (!credentials) {
355
- throw new Error('No saved WeChat credentials found. Run "bun run setup" first.');
356
- }
357
- if (!credentials.userId) {
358
- throw new Error('Saved WeChat credentials are missing userId. Run "bun run setup" again.');
359
- }
360
-
361
- const stateStore = new BridgeStateStore({
362
- ...options,
363
- authorizedUserId: credentials.userId,
364
- });
365
- const reapedPeerPids = await reapPeerBridgeProcesses({
366
- logger: (message) => stateStore.appendLog(message),
367
- });
368
- if (reapedPeerPids.length > 0) {
369
- log(`Reaped ${reapedPeerPids.length} stale bridge process(es): ${reapedPeerPids.join(", ")}`);
370
- }
371
-
372
- if (options.adapter === "opencode") {
373
- const reapedOpencodePids = await reapOrphanedOpencodeProcesses({
374
- logger: (message) => stateStore.appendLog(message),
375
- });
376
- if (reapedOpencodePids.length > 0) {
377
- log(`Reaped ${reapedOpencodePids.length} orphaned opencode process(es): ${reapedOpencodePids.join(", ")}`);
378
- }
379
- }
380
-
381
- let lockRehydratedLogged = false;
382
- const ensureRuntimeOwnership = (): boolean => {
383
- const ownership = stateStore.verifyRuntimeOwnership();
384
- if (!ownership.ok) {
385
- if (ownership.reason === "superseded") {
386
- requestShutdown(
387
- `Bridge instance ${stateStore.getState().instanceId} was superseded by ${ownership.activeInstanceId}. Stopping duplicate bridge.`,
388
- );
389
- return false;
390
- }
391
-
392
- requestShutdown(
393
- `Bridge instance ${stateStore.getState().instanceId} lost the global lock to pid=${ownership.activePid} (${ownership.activeInstanceId}). Stopping duplicate bridge.`,
394
- );
395
- return false;
396
- }
397
-
398
- if (ownership.rehydratedLock && !lockRehydratedLogged) {
399
- lockRehydratedLogged = true;
400
- stateStore.appendLog(
401
- `lock_rehydrated: pid=${process.pid} instanceId=${stateStore.getState().instanceId} adapter=${options.adapter} cwd=${options.cwd}`,
402
- );
403
- }
404
-
405
- return true;
406
- };
407
-
408
- // Clear any stale endpoint left by a previous bridge for this workspace.
409
- // This prevents `wechat-*` companions from reconnecting to a dead bridge
410
- // while the new runtime is still starting up.
411
- const adapter = createRuntimeHost({
412
- kind: options.adapter,
413
- command: options.command,
414
- cwd: options.cwd,
415
- profile: options.profile,
416
- lifecycle: options.lifecycle,
417
- initialSharedSessionId:
418
- stateStore.getState().sharedSessionId ?? stateStore.getState().sharedThreadId,
419
- initialResumeConversationId: stateStore.getState().resumeConversationId,
420
- initialTranscriptPath: stateStore.getState().transcriptPath,
421
- });
422
- const controller = new BridgeController(adapter, options.cwd);
423
- controller.clearLocalClientEndpoint();
424
- stateStore.appendLog(`Cleared stale companion endpoint for ${options.cwd} before adapter start.`);
425
- let textSendChain = Promise.resolve();
426
- let attachmentSendChain = Promise.resolve();
427
- let activeTask: ActiveTask | null = null;
428
- const deferredInboundMessages: DeferredInboundMessage[] = [];
429
- let drainingDeferredInboundMessages = false;
430
- let lastOutputAt = 0;
431
- let lastHeartbeatAt = 0;
432
- let consecutivePollFailures = 0;
433
-
434
- const queueWechatTextAction = <T>(action: () => Promise<T>) => {
435
- const run = textSendChain.then(action);
436
- textSendChain = run.then(
437
- () => undefined,
438
- () => undefined,
439
- );
440
- return run;
441
- };
442
-
443
- const queueWechatAttachmentAction = <T>(action: () => Promise<T>) => {
444
- const run = attachmentSendChain.then(action);
445
- attachmentSendChain = run.then(
446
- () => undefined,
447
- () => undefined,
448
- );
449
- return run;
450
- };
451
-
452
- const queueWechatMessage = (
453
- senderId: string,
454
- text: string,
455
- context: WechatSendContext = "message",
456
- ) => {
457
- return queueWechatTextAction(() => transport.sendText(senderId, text)).catch((err) => {
458
- logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
459
- stateStore.appendLog(
460
- formatWechatSendFailureLogEntry({
461
- context,
462
- recipientId: senderId,
463
- error: err,
464
- }),
465
- );
466
- });
467
- };
468
-
469
- const outputBatcher = new OutputBatcher(async (text) => {
470
- await queueWechatMessage(stateStore.getState().authorizedUserId, text);
471
- });
472
- const maybeDrainDeferredInboundMessages = async (): Promise<void> => {
473
- if (drainingDeferredInboundMessages || !ensureRuntimeOwnership()) {
474
- return;
475
- }
476
-
477
- const adapterState = adapter.getState();
478
- if (
479
- !canDrainDeferredCodexInboundQueue({
480
- adapter: options.adapter,
481
- deferredCount: deferredInboundMessages.length,
482
- status: adapterState.status,
483
- activeTurnId: adapterState.activeTurnId,
484
- hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
485
- hasPendingApproval: Boolean(adapterState.pendingApproval),
486
- hasActiveTask: Boolean(activeTask),
487
- })
488
- ) {
489
- return;
490
- }
491
-
492
- const nextDeferred = deferredInboundMessages.shift();
493
- if (!nextDeferred) {
494
- return;
495
- }
496
-
497
- drainingDeferredInboundMessages = true;
498
- try {
499
- stateStore.appendLog(
500
- `draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`,
501
- );
502
- const nextTask = await dispatchInboundWechatText({
503
- message: nextDeferred.message,
504
- options,
505
- stateStore,
506
- adapter,
507
- });
508
- activeTask = nextTask;
509
- lastHeartbeatAt = 0;
510
- } catch (err) {
511
- const errorText = err instanceof Error ? err.message : String(err);
512
- if (isRetryableDeferredCodexDrainError(errorText)) {
513
- deferredInboundMessages.unshift(nextDeferred);
514
- stateStore.appendLog(
515
- `deferred_inbound_blocked: ${truncatePreview(errorText, 400)}`,
516
- );
517
- return;
518
- }
519
-
520
- logError(errorText);
521
- stateStore.appendLog(`deferred_inbound_error: ${errorText}`);
522
- await queueWechatMessage(
523
- nextDeferred.message.senderId,
524
- formatUserFacingInboundError({
525
- adapter: options.adapter,
526
- cwd: options.cwd,
527
- errorText,
528
- isUserFacingShellRejection: false,
529
- }),
530
- "inbound_error",
531
- );
532
- } finally {
533
- drainingDeferredInboundMessages = false;
534
- }
535
- };
536
- const startupParentPid = process.ppid;
537
- const attachedToTerminal = Boolean(
538
- process.stdin.isTTY || process.stdout.isTTY || process.stderr.isTTY,
539
- );
540
- let shutdownPromise: Promise<void> | null = null;
541
- let requestedExitCode = 0;
542
- let stdinDetached = false;
543
- const parentWatchTimer =
544
- shouldWatchParentProcess({
545
- startupParentPid,
546
- attachedToTerminal,
547
- lifecycle: options.lifecycle,
548
- })
549
- ? setInterval(() => {
550
- if (shutdownPromise || isPidAlive(startupParentPid)) {
551
- return;
552
- }
553
- log(`Parent process ${startupParentPid} exited. Stopping bridge.`);
554
- void shutdown(0);
555
- }, PARENT_PROCESS_POLL_MS)
556
- : null;
557
- parentWatchTimer?.unref();
558
-
559
- const cleanup = async () => {
560
- if (parentWatchTimer) {
561
- clearInterval(parentWatchTimer);
562
- }
563
- try {
564
- await outputBatcher.flushNow();
565
- } catch {
566
- // Best effort flush.
567
- }
568
- try {
569
- await textSendChain;
570
- await attachmentSendChain;
571
- } catch {
572
- // Best effort flush.
573
- }
574
- try {
575
- await adapter.dispose();
576
- } catch {
577
- // Best effort shutdown.
578
- }
579
- controller.clearLocalClientEndpoint();
580
- stateStore.releaseLock();
581
- };
582
-
583
- const shutdown = async (exitCode = 0): Promise<void> => {
584
- requestedExitCode = exitCode;
585
- if (!shutdownPromise) {
586
- shutdownPromise = cleanup().catch((error) => {
587
- logError(`Shutdown cleanup failed: ${describeWechatTransportError(error)}`);
588
- });
589
- }
590
- await shutdownPromise;
591
- };
592
-
593
- const requestShutdown = (message: string, exitCode = 0) => {
594
- if (shutdownPromise) {
595
- return;
596
- }
597
- log(message);
598
- void shutdown(exitCode).finally(() => process.exit(requestedExitCode));
599
- };
600
-
601
- process.once("SIGINT", () => {
602
- requestShutdown("Received SIGINT. Stopping bridge.");
603
- });
604
- process.once("SIGTERM", () => {
605
- requestShutdown("Received SIGTERM. Stopping bridge.");
606
- });
607
- process.once("SIGHUP", () => {
608
- requestShutdown("Terminal session closed. Stopping bridge.");
609
- });
610
- if (process.platform === "win32") {
611
- process.once("SIGBREAK", () => {
612
- requestShutdown("Received SIGBREAK. Stopping bridge.");
613
- });
614
- }
615
- if (attachedToTerminal) {
616
- process.stdin.on("close", () => {
617
- if (stdinDetached) {
618
- return;
619
- }
620
- stdinDetached = true;
621
- requestShutdown("Standard input closed. Stopping bridge.");
622
- });
623
- process.stdin.on("end", () => {
624
- if (stdinDetached) {
625
- return;
626
- }
627
- stdinDetached = true;
628
- requestShutdown("Standard input ended. Stopping bridge.");
629
- });
630
- }
631
- process.on("exit", () => {
632
- if (parentWatchTimer) {
633
- clearInterval(parentWatchTimer);
634
- }
635
- stateStore.releaseLock();
636
- });
637
-
638
- try {
639
- wireAdapterEvents({
640
- adapter,
641
- options,
642
- transport,
643
- stateStore,
644
- outputBatcher,
645
- queueWechatAttachmentAction,
646
- queueWechatMessage,
647
- maybeDrainDeferredInboundMessages,
648
- getActiveTask: () => activeTask,
649
- clearActiveTask: () => {
650
- activeTask = null;
651
- lastHeartbeatAt = 0;
652
- },
653
- updateLastOutputAt: () => {
654
- lastOutputAt = Date.now();
655
- },
656
- syncSharedSessionState: () => {
657
- syncSharedSessionState(stateStore, adapter);
658
- },
659
- syncLocalClientEndpoint: () => {
660
- controller.syncLocalClientEndpoint();
661
- },
662
- requestShutdown,
663
- });
664
-
665
- await adapter.start();
666
- if (!ensureRuntimeOwnership()) {
667
- return;
668
- }
669
- syncSharedSessionState(stateStore, adapter);
670
- controller.syncLocalClientEndpoint();
671
- stateStore.appendLog(
672
- `Bridge started with adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`,
673
- );
674
-
675
- log(`WeChat bridge is ready for adapter "${options.adapter}".`);
676
- log(`Working directory: ${options.cwd}`);
677
- if (options.profile) {
678
- log(`Profile: ${options.profile}`);
679
- }
680
- log(`Authorized WeChat user: ${credentials.userId}`);
681
- if (options.adapter === "codex") {
682
- log(
683
- 'Start the visible Codex panel in a second terminal with: wechat-codex',
684
- );
685
- } else if (options.adapter === "opencode") {
686
- log(
687
- 'Start the visible OpenCode companion in a second terminal with: wechat-opencode',
688
- );
689
- } else if (options.adapter === "claude") {
690
- log(
691
- 'Start the visible Claude companion in a second terminal with: wechat-claude',
692
- );
693
- } else if (options.adapter === "shell") {
694
- log(
695
- "Shell mode runs as a headless remote executor for non-interactive commands and scripts.",
696
- );
697
- }
698
-
699
- while (true) {
700
- if (!ensureRuntimeOwnership()) {
701
- break;
702
- }
703
-
704
- let pollResult: Awaited<ReturnType<WeChatTransport["pollMessages"]>>;
705
- try {
706
- pollResult = await transport.pollMessages({
707
- timeoutMs: DEFAULT_LONG_POLL_TIMEOUT_MS,
708
- minCreatedAtMs: stateStore.getState().bridgeStartedAtMs - MESSAGE_START_GRACE_MS,
709
- });
710
- } catch (err) {
711
- const classification = classifyWechatTransportError(err);
712
- if (!classification.retryable) {
713
- throw err;
714
- }
715
-
716
- consecutivePollFailures += 1;
717
- const delayMs = computePollRetryDelayMs(consecutivePollFailures);
718
- const errorText = describeWechatTransportError(err);
719
- const statusDetails =
720
- typeof classification.statusCode === "number"
721
- ? ` status=${classification.statusCode}`
722
- : "";
723
- logError(
724
- `WeChat long poll failed (${classification.kind}${statusDetails}, attempt ${consecutivePollFailures}). Retrying in ${formatDuration(delayMs)}. ${errorText}`,
725
- );
726
- stateStore.appendLog(
727
- `poll_retry: kind=${classification.kind}${statusDetails} attempt=${consecutivePollFailures} delay_ms=${delayMs} error=${truncatePreview(errorText, 400)}`,
728
- );
729
- await delay(delayMs);
730
- continue;
731
- }
732
-
733
- if (!ensureRuntimeOwnership()) {
734
- break;
735
- }
736
-
737
- if (consecutivePollFailures > 0) {
738
- const recoveredFailures = consecutivePollFailures;
739
- consecutivePollFailures = 0;
740
- log(`WeChat long poll recovered after ${recoveredFailures} transient error(s).`);
741
- stateStore.appendLog(`poll_recovered: failures=${recoveredFailures}`);
742
- }
743
-
744
- if (pollResult.ignoredBacklogCount > 0) {
745
- stateStore.incrementIgnoredBacklog(pollResult.ignoredBacklogCount);
746
- stateStore.appendLog(
747
- `ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`,
748
- );
749
- }
750
-
751
- for (const message of pollResult.messages) {
752
- if (!ensureRuntimeOwnership()) {
753
- break;
754
- }
755
-
756
- stateStore.touchActivity(message.createdAt);
757
- let nextTask: ActiveTask | null = null;
758
- try {
759
- nextTask = await handleInboundMessage({
760
- message,
761
- options,
762
- stateStore,
763
- adapter,
764
- queueWechatMessage,
765
- outputBatcher,
766
- deferInboundMessage: async (nextMessage) => {
767
- deferredInboundMessages.push({
768
- message: nextMessage,
769
- });
770
- stateStore.appendLog(
771
- `deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`,
772
- );
773
- await queueWechatMessage(
774
- nextMessage.senderId,
775
- formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length),
776
- );
777
- },
778
- });
779
- } catch (err) {
780
- const errorText = err instanceof Error ? err.message : String(err);
781
- const isUserFacingShellRejection =
782
- err instanceof Error && err.name === "ShellCommandRejectedError";
783
- logError(errorText);
784
- stateStore.appendLog(
785
- `${isUserFacingShellRejection ? "inbound_rejected" : "inbound_error"}: ${errorText}`,
786
- );
787
- await queueWechatMessage(
788
- message.senderId,
789
- formatUserFacingInboundError({
790
- adapter: options.adapter,
791
- cwd: options.cwd,
792
- errorText,
793
- isUserFacingShellRejection,
794
- }),
795
- "inbound_error",
796
- );
797
- }
798
- if (nextTask) {
799
- activeTask = nextTask;
800
- lastHeartbeatAt = 0;
801
- }
802
- syncSharedSessionState(stateStore, adapter);
803
- await maybeDrainDeferredInboundMessages();
804
- }
805
-
806
- const adapterState = adapter.getState();
807
- const lastSignalAt = Math.max(lastHeartbeatAt, lastOutputAt || activeTask?.startedAt || 0);
808
-
809
- if (
810
- activeTask &&
811
- options.adapter === "shell" &&
812
- adapterState.status === "busy" &&
813
- Date.now() - lastSignalAt >= 30_000
814
- ) {
815
- lastHeartbeatAt = Date.now();
816
- await queueWechatMessage(
817
- stateStore.getState().authorizedUserId,
818
- `${options.adapter} is still running. Waiting for more output...`,
819
- );
820
- }
821
- }
822
- } finally {
823
- await shutdown(requestedExitCode);
824
- }
825
- }
826
-
827
- function syncSharedSessionState(
828
- stateStore: BridgeStateStore,
829
- adapter: BridgeAdapter,
830
- ): void {
831
- const persistedState = stateStore.getState();
832
- const persistedSessionId = persistedState.sharedSessionId ?? persistedState.sharedThreadId;
833
- const adapterState = adapter.getState();
834
- const adapterSessionId = adapterState.sharedSessionId ?? adapterState.sharedThreadId;
835
-
836
- if (adapterSessionId && adapterSessionId !== persistedSessionId) {
837
- stateStore.setSharedSessionId(adapterSessionId);
838
- } else if (!adapterSessionId && persistedSessionId) {
839
- stateStore.clearSharedSessionId();
840
- }
841
-
842
- if (persistedState.adapter !== "claude") {
843
- return;
844
- }
845
-
846
- if (
847
- adapterState.resumeConversationId !== persistedState.resumeConversationId ||
848
- adapterState.transcriptPath !== persistedState.transcriptPath
849
- ) {
850
- if (adapterState.resumeConversationId || adapterState.transcriptPath) {
851
- stateStore.setClaudeResumeState(
852
- adapterState.resumeConversationId,
853
- adapterState.transcriptPath,
854
- );
855
- } else {
856
- stateStore.clearClaudeResumeState();
857
- }
858
- }
859
- }
860
-
861
- function wireAdapterEvents(params: {
862
- adapter: BridgeAdapter;
863
- options: BridgeCliOptions;
864
- transport: WeChatTransport;
865
- stateStore: BridgeStateStore;
866
- outputBatcher: OutputBatcher;
867
- queueWechatAttachmentAction: <T>(action: () => Promise<T>) => Promise<T>;
868
- queueWechatMessage: (
869
- senderId: string,
870
- text: string,
871
- context?: WechatSendContext,
872
- ) => Promise<void>;
873
- maybeDrainDeferredInboundMessages: () => Promise<void>;
874
- getActiveTask: () => ActiveTask | null;
875
- clearActiveTask: () => void;
876
- updateLastOutputAt: () => void;
877
- syncSharedSessionState: () => void;
878
- syncLocalClientEndpoint: () => void;
879
- requestShutdown: (message: string, exitCode?: number) => void;
880
- }): void {
881
- const {
882
- adapter,
883
- options,
884
- transport,
885
- stateStore,
886
- outputBatcher,
887
- queueWechatAttachmentAction,
888
- queueWechatMessage,
889
- maybeDrainDeferredInboundMessages,
890
- getActiveTask,
891
- clearActiveTask,
892
- updateLastOutputAt,
893
- syncSharedSessionState,
894
- syncLocalClientEndpoint,
895
- requestShutdown,
896
- } = params;
897
-
898
- adapter.setEventSink((event) => {
899
- syncSharedSessionState();
900
- syncLocalClientEndpoint();
901
- const adapterState = adapter.getState();
902
- const bridgeState = stateStore.getState();
903
- if (bridgeState.pendingConfirmation && !adapterState.pendingApproval) {
904
- stateStore.clearPendingConfirmation();
905
- }
906
- const authorizedUserId = stateStore.getState().authorizedUserId;
907
-
908
- switch (event.type) {
909
- case "stdout":
910
- case "stderr":
911
- updateLastOutputAt();
912
- if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
913
- outputBatcher.push(event.text);
914
- }
915
- break;
916
- case "final_reply":
917
- void outputBatcher.flushNow().then(async () => {
918
- await forwardWechatFinalReply({
919
- adapter: options.adapter,
920
- rawText: event.text,
921
- sender: {
922
- sendText: (text) => queueWechatMessage(authorizedUserId, text),
923
- sendImage: (imagePath) =>
924
- queueWechatAttachmentAction(() =>
925
- transport.sendImage(imagePath, { recipientId: authorizedUserId }),
926
- ),
927
- sendFile: (filePath) =>
928
- queueWechatAttachmentAction(() =>
929
- transport.sendFile(filePath, { recipientId: authorizedUserId }),
930
- ),
931
- sendVoice: (voicePath) =>
932
- queueWechatAttachmentAction(() =>
933
- transport.sendVoice(voicePath, authorizedUserId),
934
- ),
935
- sendVideo: (videoPath) =>
936
- queueWechatAttachmentAction(() =>
937
- transport.sendVideo(videoPath, { recipientId: authorizedUserId }),
938
- ),
939
- },
940
- });
941
- });
942
- break;
943
- case "status":
944
- if (event.message) {
945
- log(`${event.status}: ${event.message}`);
946
- stateStore.appendLog(`${event.status}: ${event.message}`);
947
- }
948
- void maybeDrainDeferredInboundMessages();
949
- break;
950
- case "notice":
951
- updateLastOutputAt();
952
- stateStore.appendLog(`${event.level}_notice: ${truncatePreview(event.text)}`);
953
- if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
954
- void outputBatcher.flushNow().then(async () => {
955
- await queueWechatMessage(authorizedUserId, event.text, "notice");
956
- });
957
- }
958
- break;
959
- case "approval_required":
960
- void outputBatcher.flushNow().then(async () => {
961
- const pending = toPendingApproval(event.request);
962
- stateStore.setPendingConfirmation(pending);
963
- stateStore.appendLog(
964
- `Approval requested (${pending.source}): ${pending.commandPreview}`,
965
- );
966
- await queueWechatMessage(
967
- authorizedUserId,
968
- formatApprovalMessage(pending, adapterState),
969
- "approval_required",
970
- );
971
- });
972
- break;
973
- case "mirrored_user_input":
974
- stateStore.appendLog(`mirrored_local_input: ${truncatePreview(event.text)}`);
975
- if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
976
- void outputBatcher.flushNow().then(async () => {
977
- await queueWechatMessage(
978
- authorizedUserId,
979
- formatMirroredUserInputMessage(options.adapter, event.text),
980
- "mirrored_user_input",
981
- );
982
- });
983
- }
984
- break;
985
- case "session_switched":
986
- stateStore.appendLog(
987
- `session_switched: ${event.sessionId} source=${event.source} reason=${event.reason}`,
988
- );
989
- if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
990
- void outputBatcher.flushNow().then(async () => {
991
- await queueWechatMessage(
992
- authorizedUserId,
993
- formatSessionSwitchMessage({
994
- adapter: options.adapter,
995
- sessionId: event.sessionId,
996
- source: event.source,
997
- reason: event.reason,
998
- }),
999
- "session_switched",
1000
- );
1001
- });
1002
- }
1003
- break;
1004
- case "thread_switched":
1005
- stateStore.appendLog(
1006
- `thread_switched: ${event.threadId} source=${event.source} reason=${event.reason}`,
1007
- );
1008
- if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
1009
- void outputBatcher.flushNow().then(async () => {
1010
- await queueWechatMessage(
1011
- authorizedUserId,
1012
- formatSessionSwitchMessage({
1013
- adapter: options.adapter,
1014
- sessionId: event.threadId,
1015
- source: event.source,
1016
- reason: event.reason,
1017
- }),
1018
- "thread_switched",
1019
- );
1020
- });
1021
- }
1022
- void maybeDrainDeferredInboundMessages();
1023
- break;
1024
- case "task_complete":
1025
- void outputBatcher.flushNow().then(async () => {
1026
- stateStore.clearPendingConfirmation();
1027
- if (options.adapter === "shell") {
1028
- const summary = buildCompletionSummary({
1029
- adapter: options.adapter,
1030
- activeTask: getActiveTask(),
1031
- exitCode: event.exitCode,
1032
- recentOutput: outputBatcher.getRecentSummary(),
1033
- });
1034
- await queueWechatMessage(authorizedUserId, summary);
1035
- }
1036
- clearActiveTask();
1037
- await maybeDrainDeferredInboundMessages();
1038
- });
1039
- break;
1040
- case "task_failed":
1041
- void outputBatcher.flushNow().then(async () => {
1042
- stateStore.clearPendingConfirmation();
1043
- clearActiveTask();
1044
- await queueWechatMessage(
1045
- authorizedUserId,
1046
- formatTaskFailedMessage(options.adapter, event.message),
1047
- "task_failed",
1048
- );
1049
- await maybeDrainDeferredInboundMessages();
1050
- });
1051
- break;
1052
- case "fatal_error":
1053
- logError(event.message);
1054
- stateStore.appendLog(`fatal_error: ${event.message}`);
1055
- stateStore.clearPendingConfirmation();
1056
- clearActiveTask();
1057
- void outputBatcher.flushNow().then(async () => {
1058
- await queueWechatMessage(
1059
- authorizedUserId,
1060
- formatUserFacingBridgeFatalError(event.message),
1061
- "fatal_error",
1062
- );
1063
- await maybeDrainDeferredInboundMessages();
1064
- });
1065
- break;
1066
- case "shutdown_requested":
1067
- stateStore.appendLog(`shutdown_requested: ${event.reason}`);
1068
- requestShutdown(event.message, event.exitCode ?? 0);
1069
- break;
1070
- }
1071
- });
1072
- }
1073
-
1074
- function buildCompletionSummary(params: {
1075
- adapter: BridgeAdapterKind;
1076
- activeTask: ActiveTask | null;
1077
- exitCode?: number;
1078
- recentOutput: string;
1079
- }): string {
1080
- const lines = [`${params.adapter} task complete.`];
1081
- if (params.activeTask) {
1082
- lines.push(
1083
- `duration: ${formatDuration(Date.now() - params.activeTask.startedAt)}`,
1084
- );
1085
- lines.push(`input: ${params.activeTask.inputPreview}`);
1086
- }
1087
- if (typeof params.exitCode === "number") {
1088
- lines.push(`exit_code: ${params.exitCode}`);
1089
- }
1090
- lines.push(`recent_output:\n${params.recentOutput}`);
1091
- return lines.join("\n");
1092
- }
1093
-
1094
- async function handleInboundMessage(params: {
1095
- message: InboundWechatMessage;
1096
- options: BridgeCliOptions;
1097
- stateStore: BridgeStateStore;
1098
- adapter: BridgeAdapter;
1099
- queueWechatMessage: (
1100
- senderId: string,
1101
- text: string,
1102
- context?: WechatSendContext,
1103
- ) => Promise<void>;
1104
- outputBatcher: OutputBatcher;
1105
- deferInboundMessage: (message: InboundWechatMessage) => Promise<void>;
1106
- }): Promise<ActiveTask | null> {
1107
- const {
1108
- message,
1109
- options,
1110
- stateStore,
1111
- adapter,
1112
- queueWechatMessage,
1113
- outputBatcher,
1114
- deferInboundMessage,
1115
- } = params;
1116
- const state = stateStore.getState();
1117
- const systemCommand = parseWechatControlCommand(message.text, {
1118
- adapter: options.adapter,
1119
- hasPendingConfirmation: Boolean(state.pendingConfirmation),
1120
- });
1121
-
1122
- if (message.senderId !== state.authorizedUserId) {
1123
- await queueWechatMessage(
1124
- message.senderId,
1125
- "Unauthorized. This bridge only accepts messages from the configured WeChat owner.",
1126
- );
1127
- return null;
1128
- }
1129
-
1130
- switch (systemCommand?.type) {
1131
- case "status":
1132
- await queueWechatMessage(
1133
- message.senderId,
1134
- formatStatusReport(stateStore.getState(), adapter.getState()),
1135
- );
1136
- return null;
1137
- case "resume": {
1138
- if (options.adapter === "codex") {
1139
- await queueWechatMessage(
1140
- message.senderId,
1141
- 'WeChat /resume is disabled in codex mode. Use /resume directly inside "wechat-codex"; WeChat will follow the active local thread.',
1142
- );
1143
- return null;
1144
- }
1145
- if (options.adapter === "claude") {
1146
- await queueWechatMessage(
1147
- message.senderId,
1148
- 'WeChat /resume is disabled in claude mode. Use /resume directly inside "wechat-claude"; WeChat will follow the active local session.',
1149
- );
1150
- return null;
1151
- }
1152
- if (options.adapter === "opencode") {
1153
- await queueWechatMessage(
1154
- message.senderId,
1155
- 'WeChat /resume is disabled in opencode mode. Use /resume directly inside "wechat-opencode"; WeChat will follow the active local session.',
1156
- );
1157
- return null;
1158
- }
1159
-
1160
- await queueWechatMessage(
1161
- message.senderId,
1162
- `/resume is not available in ${options.adapter} mode.`,
1163
- );
1164
- return null;
1165
- }
1166
- case "stop": {
1167
- const interrupted = await adapter.interrupt();
1168
- await queueWechatMessage(
1169
- message.senderId,
1170
- interrupted
1171
- ? "Interrupt signal sent to the active worker."
1172
- : "No running worker was available to interrupt.",
1173
- );
1174
- return null;
1175
- }
1176
- case "reset":
1177
- await outputBatcher.flushNow();
1178
- outputBatcher.clear();
1179
- stateStore.clearPendingConfirmation();
1180
- stateStore.clearSharedSessionId();
1181
- await adapter.reset();
1182
- stateStore.appendLog("Worker reset by owner.");
1183
- await queueWechatMessage(message.senderId, "Worker session has been reset.");
1184
- return null;
1185
- case "confirm": {
1186
- const pending = state.pendingConfirmation;
1187
- if (!pending) {
1188
- await queueWechatMessage(message.senderId, "No pending approval request.");
1189
- return null;
1190
- }
1191
- if (options.adapter !== "claude" && pending.code !== systemCommand.code) {
1192
- await queueWechatMessage(message.senderId, "Confirmation code did not match.");
1193
- return null;
1194
- }
1195
- const confirmed = await adapter.resolveApproval("confirm");
1196
- if (!confirmed) {
1197
- await queueWechatMessage(
1198
- message.senderId,
1199
- "The worker could not apply this approval request.",
1200
- );
1201
- return null;
1202
- }
1203
- stateStore.clearPendingConfirmation();
1204
- stateStore.appendLog(`Approval confirmed: ${pending.commandPreview}`);
1205
- await queueWechatMessage(message.senderId, "Approval confirmed. Continuing...");
1206
- return {
1207
- startedAt: Date.now(),
1208
- inputPreview: pending.commandPreview,
1209
- };
1210
- }
1211
- case "deny": {
1212
- const pending = state.pendingConfirmation;
1213
- if (!pending) {
1214
- await queueWechatMessage(message.senderId, "No pending approval request.");
1215
- return null;
1216
- }
1217
- const denied = await adapter.resolveApproval("deny");
1218
- if (!denied) {
1219
- await queueWechatMessage(
1220
- message.senderId,
1221
- "The worker could not deny this approval request cleanly.",
1222
- );
1223
- return null;
1224
- }
1225
- stateStore.clearPendingConfirmation();
1226
- stateStore.appendLog(`Approval denied: ${pending.commandPreview}`);
1227
- await queueWechatMessage(message.senderId, "Approval denied.");
1228
- return null;
1229
- }
1230
- }
1231
-
1232
- if (state.pendingConfirmation) {
1233
- await queueWechatMessage(
1234
- message.senderId,
1235
- formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()),
1236
- );
1237
- return null;
1238
- }
1239
-
1240
- const adapterState = adapter.getState();
1241
- if (
1242
- shouldDeferCodexInboundMessage({
1243
- adapter: options.adapter,
1244
- status: adapterState.status,
1245
- activeTurnOrigin: adapterState.activeTurnOrigin,
1246
- hasPendingConfirmation: Boolean(state.pendingConfirmation),
1247
- hasSystemCommand: Boolean(systemCommand),
1248
- })
1249
- ) {
1250
- await deferInboundMessage(message);
1251
- return null;
1252
- }
1253
-
1254
- if (adapterState.status === "busy") {
1255
- if (
1256
- (options.adapter === "codex" || options.adapter === "opencode") &&
1257
- adapterState.activeTurnOrigin === "local"
1258
- ) {
1259
- await queueWechatMessage(
1260
- message.senderId,
1261
- `${
1262
- options.adapter === "opencode" ? "OpenCode" : "codex"
1263
- } is currently busy with a local terminal turn. Wait for it to finish or use /stop.`,
1264
- );
1265
- return null;
1266
- }
1267
-
1268
- await queueWechatMessage(
1269
- message.senderId,
1270
- `${options.adapter} is still working. Wait for the current reply or use /stop.`,
1271
- );
1272
- return null;
1273
- }
1274
-
1275
- return dispatchInboundWechatText({
1276
- message,
1277
- options,
1278
- stateStore,
1279
- adapter,
1280
- });
1281
- }
1282
-
1283
- async function dispatchInboundWechatText(params: {
1284
- message: InboundWechatMessage;
1285
- options: BridgeCliOptions;
1286
- stateStore: BridgeStateStore;
1287
- adapter: BridgeAdapter;
1288
- }): Promise<ActiveTask> {
1289
- const { message, options, stateStore, adapter } = params;
1290
- const activeTask = {
1291
- startedAt: Date.now(),
1292
- inputPreview: truncatePreview(message.text, 180),
1293
- };
1294
- stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(message.text)}`);
1295
- await adapter.sendInput(buildWechatInboundPrompt(message.text));
1296
- return activeTask;
1297
- }
1298
-
1299
- const isDirectRun = Boolean((import.meta as ImportMeta & { main?: boolean }).main);
1300
- if (isDirectRun) {
1301
- main().catch((err) => {
1302
- logError(describeWechatTransportError(err));
1303
- process.exit(1);
1304
- });
1305
- }