@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.0

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 (85) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +467 -475
  3. package/bin/_run-entry.mjs +29 -6
  4. package/bin/wechat-check-update.mjs +6 -1
  5. package/bin/wechat-claude.mjs +6 -1
  6. package/bin/wechat-codex.mjs +6 -1
  7. package/bin/wechat-daemon.mjs +6 -1
  8. package/bin/wechat-opencode.mjs +6 -1
  9. package/bin/wechat-pi.mjs +6 -1
  10. package/bin/wechat-setup.mjs +6 -1
  11. package/bin/wecom-check-update.mjs +10 -0
  12. package/bin/wecom-claude.mjs +10 -0
  13. package/bin/wecom-codex.mjs +10 -0
  14. package/bin/wecom-daemon.mjs +10 -0
  15. package/bin/wecom-opencode.mjs +10 -0
  16. package/bin/wecom-pi.mjs +10 -0
  17. package/bin/wecom-setup.mjs +10 -0
  18. package/dist/bridge/adapter-control.js +58 -0
  19. package/dist/bridge/bridge-adapters.claude.js +161 -4
  20. package/dist/bridge/bridge-adapters.codex.js +118 -7
  21. package/dist/bridge/bridge-adapters.core.js +25 -1
  22. package/dist/bridge/bridge-adapters.opencode.js +156 -1
  23. package/dist/bridge/bridge-adapters.pi.js +45 -0
  24. package/dist/bridge/bridge-adapters.shared.js +6 -0
  25. package/dist/bridge/bridge-controller.js +6 -0
  26. package/dist/bridge/bridge-final-reply.js +6 -0
  27. package/dist/bridge/bridge-process-reaper.js +41 -2
  28. package/dist/bridge/bridge-state.js +16 -3
  29. package/dist/bridge/bridge-utils.js +6 -31
  30. package/dist/bridge/claude-hook.js +6 -0
  31. package/dist/bridge/claude-native-controls.js +133 -0
  32. package/dist/bridge/native-terminal-control.js +259 -0
  33. package/dist/bridge/wechat-bridge.js +380 -171
  34. package/dist/bridge/wechat-forwarding.js +6 -0
  35. package/dist/bridge/wechat-outbound-queue.js +6 -0
  36. package/dist/channels/wechat/bridge-final-reply.js +13 -22
  37. package/dist/channels/wechat/index.js +6 -0
  38. package/dist/channels/wechat/wechat-driver.js +191 -0
  39. package/dist/channels/wechat/wechat-forwarding.js +18 -0
  40. package/dist/channels/wechat/wechat-outbound-queue.js +49 -3
  41. package/dist/channels/wecom/bridge-final-reply.js +32 -0
  42. package/dist/channels/wecom/setup-cli.js +12 -0
  43. package/dist/channels/wecom/setup.js +182 -0
  44. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  45. package/dist/channels/wecom/wecom-config.js +72 -0
  46. package/dist/channels/wecom/wecom-driver.js +106 -0
  47. package/dist/channels/wecom/wecom-media.js +147 -0
  48. package/dist/channels/wecom/wecom-message.js +149 -0
  49. package/dist/channels/wecom/wecom-transport.js +314 -0
  50. package/dist/commands/check-update.js +6 -0
  51. package/dist/companion/codex-remote-client.js +30 -3
  52. package/dist/companion/codex-visible-client-link.js +107 -0
  53. package/dist/companion/local-companion-link.js +6 -0
  54. package/dist/companion/local-companion-start.js +62 -11
  55. package/dist/companion/local-companion.js +25 -1
  56. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  57. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  58. package/dist/core/bridge-control.js +10 -0
  59. package/dist/core/bridge-defer.js +25 -0
  60. package/dist/core/channel-driver.js +1 -0
  61. package/dist/core/channel-types.js +3 -1
  62. package/dist/core/conversation-routing.js +58 -0
  63. package/dist/core/direct-run.js +23 -0
  64. package/dist/core/inbound-message-claims.js +67 -0
  65. package/dist/core/outbound-target.js +32 -0
  66. package/dist/core/text-utils.js +6 -0
  67. package/dist/core/turn-coordinator.js +85 -0
  68. package/dist/daemon/daemon-link.js +9 -0
  69. package/dist/daemon/emoji-bindings.js +6 -0
  70. package/dist/daemon/wechat-daemon.js +675 -122
  71. package/dist/i18n/index.js +6 -0
  72. package/dist/i18n/messages-en.js +9 -1
  73. package/dist/i18n/messages-zh.js +9 -1
  74. package/dist/runtime/create-runtime-host.js +6 -0
  75. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  76. package/dist/runtime/runtime-types.js +3 -0
  77. package/dist/utils/atomic-file.js +6 -0
  78. package/dist/utils/doctor.js +36 -10
  79. package/dist/utils/version-checker.js +17 -11
  80. package/dist/wechat/channel-config.js +6 -0
  81. package/dist/wechat/setup.js +119 -13
  82. package/dist/wechat/wechat-channel.js +6 -0
  83. package/dist/wechat/wechat-transport.js +169 -58
  84. package/package.json +13 -3
  85. package/scripts/ensure-node-pty-permissions.mjs +73 -73
@@ -1,10 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
4
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
5
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
6
+ // must be released under AGPL-3.0-or-later with full source code; see
7
+ // LICENSE.txt. Network services built on it must offer source to users.
2
8
  import net from "node:net";
3
9
  import path from "node:path";
4
10
  import { spawn } from "node:child_process";
5
11
  import { fileURLToPath } from "node:url";
6
12
  import { BRIDGE_LOG_FILE } from "../wechat/channel-config.js";
7
13
  import { ensureWechatCredentials } from "../wechat/setup.js";
14
+ import { ensureWecomAccount } from "../channels/wecom/setup.js";
15
+ import { isDirectModuleRun } from "../core/direct-run.js";
8
16
  import { readBridgeLockFile, shouldAutoReclaimBridgeLock, } from "../bridge/bridge-state.js";
9
17
  import { clearLocalCompanionOccupancy, clearLocalCompanionEndpoint, readLocalCompanionEndpoint, } from "./local-companion-link.js";
10
18
  import { runCodexRemoteClient } from "./codex-remote-client.js";
@@ -15,8 +23,8 @@ const MODULE_DIR = path.dirname(MODULE_FILE);
15
23
  const RUNTIME_ENTRY_EXTENSION = path.extname(MODULE_FILE) === ".ts" ? ".ts" : ".js";
16
24
  const DEFAULT_WAIT_TIMEOUT_MS = 15_000;
17
25
  const DEFAULT_ADAPTER = "codex";
18
- function log(adapter, message) {
19
- process.stderr.write(`[wechat-${adapter}] ${message}\n`);
26
+ function log(adapter, message, channelId = "wechat") {
27
+ process.stderr.write(`[${channelId}-${adapter}] ${message}\n`);
20
28
  }
21
29
  export function normalizeComparablePath(cwd) {
22
30
  const normalized = path.resolve(cwd);
@@ -37,8 +45,13 @@ export function formatSwitchFailureMessage(cwd) {
37
45
  export function formatRestartUnhealthyMessage(cwd) {
38
46
  return `Detected unhealthy companion state for ${cwd}. Restarting bridge...`;
39
47
  }
40
- export function defaultSessionStartMode(adapter) {
41
- return adapter === "codex" ? "restore" : "new";
48
+ export function defaultSessionStartMode(_adapter) {
49
+ // Codex 0.155+ persists app-server threads lazily, so launching the visible
50
+ // client with a freshly prepared (not yet persisted) thread id crashes its
51
+ // bootstrap with "no rollout found". Start every adapter fresh by default;
52
+ // explicit --session-start-mode restore remains available for deliberate
53
+ // restores of already persisted threads.
54
+ return "new";
42
55
  }
43
56
  export function decideLaunchAction(input) {
44
57
  if (input.lockShouldAutoReclaim) {
@@ -48,6 +61,8 @@ export function decideLaunchAction(input) {
48
61
  };
49
62
  }
50
63
  const sameWorkspace = input.runningLock.adapter === input.requestedAdapter &&
64
+ (input.runningLock.channelId ?? "wechat") ===
65
+ (input.requestedChannelId ?? "wechat") &&
51
66
  isSameWorkspaceCwd(input.runningLock.cwd, input.requestedCwd);
52
67
  if (!sameWorkspace) {
53
68
  return {
@@ -99,6 +114,7 @@ export function parseCliArgs(argv) {
99
114
  let profile;
100
115
  let timeoutMs = DEFAULT_WAIT_TIMEOUT_MS;
101
116
  let sessionStartMode;
117
+ let channelId = "wechat";
102
118
  const cliArgs = [];
103
119
  for (let i = 0; i < argv.length; i += 1) {
104
120
  const arg = argv[i];
@@ -108,7 +124,7 @@ export function parseCliArgs(argv) {
108
124
  const next = argv[i + 1];
109
125
  if (arg === "--help" || arg === "-h") {
110
126
  const helpAdapter = adapter;
111
- const commandName = `wechat-${helpAdapter}`;
127
+ const commandName = `${channelId}-${helpAdapter}`;
112
128
  const adapterLabel = {
113
129
  codex: "Codex",
114
130
  claude: "Claude Code",
@@ -118,7 +134,7 @@ export function parseCliArgs(argv) {
118
134
  process.stdout.write([
119
135
  `Usage: ${commandName} [--cwd <path>] [--profile <name-or-path>] [--timeout-ms <ms>] [--session-start-mode <restore|new>] [...${helpAdapter} args]`,
120
136
  "",
121
- "Ensures the WeChat runtime for the current directory, then opens or reuses the visible CLI.",
137
+ `Ensures the ${channelId === "wecom" ? "WeCom" : "WeChat"} runtime for the current directory, then opens or reuses the visible CLI.`,
122
138
  "If a same-workspace daemon is running, the command delegates to that daemon.",
123
139
  helpAdapter === "codex"
124
140
  ? "Codex restores the current session by default."
@@ -136,6 +152,14 @@ export function parseCliArgs(argv) {
136
152
  if (arg === "--doctor") {
137
153
  continue;
138
154
  }
155
+ if (arg === "--channel") {
156
+ if (!next || (next !== "wechat" && next !== "wecom")) {
157
+ throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
158
+ }
159
+ channelId = next;
160
+ i += 1;
161
+ continue;
162
+ }
139
163
  if (arg === "--adapter") {
140
164
  if (!next || !["codex", "claude", "opencode", "pi"].includes(next)) {
141
165
  throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
@@ -189,6 +213,7 @@ export function parseCliArgs(argv) {
189
213
  timeoutMs,
190
214
  sessionStartMode: sessionStartMode ?? defaultSessionStartMode(adapter),
191
215
  cliArgs,
216
+ channelId,
192
217
  };
193
218
  }
194
219
  function isPidAlive(pid) {
@@ -295,6 +320,9 @@ export function buildBackgroundBridgeArgs(entryPath, options) {
295
320
  args.push("--experimental-strip-types");
296
321
  }
297
322
  args.push(entryPath, "--adapter", options.adapter, "--cwd", options.cwd, "--lifecycle", lifecycle);
323
+ if (options.channelId === "wecom") {
324
+ args.push("--channel", "wecom");
325
+ }
298
326
  if (options.sessionStartMode !== "restore") {
299
327
  args.push("--session-start-mode", options.sessionStartMode);
300
328
  }
@@ -361,6 +389,7 @@ async function ensureBridgeReady(options) {
361
389
  endpointIsReachable: Boolean(endpointResult.endpoint),
362
390
  companionIsAlive: isCompanionAlive(endpointResult.endpoint),
363
391
  sessionStartMode: options.sessionStartMode,
392
+ requestedChannelId: options.channelId,
364
393
  });
365
394
  log(options.adapter, decision.message);
366
395
  if (decision.kind === "already_active") {
@@ -408,8 +437,13 @@ export async function tryDelegateToDaemon(options, deps = {}) {
408
437
  clearEndpoint(endpoint.pid);
409
438
  return false;
410
439
  }
440
+ const requestedChannel = options.channelId ?? "wechat";
441
+ const endpointChannel = endpoint.channelId ?? "wechat";
442
+ if (requestedChannel !== endpointChannel) {
443
+ throw new Error(`${endpointChannel}-daemon is running for ${endpoint.cwd}. Stop it before starting the ${requestedChannel} channel.`);
444
+ }
411
445
  if (!isSameWorkspaceCwd(endpoint.cwd, options.cwd)) {
412
- throw new Error(`wechat-daemon is running for ${endpoint.cwd}. This launcher requested ${options.cwd}; v1 daemon switching is limited to its startup cwd.`);
446
+ throw new Error(`${requestedChannel}-daemon is running for ${endpoint.cwd}. This launcher requested ${options.cwd}; v1 daemon switching is limited to its startup cwd.`);
413
447
  }
414
448
  const response = await sendRequest(endpoint, {
415
449
  command: "ensure_slot",
@@ -429,7 +463,7 @@ export async function tryDelegateToDaemon(options, deps = {}) {
429
463
  log(options.adapter, `Daemon delegation failed (${response.error ?? "unknown error"}). Falling back to a local bridge.`);
430
464
  return false;
431
465
  }
432
- log(options.adapter, `Delegated to running wechat-daemon for ${options.cwd}.`);
466
+ log(options.adapter, `Delegated to running ${requestedChannel}-daemon for ${options.cwd}.`, requestedChannel);
433
467
  return true;
434
468
  }
435
469
  export async function runVisibleClient(options, runners = {}) {
@@ -456,6 +490,15 @@ export async function ensureCompanionStartWechatCredentials(adapter, ensureCrede
456
490
  log: (message) => log(adapter, message),
457
491
  });
458
492
  }
493
+ export async function ensureCompanionStartChannelCredentials(options) {
494
+ if (options.channelId === "wecom") {
495
+ await ensureWecomAccount({
496
+ log: (message) => log(options.adapter, message, "wecom"),
497
+ });
498
+ return;
499
+ }
500
+ await ensureCompanionStartWechatCredentials(options.adapter);
501
+ }
459
502
  export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
460
503
  if (argv.includes("--doctor")) {
461
504
  const { runDoctorCheck } = await import("../utils/doctor.js");
@@ -463,7 +506,7 @@ export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
463
506
  return 0;
464
507
  }
465
508
  const options = parseCliArgs(argv);
466
- await ensureCompanionStartWechatCredentials(options.adapter);
509
+ await ensureCompanionStartChannelCredentials(options);
467
510
  if (await tryDelegateToDaemon(options)) {
468
511
  return 0;
469
512
  }
@@ -487,11 +530,19 @@ export async function main(argv = process.argv.slice(2)) {
487
530
  return DEFAULT_ADAPTER;
488
531
  }
489
532
  })();
490
- log(adapter, error instanceof Error ? error.message : String(error));
533
+ const channelId = (() => {
534
+ try {
535
+ return parseCliArgs(argv).channelId ?? "wechat";
536
+ }
537
+ catch {
538
+ return "wechat";
539
+ }
540
+ })();
541
+ log(adapter, error instanceof Error ? error.message : String(error), channelId);
491
542
  process.exit(1);
492
543
  }
493
544
  }
494
- const isDirectRun = Boolean(import.meta.main);
545
+ const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
495
546
  if (isDirectRun) {
496
547
  void main();
497
548
  }
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env bun
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
4
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
5
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
6
+ // must be released under AGPL-3.0-or-later with full source code; see
7
+ // LICENSE.txt. Network services built on it must offer source to users.
2
8
  import net from "node:net";
3
9
  import path from "node:path";
4
10
  import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
@@ -6,6 +12,7 @@ import { LOCAL_COMPANION_RECONNECT_GRACE_MS, } from "../bridge/bridge-adapters.s
6
12
  import { runCodexRemoteClientFromEndpoint } from "./codex-remote-client.js";
7
13
  import { attachLocalCompanionMessageListener, readLocalCompanionEndpoint, sendLocalCompanionMessage, } from "./local-companion-link.js";
8
14
  import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
15
+ import { isDirectModuleRun } from "../core/direct-run.js";
9
16
  export const LOCAL_COMPANION_RECONNECT_RETRY_MS = 250;
10
17
  function log(adapter, message) {
11
18
  process.stderr.write(`[${adapter}-companion] ${message}\n`);
@@ -230,6 +237,23 @@ export async function runLocalCompanion(options) {
230
237
  publishState();
231
238
  sendResponse(socket, message.id, true);
232
239
  break;
240
+ case "list_models":
241
+ if (!adapter.listModels)
242
+ throw new Error("Model listing is not supported by this companion.");
243
+ sendResponse(socket, message.id, true, await adapter.listModels());
244
+ break;
245
+ case "select_model":
246
+ if (!adapter.selectModel)
247
+ throw new Error("Model selection is not supported by this companion.");
248
+ sendResponse(socket, message.id, true, await adapter.selectModel(message.payload.modelId));
249
+ publishState();
250
+ break;
251
+ case "set_plan_mode":
252
+ if (!adapter.setPlanMode)
253
+ throw new Error("Plan mode is not supported by this companion.");
254
+ sendResponse(socket, message.id, true, await adapter.setPlanMode(message.payload.enabled));
255
+ publishState();
256
+ break;
233
257
  case "interrupt":
234
258
  sendResponse(socket, message.id, true, await adapter.interrupt());
235
259
  break;
@@ -439,7 +463,7 @@ export async function main(argv = process.argv.slice(2)) {
439
463
  process.exit(1);
440
464
  }
441
465
  }
442
- const isDirectRun = Boolean(import.meta.main);
466
+ const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
443
467
  if (isDirectRun) {
444
468
  void main();
445
469
  }
@@ -1,3 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import net from "node:net";
2
8
  const ROUTE_POLL_INTERVAL_MS = 100;
3
9
  const MAX_BUFFER_SIZE = 1024 * 1024;
@@ -1,3 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import fs from "node:fs";
2
8
  import net from "node:net";
3
9
  import path from "node:path";
@@ -167,7 +173,7 @@ export default function piTuiBridgeExtension(pi) {
167
173
  }
168
174
  },
169
175
  });
170
- const executeCommand = (command) => {
176
+ const executeCommand = async (command) => {
171
177
  try {
172
178
  if (command.type === "prompt" && typeof command.text === "string") {
173
179
  if (!latestContext?.isIdle()) {
@@ -180,6 +186,59 @@ export default function piTuiBridgeExtension(pi) {
180
186
  latestContext?.abort();
181
187
  writeFrame({ type: "response", id: command.id, success: true });
182
188
  }
189
+ else if (command.type === "list_models") {
190
+ if (!latestContext) {
191
+ throw new Error("Pi session context is not ready.");
192
+ }
193
+ const models = latestContext.scopedModels.length > 0
194
+ ? latestContext.scopedModels.map((entry) => entry.model)
195
+ : latestContext.modelRegistry.getAvailable();
196
+ writeFrame({
197
+ type: "response",
198
+ id: command.id,
199
+ success: true,
200
+ data: {
201
+ models: models.map((model) => ({
202
+ id: `${model.provider}/${model.id}`,
203
+ displayName: model.name
204
+ ? `${model.name} (${model.provider})`
205
+ : `${model.id} (${model.provider})`,
206
+ isCurrent: latestContext?.model?.provider === model.provider &&
207
+ latestContext.model.id === model.id,
208
+ })),
209
+ },
210
+ });
211
+ }
212
+ else if (command.type === "select_model" && typeof command.modelId === "string") {
213
+ if (!latestContext?.isIdle()) {
214
+ throw new Error("Pi TUI is already processing a turn.");
215
+ }
216
+ const models = latestContext.scopedModels.length > 0
217
+ ? latestContext.scopedModels.map((entry) => entry.model)
218
+ : latestContext.modelRegistry.getAvailable();
219
+ const model = models.find((candidate) => `${candidate.provider}/${candidate.id}` === command.modelId);
220
+ if (!model) {
221
+ throw new Error(`Pi model is no longer available: ${command.modelId}`);
222
+ }
223
+ const selected = await pi.setModel(model);
224
+ if (!selected) {
225
+ throw new Error(`Pi could not switch to model: ${command.modelId}`);
226
+ }
227
+ writeFrame({
228
+ type: "response",
229
+ id: command.id,
230
+ success: true,
231
+ data: {
232
+ model: {
233
+ id: `${model.provider}/${model.id}`,
234
+ displayName: model.name
235
+ ? `${model.name} (${model.provider})`
236
+ : `${model.id} (${model.provider})`,
237
+ isCurrent: true,
238
+ },
239
+ },
240
+ });
241
+ }
183
242
  else if (command.type === "new_session" && typeof command.id === "string") {
184
243
  pi.sendUserMessage(`/${INTERNAL_NEW_COMMAND} ${command.id}`, {
185
244
  expandPromptTemplates: true,
@@ -245,7 +304,7 @@ export default function piTuiBridgeExtension(pi) {
245
304
  try {
246
305
  const parsed = JSON.parse(line);
247
306
  if (isRecord(parsed)) {
248
- dispatchCommand(parsed);
307
+ void dispatchCommand(parsed);
249
308
  }
250
309
  }
251
310
  catch {
@@ -260,7 +319,7 @@ export default function piTuiBridgeExtension(pi) {
260
319
  latestContext = context;
261
320
  writeSessionState(context, typeof event.reason === "string" ? event.reason : "startup");
262
321
  for (const command of queuedCommands.splice(0)) {
263
- executeCommand(command);
322
+ void executeCommand(command);
264
323
  }
265
324
  });
266
325
  pi.on("input", (event, context) => {
@@ -26,6 +26,16 @@ export function parseBridgeControlCommand(text) {
26
26
  return { type: "deny" };
27
27
  case "/answer":
28
28
  return argument ? { type: "answer", raw: argument } : null;
29
+ case "/model":
30
+ return { type: "model", target: argument || undefined };
31
+ case "/plan":
32
+ if (!argument || argument.toLowerCase() === "on")
33
+ return { type: "plan", enabled: true };
34
+ if (argument.toLowerCase() === "off")
35
+ return { type: "plan", enabled: false };
36
+ return null;
37
+ case "/all":
38
+ return argument ? { type: "broadcast", text: argument } : null;
29
39
  default:
30
40
  return null;
31
41
  }
@@ -0,0 +1,25 @@
1
+ export function shouldDeferCodexInboundMessage(params) {
2
+ return (params.adapter === "codex" &&
3
+ !params.hasPendingConfirmation &&
4
+ !params.hasSystemCommand &&
5
+ params.activeTurnOrigin === "local" &&
6
+ (params.status === "busy" || params.status === "awaiting_approval"));
7
+ }
8
+ export function canDrainDeferredCodexInboundQueue(params) {
9
+ return (params.adapter === "codex" &&
10
+ params.deferredCount > 0 &&
11
+ !params.hasPendingConfirmation &&
12
+ !params.hasPendingUserInput &&
13
+ !params.hasPendingApproval &&
14
+ !params.hasActiveTask &&
15
+ !params.activeTurnId &&
16
+ params.status !== "busy" &&
17
+ params.status !== "awaiting_approval" &&
18
+ params.status !== "awaiting_input");
19
+ }
20
+ export function formatDeferredCodexInboundQueueMessage(queuePosition) {
21
+ return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
22
+ }
23
+ export function isRetryableDeferredCodexDrainError(errorText) {
24
+ return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,3 @@
1
- export {};
1
+ export function normalizeBridgeChannelId(value) {
2
+ return value === "wecom" ? "wecom" : "wechat";
3
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
+ /**
9
+ * Keeps the reply target scoped to one inbound async call chain. Different
10
+ * conversations may run concurrently without overwriting each other's target.
11
+ */
12
+ export class InboundConversationContext {
13
+ storage = new AsyncLocalStorage();
14
+ run(conversation, callback) {
15
+ return this.storage.run(conversation, callback);
16
+ }
17
+ get() {
18
+ return this.storage.getStore();
19
+ }
20
+ }
21
+ export function tryBeginTurn(state, task, conversation) {
22
+ if (state.activeTask) {
23
+ return null;
24
+ }
25
+ const lease = {
26
+ task,
27
+ previousConversation: state.activeConversation,
28
+ previousLastConversation: state.lastConversation,
29
+ };
30
+ state.activeTask = task;
31
+ if (conversation) {
32
+ state.activeConversation = conversation;
33
+ state.lastConversation = conversation;
34
+ }
35
+ return lease;
36
+ }
37
+ export function rollbackTurn(state, lease, options = {}) {
38
+ if (state.activeTask !== lease.task) {
39
+ return false;
40
+ }
41
+ state.activeTask = null;
42
+ state.activeConversation = lease.previousConversation;
43
+ if (options.restoreLastConversation) {
44
+ state.lastConversation = lease.previousLastConversation;
45
+ }
46
+ return true;
47
+ }
48
+ export function clearTurn(state, expectedTask) {
49
+ if (expectedTask && state.activeTask !== expectedTask) {
50
+ return false;
51
+ }
52
+ state.activeTask = null;
53
+ state.activeConversation = undefined;
54
+ return true;
55
+ }
56
+ export function resolveConversationTarget(params) {
57
+ return params.active ?? params.last ?? params.fallback;
58
+ }
@@ -0,0 +1,23 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ function normalizeComparablePath(filePath) {
10
+ const normalized = path.resolve(filePath);
11
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
12
+ }
13
+ export function isDirectModuleRun(importMetaUrl, argv = process.argv, importMetaMain) {
14
+ if (typeof importMetaMain === "boolean") {
15
+ return importMetaMain;
16
+ }
17
+ const entryPath = argv[1];
18
+ if (!entryPath) {
19
+ return false;
20
+ }
21
+ return normalizeComparablePath(fileURLToPath(importMetaUrl)) ===
22
+ normalizeComparablePath(entryPath);
23
+ }
@@ -0,0 +1,67 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ import crypto from "node:crypto";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ export const DEFAULT_INBOUND_MESSAGE_CLAIM_TTL_MS = 10 * 60 * 1000;
11
+ export function buildInboundMessageClaimPath(messageKey, claimsDir) {
12
+ const fileName = `${crypto.createHash("sha1").update(messageKey).digest("hex")}.json`;
13
+ return path.join(claimsDir, fileName);
14
+ }
15
+ export function tryClaimChannelInboundMessage(messageKey, options) {
16
+ if (!messageKey) {
17
+ return false;
18
+ }
19
+ const nowMs = options.nowMs ?? Date.now();
20
+ const ttlMs = options.ttlMs ?? DEFAULT_INBOUND_MESSAGE_CLAIM_TTL_MS;
21
+ const claimPath = buildInboundMessageClaimPath(messageKey, options.claimsDir);
22
+ const attemptClaim = () => {
23
+ fs.mkdirSync(options.claimsDir, { recursive: true });
24
+ const handle = fs.openSync(claimPath, "wx");
25
+ try {
26
+ fs.writeFileSync(handle, JSON.stringify({
27
+ key: messageKey,
28
+ claimedAt: new Date(nowMs).toISOString(),
29
+ pid: process.pid,
30
+ }, null, 2), "utf-8");
31
+ }
32
+ finally {
33
+ fs.closeSync(handle);
34
+ }
35
+ return true;
36
+ };
37
+ try {
38
+ return attemptClaim();
39
+ }
40
+ catch (error) {
41
+ const code = typeof error === "object" &&
42
+ error !== null &&
43
+ "code" in error &&
44
+ typeof error.code === "string"
45
+ ? error.code
46
+ : "";
47
+ if (code !== "EEXIST") {
48
+ return false;
49
+ }
50
+ }
51
+ try {
52
+ const stat = fs.statSync(claimPath);
53
+ if (Number.isFinite(stat.mtimeMs) && nowMs - stat.mtimeMs > ttlMs) {
54
+ fs.rmSync(claimPath, { force: true });
55
+ return attemptClaim();
56
+ }
57
+ }
58
+ catch {
59
+ try {
60
+ return attemptClaim();
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ return false;
67
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Outbound target priority for standalone bridges:
3
+ * 1. an explicit override captured at enqueue time;
4
+ * 2. for the operator on multi-conversation channels, the pre-resolved turn
5
+ * target (callers pass `TurnCoordinator.resolveTarget(operatorConversation)`);
6
+ * 3. otherwise a direct conversation to the sender.
7
+ */
8
+ export function resolveOutboundConversationTarget(params) {
9
+ if (params.override) {
10
+ return params.override;
11
+ }
12
+ if (params.multiConversation && params.senderId === params.operatorId) {
13
+ return params.operatorTarget;
14
+ }
15
+ return params.directConversation(params.senderId);
16
+ }
17
+ /**
18
+ * Outbound target priority for daemon sends. Multi-conversation operators
19
+ * resolve through the current inbound conversation first, then the active
20
+ * slot's remembered target, then the daemon fallback conversation.
21
+ */
22
+ export function resolveDaemonOutboundTarget(params) {
23
+ if (params.override) {
24
+ return params.override;
25
+ }
26
+ if (params.multiConversation && params.senderId === params.operatorId) {
27
+ return params.inboundConversation
28
+ ?? params.activeSlotTarget
29
+ ?? params.fallbackConversation;
30
+ }
31
+ return params.directConversation(params.senderId);
32
+ }
@@ -1,3 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  const ANSI_ESCAPE_RE =
2
8
  // eslint-disable-next-line no-control-regex
3
9
  /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;