@unlinearity/cli-wechat-bridge 1.2.0 → 1.2.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 (50) hide show
  1. package/README.md +25 -10
  2. package/bin/wechat-check-update.mjs +0 -0
  3. package/bin/wechat-claude.mjs +0 -0
  4. package/bin/wechat-codex.mjs +0 -0
  5. package/bin/wechat-daemon.mjs +0 -0
  6. package/bin/wechat-opencode.mjs +0 -0
  7. package/bin/wechat-pi.mjs +0 -0
  8. package/bin/wechat-setup.mjs +0 -0
  9. package/bin/wecom-check-update.mjs +0 -0
  10. package/bin/wecom-claude.mjs +0 -0
  11. package/bin/wecom-codex.mjs +0 -0
  12. package/bin/wecom-daemon.mjs +0 -0
  13. package/bin/wecom-opencode.mjs +0 -0
  14. package/bin/wecom-pi.mjs +0 -0
  15. package/bin/wecom-setup.mjs +0 -0
  16. package/dist/bridge/adapter-control.js +19 -12
  17. package/dist/bridge/bridge-adapters.claude.js +219 -115
  18. package/dist/bridge/bridge-adapters.codex.js +295 -90
  19. package/dist/bridge/bridge-adapters.core.js +84 -7
  20. package/dist/bridge/bridge-adapters.opencode.js +344 -74
  21. package/dist/bridge/bridge-adapters.pi.js +23 -18
  22. package/dist/bridge/bridge-adapters.shared.js +9 -9
  23. package/dist/bridge/bridge-session-resume.js +13 -6
  24. package/dist/bridge/bridge-utils.js +94 -144
  25. package/dist/bridge/claude-hooks.js +60 -3
  26. package/dist/bridge/claude-native-controls.js +32 -13
  27. package/dist/bridge/codex-visible-thread-proxy.js +140 -0
  28. package/dist/bridge/language-control.js +33 -0
  29. package/dist/bridge/native-terminal-control.js +38 -10
  30. package/dist/bridge/opencode-v2-compat.js +458 -0
  31. package/dist/bridge/wechat-bridge.js +57 -42
  32. package/dist/channels/channel-driver-factory.js +44 -0
  33. package/dist/channels/local/local-channel-port.js +40 -0
  34. package/dist/channels/local/local-driver.js +217 -0
  35. package/dist/channels/wechat/wechat-driver.js +11 -2
  36. package/dist/channels/wecom/wecom-driver.js +8 -1
  37. package/dist/companion/local-companion.js +27 -7
  38. package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
  39. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  40. package/dist/core/bridge-control.js +3 -0
  41. package/dist/core/bridge-defer.js +3 -2
  42. package/dist/core/bridge-event-forwarder.js +3 -0
  43. package/dist/core/channel-types.js +5 -1
  44. package/dist/daemon/wechat-daemon.js +202 -124
  45. package/dist/i18n/index.js +25 -11
  46. package/dist/i18n/messages-en.js +244 -1
  47. package/dist/i18n/messages-zh.js +244 -1
  48. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  49. package/dist/wechat/channel-config.js +3 -2
  50. package/package.json +4 -2
@@ -7,6 +7,7 @@
7
7
  // LICENSE.txt. Network services built on it must offer source to users.
8
8
  import net from "node:net";
9
9
  import path from "node:path";
10
+ import { parseLocale, setLocale, t } from "../i18n/index.js";
10
11
  import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
11
12
  import { LOCAL_COMPANION_RECONNECT_GRACE_MS, } from "../bridge/bridge-adapters.shared.js";
12
13
  import { runCodexRemoteClientFromEndpoint } from "./codex-remote-client.js";
@@ -170,7 +171,7 @@ export async function runLocalCompanion(options) {
170
171
  }
171
172
  sendLocalCompanionMessage(activeSocket, {
172
173
  type: "state",
173
- state: adapter.getState(),
174
+ state: { ...adapter.getState(), supportsLocaleUpdates: true },
174
175
  });
175
176
  };
176
177
  const sendResponse = (socket, id, ok, result, error) => {
@@ -211,6 +212,15 @@ export async function runLocalCompanion(options) {
211
212
  const handleBridgeRequest = async (socket, message) => {
212
213
  try {
213
214
  switch (message.payload.command) {
215
+ case "set_locale": {
216
+ const locale = parseLocale(message.payload.locale);
217
+ if (!locale)
218
+ throw new Error(t("language.usage"));
219
+ setLocale(locale);
220
+ process.env.CLI_BRIDGE_LANG = locale;
221
+ sendResponse(socket, message.id, true);
222
+ break;
223
+ }
214
224
  case "send_input":
215
225
  await adapter.sendInput(message.payload.text);
216
226
  sendResponse(socket, message.id, true);
@@ -239,18 +249,18 @@ export async function runLocalCompanion(options) {
239
249
  break;
240
250
  case "list_models":
241
251
  if (!adapter.listModels)
242
- throw new Error("Model listing is not supported by this companion.");
252
+ throw new Error(t("companion.modelsUnsupported"));
243
253
  sendResponse(socket, message.id, true, await adapter.listModels());
244
254
  break;
245
255
  case "select_model":
246
256
  if (!adapter.selectModel)
247
- throw new Error("Model selection is not supported by this companion.");
257
+ throw new Error(t("companion.modelUnsupported"));
248
258
  sendResponse(socket, message.id, true, await adapter.selectModel(message.payload.modelId));
249
259
  publishState();
250
260
  break;
251
261
  case "set_plan_mode":
252
262
  if (!adapter.setPlanMode)
253
- throw new Error("Plan mode is not supported by this companion.");
263
+ throw new Error(t("companion.planUnsupported"));
254
264
  sendResponse(socket, message.id, true, await adapter.setPlanMode(message.payload.enabled));
255
265
  publishState();
256
266
  break;
@@ -263,10 +273,14 @@ export async function runLocalCompanion(options) {
263
273
  sendResponse(socket, message.id, true);
264
274
  break;
265
275
  case "resolve_approval":
266
- sendResponse(socket, message.id, true, await adapter.resolveApproval(message.payload.action));
276
+ sendResponse(socket, message.id, true, message.payload.requestId === undefined
277
+ ? false
278
+ : await adapter.resolveApproval(message.payload.action, message.payload.requestId));
267
279
  break;
268
280
  case "submit_user_input":
269
- sendResponse(socket, message.id, true, await adapter.submitUserInput(message.payload.answers));
281
+ sendResponse(socket, message.id, true, adapter.getState().supportsUserInputRequestIds && !message.payload.requestId
282
+ ? false
283
+ : await adapter.submitUserInput(message.payload.answers, message.payload.requestId));
270
284
  break;
271
285
  case "dispose":
272
286
  sendResponse(socket, message.id, true);
@@ -308,6 +322,11 @@ export async function runLocalCompanion(options) {
308
322
  localDetach = attachLocalCompanionMessageListener(socket, (message) => {
309
323
  if (!helloAcknowledged) {
310
324
  if (message.type === "hello_ack") {
325
+ const locale = parseLocale(message.locale);
326
+ if (locale) {
327
+ setLocale(locale);
328
+ process.env.CLI_BRIDGE_LANG = locale;
329
+ }
311
330
  helloAcknowledged = true;
312
331
  closeReason = null;
313
332
  activeSocket = socket;
@@ -356,6 +375,7 @@ export async function runLocalCompanion(options) {
356
375
  type: "hello",
357
376
  token: endpoint.token,
358
377
  companionPid: process.pid,
378
+ supportsLocaleUpdates: true,
359
379
  });
360
380
  });
361
381
  socket.once("error", (error) => {
@@ -402,13 +422,13 @@ export async function runLocalCompanion(options) {
402
422
  }
403
423
  };
404
424
  adapter.setEventSink((event) => {
425
+ publishState();
405
426
  if (activeSocket) {
406
427
  sendLocalCompanionMessage(activeSocket, {
407
428
  type: "event",
408
429
  event,
409
430
  });
410
431
  }
411
- publishState();
412
432
  if (event.type === "fatal_error") {
413
433
  void closeCompanion(1, "fatal_error");
414
434
  return;
@@ -0,0 +1,93 @@
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 net from "node:net";
8
+ const ROUTE_POLL_INTERVAL_MS = 100;
9
+ const MAX_BUFFER_SIZE = 1024 * 1024;
10
+ export default {
11
+ id: "cli-wechat-bridge.session-route-v2",
12
+ setup(context) {
13
+ const port = Number(context.options.port);
14
+ const token = typeof context.options.token === "string" ? context.options.token : "";
15
+ if (!Number.isInteger(port) || port <= 0 || !token) {
16
+ return () => undefined;
17
+ }
18
+ const socket = net.connect({ host: "127.0.0.1", port });
19
+ let connected = false;
20
+ let inputBuffer = "";
21
+ let lastSessionId;
22
+ let queuedFrame = null;
23
+ const writeFrame = (frame) => {
24
+ if (socket.destroyed)
25
+ return;
26
+ if (!connected) {
27
+ queuedFrame = frame;
28
+ return;
29
+ }
30
+ socket.write(`${JSON.stringify(frame)}\n`);
31
+ };
32
+ const publishRoute = () => {
33
+ const route = context.ui.router.current();
34
+ const sessionId = route.type === "session" && typeof route.sessionID === "string"
35
+ ? route.sessionID
36
+ : null;
37
+ if (sessionId === lastSessionId)
38
+ return;
39
+ lastSessionId = sessionId;
40
+ writeFrame({
41
+ type: "route_state",
42
+ sessionId,
43
+ observedAt: new Date().toISOString(),
44
+ });
45
+ };
46
+ socket.setEncoding("utf8");
47
+ socket.on("connect", () => {
48
+ socket.setNoDelay(true);
49
+ socket.write(`${JSON.stringify({ type: "hello", token })}\n`);
50
+ connected = true;
51
+ publishRoute();
52
+ if (queuedFrame) {
53
+ socket.write(`${JSON.stringify(queuedFrame)}\n`);
54
+ queuedFrame = null;
55
+ }
56
+ });
57
+ socket.on("data", (chunk) => {
58
+ inputBuffer += chunk;
59
+ if (inputBuffer.length > MAX_BUFFER_SIZE) {
60
+ socket.destroy();
61
+ return;
62
+ }
63
+ for (;;) {
64
+ const newline = inputBuffer.indexOf("\n");
65
+ if (newline < 0)
66
+ break;
67
+ const line = inputBuffer.slice(0, newline).trim();
68
+ inputBuffer = inputBuffer.slice(newline + 1);
69
+ if (!line)
70
+ continue;
71
+ try {
72
+ const frame = JSON.parse(line);
73
+ if (frame.type === "select_session" && typeof frame.sessionId === "string") {
74
+ context.ui.router.navigate({ type: "session", sessionID: frame.sessionId });
75
+ publishRoute();
76
+ }
77
+ }
78
+ catch {
79
+ // Ignore malformed bridge frames and keep the visible TUI alive.
80
+ }
81
+ }
82
+ });
83
+ socket.on("error", () => {
84
+ // Keep the native TUI usable if the bridge-side observer disappears.
85
+ });
86
+ const timer = setInterval(publishRoute, ROUTE_POLL_INTERVAL_MS);
87
+ timer.unref?.();
88
+ return () => {
89
+ clearInterval(timer);
90
+ socket.destroy();
91
+ };
92
+ },
93
+ };
@@ -330,11 +330,14 @@ export default function piTuiBridgeExtension(pi) {
330
330
  });
331
331
  pi.on("message_end", (event, context) => {
332
332
  latestContext = context;
333
+ const message = isRecord(event.message) ? event.message : {};
334
+ if (message.role !== "assistant") {
335
+ return;
336
+ }
333
337
  const text = extractAssistantText(event.message);
334
- if (!text) {
338
+ if (!text && message.stopReason !== "error") {
335
339
  return;
336
340
  }
337
- const message = isRecord(event.message) ? event.message : {};
338
341
  writeFrame({
339
342
  type: "assistant_message",
340
343
  text,
@@ -346,9 +349,12 @@ export default function piTuiBridgeExtension(pi) {
346
349
  latestContext = context;
347
350
  writeFrame({ type: "agent_start" });
348
351
  });
349
- pi.on("agent_settled", (_event, context) => {
352
+ pi.on("agent_settled", (event, context) => {
350
353
  latestContext = context;
351
354
  writeSessionState(context, "settled");
352
- writeFrame({ type: "agent_settled" });
355
+ writeFrame({
356
+ type: "agent_settled",
357
+ ...(typeof event.aborted === "boolean" ? { aborted: event.aborted } : {}),
358
+ });
353
359
  });
354
360
  }
@@ -7,6 +7,9 @@ export function parseBridgeControlCommand(text) {
7
7
  const command = rawCommand.toLowerCase();
8
8
  const argument = rest.join(" ").trim();
9
9
  switch (command) {
10
+ case "/lang":
11
+ case "/language":
12
+ return { type: "language", target: argument || undefined };
10
13
  case "/status":
11
14
  return { type: "status" };
12
15
  case "/resume":
@@ -1,3 +1,4 @@
1
+ import { t } from "../i18n/index.js";
1
2
  export function shouldDeferCodexInboundMessage(params) {
2
3
  return (params.adapter === "codex" &&
3
4
  !params.hasPendingConfirmation &&
@@ -18,8 +19,8 @@ export function canDrainDeferredCodexInboundQueue(params) {
18
19
  params.status !== "awaiting_input");
19
20
  }
20
21
  export function formatDeferredCodexInboundQueueMessage(queuePosition) {
21
- return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
22
+ return t("bridge.deferred", { position: queuePosition });
22
23
  }
23
24
  export function isRetryableDeferredCodexDrainError(errorText) {
24
- return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
25
+ return /still working|approval request is pending|waiting for local terminal input|仍在运行|有待审批请求|正在等待本地终端输入/i.test(errorText);
25
26
  }
@@ -19,6 +19,9 @@ export async function forwardBridgeEvent(event, handlers) {
19
19
  case "thinking":
20
20
  await handlers.thinking?.(event);
21
21
  return;
22
+ case "approval_state_changed":
23
+ await handlers.approvalStateChanged?.(event);
24
+ return;
22
25
  case "approval_required":
23
26
  await handlers.approvalRequired?.(event);
24
27
  return;
@@ -1,3 +1,7 @@
1
1
  export function normalizeBridgeChannelId(value) {
2
- return value === "wecom" ? "wecom" : "wechat";
2
+ if (value === "wecom")
3
+ return "wecom";
4
+ if (value === "local")
5
+ return "local";
6
+ return "wechat";
3
7
  }