cli-wechat-bridge 1.1.6 → 1.1.8

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/LICENSE.txt +661 -661
  2. package/README.md +454 -475
  3. package/bin/_run-entry.mjs +23 -5
  4. package/bin/wecom-check-update.mjs +5 -0
  5. package/bin/wecom-claude.mjs +5 -0
  6. package/bin/wecom-codex.mjs +5 -0
  7. package/bin/wecom-daemon.mjs +5 -0
  8. package/bin/wecom-opencode.mjs +5 -0
  9. package/bin/wecom-pi.mjs +5 -0
  10. package/bin/wecom-setup.mjs +5 -0
  11. package/dist/bridge/adapter-control.js +58 -0
  12. package/dist/bridge/bridge-adapters.claude.js +155 -4
  13. package/dist/bridge/bridge-adapters.codex.js +112 -7
  14. package/dist/bridge/bridge-adapters.core.js +19 -1
  15. package/dist/bridge/bridge-adapters.opencode.js +150 -1
  16. package/dist/bridge/bridge-adapters.pi.js +39 -0
  17. package/dist/bridge/bridge-process-reaper.js +35 -2
  18. package/dist/bridge/bridge-state.js +10 -3
  19. package/dist/bridge/claude-native-controls.js +133 -0
  20. package/dist/bridge/native-terminal-control.js +253 -0
  21. package/dist/bridge/wechat-bridge.js +389 -87
  22. package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
  23. package/dist/channels/wecom/bridge-final-reply.js +26 -0
  24. package/dist/channels/wecom/setup-cli.js +6 -0
  25. package/dist/channels/wecom/setup.js +176 -0
  26. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  27. package/dist/channels/wecom/wecom-config.js +66 -0
  28. package/dist/channels/wecom/wecom-media.js +141 -0
  29. package/dist/channels/wecom/wecom-message.js +149 -0
  30. package/dist/channels/wecom/wecom-transport.js +308 -0
  31. package/dist/companion/codex-remote-client.js +24 -3
  32. package/dist/companion/codex-visible-client-link.js +101 -0
  33. package/dist/companion/local-companion-start.js +56 -11
  34. package/dist/companion/local-companion.js +19 -1
  35. package/dist/companion/pi-tui-bridge-extension.js +56 -3
  36. package/dist/core/bridge-control.js +8 -0
  37. package/dist/core/channel-types.js +3 -1
  38. package/dist/core/conversation-routing.js +48 -0
  39. package/dist/core/direct-run.js +17 -0
  40. package/dist/core/inbound-message-claims.js +61 -0
  41. package/dist/daemon/daemon-link.js +3 -0
  42. package/dist/daemon/wechat-daemon.js +335 -60
  43. package/dist/i18n/messages-en.js +3 -1
  44. package/dist/i18n/messages-zh.js +3 -1
  45. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  46. package/dist/runtime/runtime-types.js +3 -0
  47. package/dist/utils/doctor.js +30 -10
  48. package/dist/utils/version-checker.js +11 -11
  49. package/dist/wechat/setup.js +113 -13
  50. package/dist/wechat/wechat-transport.js +96 -58
  51. package/package.json +116 -104
  52. package/scripts/ensure-node-pty-permissions.mjs +73 -73
@@ -17,12 +17,19 @@ import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataD
17
17
  import { ensureWechatCredentials } from "../wechat/setup.js";
18
18
  import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
19
19
  import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
20
- import { getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
20
+ import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
21
21
  import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
22
22
  import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
23
23
  import { routeBridgeMessage } from "../core/bridge-message-router.js";
24
+ import { clearTurn, InboundConversationContext, resolveConversationTarget, rollbackTurn, tryBeginTurn, } from "../core/conversation-routing.js";
25
+ import { handleAdapterControl, invalidateModelSnapshot } from "../bridge/adapter-control.js";
24
26
  import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
27
+ import { isDirectModuleRun } from "../core/direct-run.js";
25
28
  import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
29
+ import { ensureWecomAccount } from "../channels/wecom/setup.js";
30
+ import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
31
+ import { WecomTransport } from "../channels/wecom/wecom-transport.js";
32
+ import { buildWecomInboundPrompt, formatWecomVisibleText, } from "../channels/wecom/wecom-message.js";
26
33
  import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
27
34
  import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
28
35
  const MODULE_FILE = fileURLToPath(import.meta.url);
@@ -40,11 +47,15 @@ const DAEMON_TAKEOVER_STOP_POLL_MS = 250;
40
47
  const VISIBLE_CLIENT_CONNECT_TIMEOUT_MS = 15_000;
41
48
  const VISIBLE_CLIENT_CONNECT_POLL_MS = 250;
42
49
  const DAEMON_ADAPTERS = ["codex", "claude", "opencode", "pi"];
50
+ function getCliChannelId(argv = process.argv.slice(2)) {
51
+ const index = argv.indexOf("--channel");
52
+ return index >= 0 && argv[index + 1] === "wecom" ? "wecom" : "wechat";
53
+ }
43
54
  function log(message) {
44
- process.stderr.write(`[wechat-daemon] ${message}\n`);
55
+ process.stderr.write(`[${getCliChannelId()}-daemon] ${message}\n`);
45
56
  }
46
57
  function logError(message) {
47
- process.stderr.write(`[wechat-daemon] ERROR: ${message}\n`);
58
+ process.stderr.write(`[${getCliChannelId()}-daemon] ERROR: ${message}\n`);
48
59
  }
49
60
  function appendDaemonLog(message) {
50
61
  ensureChannelDataDir();
@@ -55,6 +66,48 @@ function computePollRetryDelayMs(consecutiveFailures) {
55
66
  const exponent = Math.min(normalizedFailures - 1, 5);
56
67
  return Math.min(POLL_RETRY_MAX_MS, POLL_RETRY_BASE_MS * 2 ** exponent);
57
68
  }
69
+ function toWecomOutputKind(context) {
70
+ switch (context) {
71
+ case "notice":
72
+ case "thinking":
73
+ case "final_reply":
74
+ case "approval_required":
75
+ case "user_input_required":
76
+ case "task_failed":
77
+ case "fatal_error":
78
+ return context;
79
+ case "mirrored_user_input":
80
+ return "mirrored_input";
81
+ default:
82
+ return "notice";
83
+ }
84
+ }
85
+ function toWechatSendContext(kind) {
86
+ if (kind === "mirrored_input") {
87
+ return "mirrored_user_input";
88
+ }
89
+ if (kind === "status") {
90
+ return "message";
91
+ }
92
+ return kind;
93
+ }
94
+ function toLegacyInboundWechatMessage(message) {
95
+ return {
96
+ senderId: message.senderId,
97
+ sender: message.senderId,
98
+ sessionId: message.conversation.conversationId,
99
+ text: message.text,
100
+ attachments: message.attachments.map((attachment) => ({
101
+ kind: attachment.kind === "image" ? "image" : "file",
102
+ path: attachment.path,
103
+ fileName: attachment.fileName || path.basename(attachment.path),
104
+ sizeBytes: attachment.sizeBytes ?? 0,
105
+ })),
106
+ contextToken: message.conversation.opaqueRef,
107
+ createdAt: message.createdAt,
108
+ createdAtMs: Date.parse(message.createdAt),
109
+ };
110
+ }
58
111
  function isDaemonAdapterKind(value) {
59
112
  return value === "codex" || value === "claude" || value === "opencode" || value === "pi";
60
113
  }
@@ -76,6 +129,7 @@ export function parseDaemonCliArgs(argv) {
76
129
  let profile;
77
130
  let initialAdapter;
78
131
  let openVisible = true;
132
+ let channelId = "wechat";
79
133
  for (let i = 0; i < argv.length; i += 1) {
80
134
  const arg = argv[i];
81
135
  if (!arg) {
@@ -84,10 +138,10 @@ export function parseDaemonCliArgs(argv) {
84
138
  const next = argv[i + 1];
85
139
  if (arg === "--help" || arg === "-h") {
86
140
  process.stdout.write([
87
- "Usage: wechat-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]",
141
+ `Usage: ${channelId}-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]`,
88
142
  "",
89
- "Keeps one WeChat connection alive and switches between Codex, Claude Code, OpenCode, and Pi from WeChat.",
90
- "Send /codex, /claude, /opencode, or /pi in WeChat to switch the active terminal.",
143
+ `Keeps one ${channelId === "wecom" ? "WeCom" : "WeChat"} connection alive and switches between Codex, Claude Code, OpenCode, and Pi.`,
144
+ `Send /codex, /claude, /opencode, or /pi in ${channelId === "wecom" ? "WeCom" : "WeChat"} to switch the active terminal.`,
91
145
  "",
92
146
  ].join("\n"));
93
147
  process.exit(0);
@@ -100,6 +154,14 @@ export function parseDaemonCliArgs(argv) {
100
154
  i += 1;
101
155
  continue;
102
156
  }
157
+ if (arg === "--channel") {
158
+ if (!next || (next !== "wechat" && next !== "wecom")) {
159
+ throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
160
+ }
161
+ channelId = next;
162
+ i += 1;
163
+ continue;
164
+ }
103
165
  if (arg === "--adapter") {
104
166
  if (!isDaemonAdapterKind(next)) {
105
167
  throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
@@ -122,7 +184,13 @@ export function parseDaemonCliArgs(argv) {
122
184
  }
123
185
  throw new Error(`Unknown argument: ${arg}`);
124
186
  }
125
- return { cwd, profile, initialAdapter, openVisible };
187
+ return {
188
+ cwd,
189
+ profile,
190
+ initialAdapter,
191
+ openVisible,
192
+ ...(channelId === "wecom" ? { channelId } : {}),
193
+ };
126
194
  }
127
195
  export function parseDaemonSwitchCommand(text) {
128
196
  const normalized = text.trim().toLowerCase();
@@ -151,8 +219,13 @@ export function parseDaemonSwitchDirective(text) {
151
219
  remainder: match[2]?.trim() ?? "",
152
220
  };
153
221
  }
154
- export function defaultDaemonSessionStartMode(adapter) {
155
- return adapter === "codex" ? "restore" : "new";
222
+ export function defaultDaemonSessionStartMode(_adapter) {
223
+ // Codex 0.155+ persists app-server threads lazily, so resuming a freshly
224
+ // prepared (not yet persisted) thread crashes the visible TUI bootstrap
225
+ // with "no rollout found". Every adapter therefore opens a fresh session
226
+ // by default; explicit --session-start-mode restore and WeChat /resume
227
+ // remain available for deliberate restores of persisted threads.
228
+ return "new";
156
229
  }
157
230
  export function resolveDaemonSessionStartMode(params) {
158
231
  // An explicit --session-start-mode wins over visible-client reuse: the user
@@ -165,9 +238,6 @@ export function resolveDaemonSessionStartMode(params) {
165
238
  if (params.reuseExistingVisible && params.visibleConnected) {
166
239
  return "restore";
167
240
  }
168
- if (params.adapter === "codex") {
169
- return "restore";
170
- }
171
241
  if (params.slotCreated) {
172
242
  return "new";
173
243
  }
@@ -221,7 +291,7 @@ export function buildVisibleClientLaunchArgs(params) {
221
291
  export function buildWindowsVisibleClientLaunchCommand(params) {
222
292
  return [
223
293
  "start",
224
- quoteWindowsCommandArg(`wechat-${params.adapter}`),
294
+ quoteWindowsCommandArg(`${params.channelId ?? "wechat"}-${params.adapter}`),
225
295
  "/D",
226
296
  quoteWindowsCommandArg(params.cwd),
227
297
  quoteWindowsCommandArg(process.execPath),
@@ -269,6 +339,7 @@ function openVisibleClient(params) {
269
339
  adapter: params.adapter,
270
340
  cwd: params.cwd,
271
341
  args,
342
+ channelId: params.channelId,
272
343
  }),
273
344
  ];
274
345
  const child = spawn(command, launchArgs, {
@@ -289,13 +360,13 @@ function openVisibleClient(params) {
289
360
  pid: child.pid,
290
361
  };
291
362
  }
292
- const title = `wechat-${params.adapter}`;
363
+ const title = `${params.channelId ?? "wechat"}-${params.adapter}`;
293
364
  const fullArgs = [process.execPath, ...args];
294
365
  if (process.platform === "darwin") {
295
366
  const cmdLine = fullArgs.map(shellQuotePosix).join(" ");
296
- const script = `tell application "Terminal"
297
- activate
298
- do script "cd ${shellQuotePosix(params.cwd)} && exec ${cmdLine}"
367
+ const script = `tell application "Terminal"
368
+ activate
369
+ do script "cd ${shellQuotePosix(params.cwd)} && exec ${cmdLine}"
299
370
  end tell`;
300
371
  const child = spawn("osascript", ["-e", script], {
301
372
  cwd: params.cwd,
@@ -486,6 +557,10 @@ class WechatDaemon {
486
557
  profile;
487
558
  authorizedUserId;
488
559
  transport;
560
+ channelId;
561
+ wecomTransport;
562
+ inboundConversationContext = new InboundConversationContext();
563
+ fallbackConversation;
489
564
  slots = new Map();
490
565
  // Per-adapter serialization chains for ensureSlot (see ensureSlot comment).
491
566
  slotEnsureChains = new Map();
@@ -506,7 +581,17 @@ class WechatDaemon {
506
581
  this.profile = params.profile;
507
582
  this.authorizedUserId = params.authorizedUserId;
508
583
  this.transport = params.transport;
509
- this.pendingWechatMessages = new PendingWechatMessageStore(getPendingWechatMessagesFile(this.cwd));
584
+ this.channelId = params.channelId ?? "wechat";
585
+ this.wecomTransport = params.wecomTransport ?? null;
586
+ this.fallbackConversation = {
587
+ channelId: this.channelId,
588
+ conversationId: params.authorizedUserId,
589
+ recipientId: params.authorizedUserId,
590
+ metadata: this.channelId === "wecom" ? { chatType: "direct" } : undefined,
591
+ };
592
+ this.pendingWechatMessages = new PendingWechatMessageStore(this.channelId === "wechat"
593
+ ? getPendingWechatMessagesFile(this.cwd)
594
+ : getPendingChannelMessagesFile(this.cwd, this.channelId));
510
595
  }
511
596
  async startIpcServer() {
512
597
  this.endpointToken = buildDaemonToken();
@@ -554,6 +639,7 @@ class WechatDaemon {
554
639
  token: this.endpointToken,
555
640
  cwd: this.cwd,
556
641
  startedAt: this.startedAt,
642
+ channelId: this.channelId,
557
643
  });
558
644
  resolve();
559
645
  });
@@ -562,6 +648,7 @@ class WechatDaemon {
562
648
  getStatus() {
563
649
  return {
564
650
  cwd: this.cwd,
651
+ channelId: this.channelId,
565
652
  activeAdapter: this.activeAdapter ?? undefined,
566
653
  startedAt: this.startedAt,
567
654
  slots: Array.from(this.slots.values()).map((slot) => {
@@ -590,10 +677,47 @@ class WechatDaemon {
590
677
  }
591
678
  async runPollLoop() {
592
679
  let consecutivePollFailures = 0;
593
- log("WeChat daemon is ready.");
680
+ if (this.wecomTransport) {
681
+ this.wecomTransport.setHandlers({
682
+ onMessage: async (channelMessage) => {
683
+ if (this.shutdownPromise) {
684
+ return;
685
+ }
686
+ await this.inboundConversationContext.run(channelMessage.conversation, async () => {
687
+ if (this.pendingWechatMessages.list().length > 0) {
688
+ await this.flushPendingWechatMessages();
689
+ }
690
+ await this.handleInboundMessage(toLegacyInboundWechatMessage(channelMessage));
691
+ });
692
+ },
693
+ onUnauthorized: async (senderId, chatType) => {
694
+ appendDaemonLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
695
+ if (chatType === "direct") {
696
+ await this.wecomTransport.sendText({
697
+ channelId: "wecom",
698
+ conversationId: senderId,
699
+ recipientId: senderId,
700
+ metadata: { chatType: "direct" },
701
+ }, "Unauthorized.", "notice");
702
+ }
703
+ },
704
+ onFatal: async (error) => {
705
+ appendDaemonLog(`wecom_fatal_error: ${error.message}`);
706
+ await this.shutdown();
707
+ },
708
+ onConnected: async () => {
709
+ if (this.pendingWechatMessages.list().length > 0) {
710
+ await this.flushPendingWechatMessages();
711
+ }
712
+ },
713
+ });
714
+ this.wecomTransport.start();
715
+ await this.wecomTransport.waitUntilConnected();
716
+ }
717
+ log(`${this.channelId === "wecom" ? "WeCom" : "WeChat"} daemon is ready.`);
594
718
  log(`Working directory: ${this.cwd}`);
595
- log("Switch from WeChat with /codex, /claude, /opencode, or /pi.");
596
- appendDaemonLog(`started: cwd=${this.cwd}`);
719
+ log(`Switch from ${this.channelId === "wecom" ? "WeCom" : "WeChat"} with /codex, /claude, /opencode, or /pi.`);
720
+ appendDaemonLog(`started: channel=${this.channelId} cwd=${this.cwd}`);
597
721
  const activeSlot = this.getActiveSlot();
598
722
  const welcomeText = t("daemon.welcome", {
599
723
  cwd: this.cwd,
@@ -601,6 +725,13 @@ class WechatDaemon {
601
725
  bindings: formatBindingsListMessage(listBindings()),
602
726
  });
603
727
  await this.queueWechatMessage(this.authorizedUserId, welcomeText);
728
+ if (this.channelId === "wecom") {
729
+ while (!this.shutdownPromise) {
730
+ await delay(1_000);
731
+ }
732
+ await this.shutdownPromise;
733
+ return;
734
+ }
604
735
  while (!this.shutdownPromise) {
605
736
  let pollResult;
606
737
  try {
@@ -694,6 +825,23 @@ class WechatDaemon {
694
825
  }
695
826
  async cleanup() {
696
827
  appendDaemonLog("shutdown_started");
828
+ // Stop accepting new channel work and abort an in-flight long poll before
829
+ // waiting for queued sends or adapter disposal. Otherwise Ctrl+C can leave
830
+ // the process alive until the 35-second poll timeout and retain the daemon
831
+ // endpoint for the next startup to clean up.
832
+ try {
833
+ this.transport.stop();
834
+ }
835
+ catch {
836
+ // Best effort shutdown.
837
+ }
838
+ try {
839
+ this.wecomTransport?.stop();
840
+ }
841
+ catch {
842
+ // Best effort shutdown.
843
+ }
844
+ clearDaemonEndpoint();
697
845
  for (const slot of this.slots.values()) {
698
846
  try {
699
847
  await slot.outputBatcher.flushNow();
@@ -736,7 +884,7 @@ class WechatDaemon {
736
884
  return { shuttingDown: true };
737
885
  case "ensure_slot":
738
886
  if (!isSameWorkspaceCwd(request.cwd, this.cwd)) {
739
- throw new Error(`wechat-daemon is bound to ${this.cwd}; requested cwd was ${request.cwd}.`);
887
+ throw new Error(`${this.channelId}-daemon is bound to ${this.cwd}; requested cwd was ${request.cwd}.`);
740
888
  }
741
889
  return await this.ensureSlot(request.adapter, {
742
890
  profile: request.profile,
@@ -826,8 +974,12 @@ class WechatDaemon {
826
974
  const launch = openVisibleClient({
827
975
  adapter,
828
976
  cwd: this.cwd,
977
+ // A freshly prepared blank thread is semantically a new session, but
978
+ // the visible client must resume its id from the endpoint instead of
979
+ // creating a second thread of its own.
829
980
  sessionStartMode,
830
981
  cliArgs: options.cliArgs,
982
+ channelId: this.channelId,
831
983
  onError: (error) => {
832
984
  appendDaemonLog(`visible_client_open_error: adapter=${adapter} error=${truncatePreview(error.message, 400)}`);
833
985
  },
@@ -868,6 +1020,9 @@ class WechatDaemon {
868
1020
  }
869
1021
  const activated = options.openVisible === false || (visibleConnected && visibleReady);
870
1022
  if (activated) {
1023
+ const previousSlot = this.getActiveSlot();
1024
+ if (previousSlot && this.activeAdapter !== adapter)
1025
+ invalidateModelSnapshot(previousSlot.runtime);
871
1026
  this.activeAdapter = adapter;
872
1027
  }
873
1028
  appendDaemonLog(`switch_adapter: adapter=${adapter} created=${created} opened_visible=${openedVisible} visible_connected=${visibleConnected} visible_ready=${visibleReady} activated=${activated} previous_active=${previousActiveAdapter ?? "(none)"} session_start_mode=${sessionStartMode}`);
@@ -898,7 +1053,7 @@ class WechatDaemon {
898
1053
  runtime,
899
1054
  controller,
900
1055
  outputBatcher: new OutputBatcher(async (text) => {
901
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(adapter, text));
1056
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(adapter, text), "message", this.resolveSlotOutputTarget(slot));
902
1057
  }),
903
1058
  pendingConfirmations: [],
904
1059
  pendingUserInput: null,
@@ -924,7 +1079,7 @@ class WechatDaemon {
924
1079
  slot.outputBatcher.clear();
925
1080
  slot.pendingConfirmations = [];
926
1081
  slot.pendingUserInput = null;
927
- slot.activeTask = null;
1082
+ clearTurn(slot);
928
1083
  if (slot.adapter === "codex" || slot.adapter === "claude") {
929
1084
  await slot.runtime.reset();
930
1085
  }
@@ -937,6 +1092,16 @@ class WechatDaemon {
937
1092
  slot.controller.syncLocalClientEndpoint();
938
1093
  }
939
1094
  createWechatChannelPort(adapter) {
1095
+ if (this.channelId === "wecom") {
1096
+ return new WecomChannelPort({
1097
+ transport: this.wecomTransport,
1098
+ sendText: (target, text, kind) => this.queueWechatMessage(this.authorizedUserId, text, toWechatSendContext(kind), target),
1099
+ prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
1100
+ onEmptyVisibleReply: (currentAdapter, rawText) => {
1101
+ appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
1102
+ },
1103
+ });
1104
+ }
940
1105
  return new WechatChannelPort({
941
1106
  sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
942
1107
  sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(filePath, { recipientId })),
@@ -956,6 +1121,10 @@ class WechatDaemon {
956
1121
  slot.controller.syncLocalClientEndpoint();
957
1122
  const adapterState = slot.runtime.getState();
958
1123
  const channelPort = this.createWechatChannelPort(slot.adapter);
1124
+ const eventTarget = this.channelId === "wecom"
1125
+ ? this.resolveSlotOutputTarget(slot)
1126
+ : this.fallbackConversation;
1127
+ const eventTask = slot.activeTask;
959
1128
  if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
960
1129
  slot.pendingConfirmations = [];
961
1130
  }
@@ -981,11 +1150,7 @@ class WechatDaemon {
981
1150
  appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
982
1151
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
983
1152
  await channelPort.send({
984
- target: {
985
- channelId: "wechat",
986
- conversationId: this.authorizedUserId,
987
- recipientId: this.authorizedUserId,
988
- },
1153
+ target: eventTarget,
989
1154
  kind: "final_reply",
990
1155
  text: next.text,
991
1156
  adapter: slot.adapter,
@@ -1003,7 +1168,7 @@ class WechatDaemon {
1003
1168
  appendDaemonLog(`${slot.adapter}_${next.level}_notice: ${truncatePreview(next.text)}`);
1004
1169
  if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
1005
1170
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1006
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice");
1171
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice", eventTarget);
1007
1172
  }));
1008
1173
  }
1009
1174
  },
@@ -1012,7 +1177,7 @@ class WechatDaemon {
1012
1177
  const pending = toPendingApproval(next);
1013
1178
  slot.pendingConfirmations.push(pending);
1014
1179
  appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
1015
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
1180
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required", eventTarget);
1016
1181
  }));
1017
1182
  },
1018
1183
  userInputRequired: (next) => {
@@ -1020,14 +1185,14 @@ class WechatDaemon {
1020
1185
  slot.pendingUserInput = pending;
1021
1186
  appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
1022
1187
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1023
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
1188
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required", eventTarget);
1024
1189
  }));
1025
1190
  },
1026
1191
  mirroredUserInput: (next) => {
1027
1192
  appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
1028
1193
  if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
1029
1194
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1030
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input");
1195
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input", eventTarget);
1031
1196
  }));
1032
1197
  }
1033
1198
  },
@@ -1037,7 +1202,7 @@ class WechatDaemon {
1037
1202
  appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${next.sessionId} source=${next.source} reason=${next.reason}`);
1038
1203
  if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1039
1204
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1040
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched");
1205
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched", eventTarget);
1041
1206
  }));
1042
1207
  }
1043
1208
  },
@@ -1055,7 +1220,7 @@ class WechatDaemon {
1055
1220
  }
1056
1221
  if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1057
1222
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1058
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched");
1223
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched", eventTarget);
1059
1224
  }));
1060
1225
  }
1061
1226
  },
@@ -1063,15 +1228,19 @@ class WechatDaemon {
1063
1228
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
1064
1229
  slot.pendingConfirmations = [];
1065
1230
  slot.pendingUserInput = null;
1066
- slot.activeTask = null;
1231
+ if (eventTask) {
1232
+ clearTurn(slot, eventTask);
1233
+ }
1067
1234
  }));
1068
1235
  },
1069
1236
  taskFailed: (next) => {
1070
1237
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1071
1238
  slot.pendingConfirmations = [];
1072
1239
  slot.pendingUserInput = null;
1073
- slot.activeTask = null;
1074
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed");
1240
+ if (eventTask) {
1241
+ clearTurn(slot, eventTask);
1242
+ }
1243
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed", eventTarget);
1075
1244
  }));
1076
1245
  },
1077
1246
  fatalError: (next) => {
@@ -1079,10 +1248,12 @@ class WechatDaemon {
1079
1248
  appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${next.message}`);
1080
1249
  slot.pendingConfirmations = [];
1081
1250
  slot.pendingUserInput = null;
1082
- slot.activeTask = null;
1251
+ if (eventTask) {
1252
+ clearTurn(slot, eventTask);
1253
+ }
1083
1254
  this.disposeDeadSlot(slot);
1084
1255
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1085
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
1256
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error", eventTarget);
1086
1257
  }));
1087
1258
  },
1088
1259
  shutdownRequested: (next) => {
@@ -1094,9 +1265,25 @@ class WechatDaemon {
1094
1265
  appendDaemonLog(`event_forward_failed: adapter=${slot.adapter} message=${message}`);
1095
1266
  });
1096
1267
  }
1268
+ resolveSlotOutputTarget(slot) {
1269
+ return resolveConversationTarget({
1270
+ active: slot.activeConversation,
1271
+ last: slot.lastConversation,
1272
+ fallback: this.fallbackConversation,
1273
+ });
1274
+ }
1275
+ bindCurrentWecomConversation(slot) {
1276
+ const conversation = this.inboundConversationContext.get();
1277
+ if (this.channelId !== "wecom" || !slot || !conversation) {
1278
+ return;
1279
+ }
1280
+ slot.lastConversation = conversation;
1281
+ }
1097
1282
  async handleInboundMessage(message) {
1098
1283
  if (message.senderId !== this.authorizedUserId) {
1099
- await this.queueWechatMessage(message.senderId, "Unauthorized. This daemon only accepts messages from the configured WeChat owner.");
1284
+ await this.queueWechatMessage(message.senderId, this.channelId === "wecom"
1285
+ ? "Unauthorized. This daemon only accepts messages from the paired WeCom operator."
1286
+ : "Unauthorized. This daemon only accepts messages from the configured WeChat owner.");
1100
1287
  return;
1101
1288
  }
1102
1289
  const emojiMatch = resolveEmojiCommand(message.text);
@@ -1108,6 +1295,7 @@ class WechatDaemon {
1108
1295
  reuseExistingVisible: true,
1109
1296
  });
1110
1297
  if (result.activated) {
1298
+ this.bindCurrentWecomConversation(this.getActiveSlot());
1111
1299
  message = { ...message, text: emojiMatch.remainder };
1112
1300
  }
1113
1301
  else {
@@ -1134,6 +1322,7 @@ class WechatDaemon {
1134
1322
  await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchDirective.adapter}.\n${detail}`);
1135
1323
  return;
1136
1324
  }
1325
+ this.bindCurrentWecomConversation(this.getActiveSlot());
1137
1326
  if (switchDirective.remainder) {
1138
1327
  message = { ...message, text: switchDirective.remainder };
1139
1328
  }
@@ -1143,7 +1332,7 @@ class WechatDaemon {
1143
1332
  }
1144
1333
  }
1145
1334
  if (message.text.trim().toLowerCase() === "/daemon-stop") {
1146
- await this.queueWechatMessage(message.senderId, "Stopping wechat-daemon...");
1335
+ await this.queueWechatMessage(message.senderId, `Stopping ${this.channelId}-daemon...`);
1147
1336
  setTimeout(() => {
1148
1337
  void this.shutdown().finally(() => process.exit(0));
1149
1338
  }, 0);
@@ -1163,7 +1352,9 @@ class WechatDaemon {
1163
1352
  await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
1164
1353
  return;
1165
1354
  }
1355
+ this.bindCurrentWecomConversation(slot);
1166
1356
  const routeCurrentMessage = async (currentSlot, command) => {
1357
+ const previousTask = currentSlot.activeTask;
1167
1358
  await routeBridgeMessage({
1168
1359
  message: toChannelInboundMessage(message),
1169
1360
  authorized: true,
@@ -1192,6 +1383,14 @@ class WechatDaemon {
1192
1383
  await this.dispatchInboundWechatText(message, currentSlot);
1193
1384
  },
1194
1385
  });
1386
+ if (this.channelId === "wecom" &&
1387
+ this.inboundConversationContext.get() &&
1388
+ currentSlot.activeTask &&
1389
+ currentSlot.activeTask !== previousTask) {
1390
+ const conversation = this.inboundConversationContext.get();
1391
+ currentSlot.activeConversation = conversation;
1392
+ currentSlot.lastConversation = conversation;
1393
+ }
1195
1394
  };
1196
1395
  const command = parseWechatControlCommand(message.text, {
1197
1396
  adapter: slot.adapter,
@@ -1215,6 +1414,7 @@ class WechatDaemon {
1215
1414
  return;
1216
1415
  }
1217
1416
  slot = this.getActiveSlot() ?? slot;
1417
+ this.bindCurrentWecomConversation(slot);
1218
1418
  await routeCurrentMessage(slot, null);
1219
1419
  }
1220
1420
  async handleEmojiBindingsCommand(senderId, cmd) {
@@ -1239,12 +1439,16 @@ class WechatDaemon {
1239
1439
  }
1240
1440
  async handleSystemCommand(message, activeSlot, command) {
1241
1441
  switch (command.type) {
1442
+ case "model":
1443
+ case "plan":
1444
+ await this.queueWechatMessage(message.senderId, await handleAdapterControl(activeSlot.runtime, message.senderId, command));
1445
+ return;
1242
1446
  case "status":
1243
1447
  await this.queueWechatMessage(message.senderId, formatDaemonStatus(this.getStatus()));
1244
1448
  return;
1245
1449
  case "resume": {
1246
1450
  if (!isWechatResumeEnabled(activeSlot.adapter)) {
1247
- await this.queueWechatMessage(message.senderId, `WeChat /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; WeChat will follow that local session.`);
1451
+ await this.queueWechatMessage(message.senderId, `${this.channelId === "wecom" ? "WeCom" : "WeChat"} /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; the remote channel will follow that local session.`);
1248
1452
  return;
1249
1453
  }
1250
1454
  try {
@@ -1262,9 +1466,10 @@ class WechatDaemon {
1262
1466
  if (command.target) {
1263
1467
  await resumeSlot.outputBatcher.flushNow();
1264
1468
  }
1469
+ const taskBeforeResume = resumeSlot.activeTask;
1265
1470
  const result = await resumeSlot.resumeCoordinator.execute(command.target);
1266
- if (result.kind === "resumed") {
1267
- resumeSlot.activeTask = null;
1471
+ if (result.kind === "resumed" && taskBeforeResume) {
1472
+ clearTurn(resumeSlot, taskBeforeResume);
1268
1473
  }
1269
1474
  await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(resumeSlot.adapter, result.message));
1270
1475
  }
@@ -1403,12 +1608,28 @@ class WechatDaemon {
1403
1608
  }
1404
1609
  async dispatchInboundWechatText(message, slot) {
1405
1610
  const preview = formatInboundMessagePreview(message);
1406
- slot.activeTask = {
1611
+ const nextTask = {
1407
1612
  startedAt: Date.now(),
1408
1613
  inputPreview: truncatePreview(preview, 180),
1409
1614
  };
1615
+ const inboundConversation = this.channelId === "wecom"
1616
+ ? this.inboundConversationContext.get()
1617
+ : undefined;
1618
+ const lease = tryBeginTurn(slot, nextTask, inboundConversation);
1619
+ if (!lease) {
1620
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
1621
+ return;
1622
+ }
1410
1623
  appendDaemonLog(`forwarded_input: adapter=${slot.adapter} text=${truncatePreview(preview)}`);
1411
- await slot.runtime.sendInput(buildWechatInboundPrompt(message.text, message.attachments));
1624
+ try {
1625
+ await slot.runtime.sendInput(this.channelId === "wecom"
1626
+ ? buildWecomInboundPrompt(message.text, message.attachments)
1627
+ : buildWechatInboundPrompt(message.text, message.attachments));
1628
+ }
1629
+ catch (error) {
1630
+ rollbackTurn(slot, lease);
1631
+ throw error;
1632
+ }
1412
1633
  }
1413
1634
  queueWechatTextAction(action) {
1414
1635
  const run = this.textSendChain.then(action);
@@ -1420,7 +1641,33 @@ class WechatDaemon {
1420
1641
  this.attachmentSendChain = run.then(() => undefined, () => undefined);
1421
1642
  return run;
1422
1643
  }
1423
- async sendWechatMessageNow(senderId, text, context = "message") {
1644
+ async sendWechatMessageNow(senderId, text, context = "message", targetOverride) {
1645
+ if (this.channelId === "wecom") {
1646
+ const activeSlot = this.getActiveSlot();
1647
+ const target = targetOverride ??
1648
+ (senderId === this.authorizedUserId
1649
+ ? this.inboundConversationContext.get() ??
1650
+ (activeSlot ? this.resolveSlotOutputTarget(activeSlot) : this.fallbackConversation)
1651
+ : {
1652
+ channelId: "wecom",
1653
+ conversationId: senderId,
1654
+ recipientId: senderId,
1655
+ metadata: { chatType: "direct" },
1656
+ });
1657
+ const startedAtMs = Date.now();
1658
+ appendDaemonLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
1659
+ try {
1660
+ await this.wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
1661
+ appendDaemonLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
1662
+ return { status: "sent" };
1663
+ }
1664
+ catch (error) {
1665
+ const message = error instanceof Error ? error.message : String(error);
1666
+ appendDaemonLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
1667
+ logError(`Failed to send WeCom ${context}: ${message}`);
1668
+ return { status: "context_unavailable", error, target };
1669
+ }
1670
+ }
1424
1671
  const startedAtMs = Date.now();
1425
1672
  appendDaemonLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
1426
1673
  for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
@@ -1467,11 +1714,17 @@ class WechatDaemon {
1467
1714
  }
1468
1715
  return { status: "failed" };
1469
1716
  }
1470
- queueWechatMessage(senderId, text, context = "message") {
1717
+ queueWechatMessage(senderId, text, context = "message", targetOverride) {
1718
+ const activeSlot = this.getActiveSlot();
1719
+ const queuedTarget = this.channelId === "wecom" && senderId === this.authorizedUserId
1720
+ ? targetOverride ??
1721
+ this.inboundConversationContext.get() ??
1722
+ (activeSlot ? this.resolveSlotOutputTarget(activeSlot) : this.fallbackConversation)
1723
+ : targetOverride;
1471
1724
  return this.queueWechatTextAction(async () => {
1472
- const result = await this.sendWechatMessageNow(senderId, text, context);
1725
+ const result = await this.sendWechatMessageNow(senderId, text, context, queuedTarget);
1473
1726
  if (result.status === "context_unavailable") {
1474
- const pending = this.pendingWechatMessages.enqueue(senderId, text, context);
1727
+ const pending = this.pendingWechatMessages.enqueue(senderId, text, context, result.target);
1475
1728
  if (pending) {
1476
1729
  appendDaemonLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${this.pendingWechatMessages.list().length}`);
1477
1730
  }
@@ -1482,7 +1735,7 @@ class WechatDaemon {
1482
1735
  flushPendingWechatMessages() {
1483
1736
  return this.queueWechatTextAction(async () => {
1484
1737
  for (const pending of this.pendingWechatMessages.list()) {
1485
- const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
1738
+ const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
1486
1739
  if (result.status === "sent") {
1487
1740
  this.pendingWechatMessages.remove(pending.id);
1488
1741
  appendDaemonLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
@@ -1793,7 +2046,9 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
1793
2046
  return { action: "stopped", lock, forced };
1794
2047
  }
1795
2048
  export async function runDaemon(options) {
1796
- migrateLegacyChannelFiles((message) => log(message));
2049
+ if ((options.channelId ?? "wechat") === "wechat") {
2050
+ migrateLegacyChannelFiles((message) => log(message));
2051
+ }
1797
2052
  loadEmojiBindings();
1798
2053
  await cleanupDaemonBeforeStart({ cwd: options.cwd });
1799
2054
  const cleanupResult = await cleanupSingleBridgeBeforeDaemon();
@@ -1809,19 +2064,36 @@ export async function runDaemon(options) {
1809
2064
  if (reapedOpencodePids.length > 0) {
1810
2065
  log(`Cleaned ${reapedOpencodePids.length} orphaned OpenCode process(es): ${reapedOpencodePids.join(", ")}`);
1811
2066
  }
1812
- const credentials = await ensureWechatCredentials({
1813
- requireUserId: true,
1814
- validateExisting: true,
1815
- log,
1816
- });
2067
+ const wecomAccount = options.channelId === "wecom"
2068
+ ? await ensureWecomAccount({ log })
2069
+ : null;
2070
+ const credentials = wecomAccount
2071
+ ? { userId: wecomAccount.operatorUserId }
2072
+ : await ensureWechatCredentials({
2073
+ requireUserId: true,
2074
+ validateExisting: true,
2075
+ log,
2076
+ });
1817
2077
  if (!credentials.userId) {
1818
- throw new Error("Saved WeChat credentials are missing userId.");
2078
+ throw new Error(options.channelId === "wecom"
2079
+ ? "Saved WeCom credentials are missing operatorUserId."
2080
+ : "Saved WeChat credentials are missing userId.");
1819
2081
  }
1820
2082
  const daemon = new WechatDaemon({
1821
2083
  cwd: options.cwd,
1822
2084
  profile: options.profile,
1823
2085
  authorizedUserId: credentials.userId,
1824
2086
  transport: new WeChatTransport({ log, logError }),
2087
+ channelId: options.channelId,
2088
+ wecomTransport: wecomAccount
2089
+ ? new WecomTransport({
2090
+ account: wecomAccount,
2091
+ logger: {
2092
+ log: (message) => log(message),
2093
+ error: (message) => logError(message),
2094
+ },
2095
+ })
2096
+ : null,
1825
2097
  });
1826
2098
  if (cleanupResult.action === "stopped" && isDaemonAdapterKind(cleanupResult.lock.adapter)) {
1827
2099
  daemon.takenOverAdapter = cleanupResult.lock.adapter;
@@ -1837,6 +2109,9 @@ export async function runDaemon(options) {
1837
2109
  }
1838
2110
  shutdownInProgress = true;
1839
2111
  log(`Received ${signal}. Stopping daemon.`);
2112
+ // Remove this process's endpoint synchronously so a new terminal can be
2113
+ // started immediately even if asynchronous adapter cleanup takes time.
2114
+ clearDaemonEndpoint();
1840
2115
  void daemon.shutdown().finally(() => process.exit(0));
1841
2116
  };
1842
2117
  process.on("SIGINT", () => handleSignal("SIGINT"));
@@ -1871,7 +2146,7 @@ export async function main(argv = process.argv.slice(2)) {
1871
2146
  process.exit(1);
1872
2147
  }
1873
2148
  }
1874
- const isDirectRun = Boolean(import.meta.main);
2149
+ const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
1875
2150
  if (isDirectRun) {
1876
2151
  void main();
1877
2152
  }