@unlinearity/cli-wechat-bridge 1.2.1 → 1.2.3

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 (54) hide show
  1. package/README.md +146 -73
  2. package/README_EN.md +544 -0
  3. package/bin/telegram-check-update.mjs +10 -0
  4. package/bin/telegram-claude.mjs +10 -0
  5. package/bin/telegram-codex.mjs +10 -0
  6. package/bin/telegram-daemon.mjs +10 -0
  7. package/bin/telegram-opencode.mjs +10 -0
  8. package/bin/telegram-pi.mjs +10 -0
  9. package/bin/telegram-setup.mjs +10 -0
  10. package/dist/bridge/adapter-control.js +19 -12
  11. package/dist/bridge/bridge-adapters.claude.js +219 -115
  12. package/dist/bridge/bridge-adapters.codex.js +532 -99
  13. package/dist/bridge/bridge-adapters.core.js +84 -7
  14. package/dist/bridge/bridge-adapters.opencode.js +202 -55
  15. package/dist/bridge/bridge-adapters.pi.js +23 -18
  16. package/dist/bridge/bridge-adapters.shared.js +9 -9
  17. package/dist/bridge/bridge-session-resume.js +13 -6
  18. package/dist/bridge/bridge-state.js +3 -1
  19. package/dist/bridge/bridge-utils.js +94 -144
  20. package/dist/bridge/claude-hooks.js +60 -3
  21. package/dist/bridge/claude-native-controls.js +19 -12
  22. package/dist/bridge/language-control.js +33 -0
  23. package/dist/bridge/native-terminal-control.js +26 -10
  24. package/dist/bridge/opencode-v2-compat.js +11 -3
  25. package/dist/bridge/wechat-bridge.js +154 -72
  26. package/dist/channels/channel-driver-factory.js +56 -0
  27. package/dist/channels/local/local-channel-port.js +40 -0
  28. package/dist/channels/local/local-driver.js +217 -0
  29. package/dist/channels/telegram/setup-cli.js +12 -0
  30. package/dist/channels/telegram/setup.js +163 -0
  31. package/dist/channels/telegram/telegram-channel-port.js +47 -0
  32. package/dist/channels/telegram/telegram-config.js +66 -0
  33. package/dist/channels/telegram/telegram-driver.js +106 -0
  34. package/dist/channels/telegram/telegram-message.js +41 -0
  35. package/dist/channels/telegram/telegram-transport.js +378 -0
  36. package/dist/channels/wechat/wechat-driver.js +11 -2
  37. package/dist/channels/wecom/wecom-driver.js +8 -1
  38. package/dist/companion/local-companion-start.js +180 -61
  39. package/dist/companion/local-companion.js +27 -7
  40. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  41. package/dist/core/bridge-control.js +3 -0
  42. package/dist/core/bridge-defer.js +3 -2
  43. package/dist/core/bridge-event-forwarder.js +3 -0
  44. package/dist/core/channel-types.js +13 -1
  45. package/dist/daemon/wechat-daemon.js +239 -126
  46. package/dist/i18n/index.js +25 -11
  47. package/dist/i18n/messages-en.js +245 -1
  48. package/dist/i18n/messages-zh.js +245 -1
  49. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  50. package/dist/wechat/channel-config.js +3 -2
  51. package/package.json +14 -4
  52. package/skills/codex-bridge/SKILL.md +35 -0
  53. package/skills/codex-bridge/agents/openai.yaml +4 -0
  54. package/skills/codex-bridge/scripts/install.mjs +28 -0
@@ -6,6 +6,7 @@
6
6
  // LICENSE.txt. Network services built on it must offer source to users.
7
7
  import crypto from "node:crypto";
8
8
  import fs from "node:fs";
9
+ import { t } from "../i18n/index.js";
9
10
  import net from "node:net";
10
11
  import os from "node:os";
11
12
  import path from "node:path";
@@ -301,7 +302,7 @@ export class PiTuiAdapter {
301
302
  return;
302
303
  }
303
304
  if (this.state.status === "busy") {
304
- throw new Error("Pi is still working. Wait for the current reply or use /stop.");
305
+ throw new Error(t("adapter.pi.busy"));
305
306
  }
306
307
  this.beginTurn("wechat");
307
308
  try {
@@ -319,7 +320,7 @@ export class PiTuiAdapter {
319
320
  async resumeSession(sessionId) {
320
321
  this.assertConnected();
321
322
  if (this.state.status === "busy") {
322
- throw new Error("Pi is still working. Stop the current turn before switching sessions.");
323
+ throw new Error(t("adapter.pi.switchBusy"));
323
324
  }
324
325
  const sessionPath = findPiSessionFile(this.options.cwd, sessionId);
325
326
  if (!sessionPath) {
@@ -338,7 +339,7 @@ export class PiTuiAdapter {
338
339
  });
339
340
  this.applySessionState(response.data, "wechat", "wechat_resume", true);
340
341
  if (isRecord(response.data) && response.data.cancelled === true) {
341
- throw new Error("Pi session switch was cancelled by an extension.");
342
+ throw new Error(t("adapter.pi.switchCancelled"));
342
343
  }
343
344
  }
344
345
  finally {
@@ -348,14 +349,14 @@ export class PiTuiAdapter {
348
349
  async createSession() {
349
350
  this.assertConnected();
350
351
  if (this.state.status === "busy") {
351
- throw new Error("Pi is still working. Stop the current turn before creating a session.");
352
+ throw new Error(t("adapter.pi.createBusy"));
352
353
  }
353
354
  this.pendingSessionSwitch = { source: "wechat", reason: "wechat_resume" };
354
355
  try {
355
356
  const response = await this.sendCommand("new_session");
356
357
  this.applySessionState(response.data, "wechat", "wechat_resume", true);
357
358
  if (isRecord(response.data) && response.data.cancelled === true) {
358
- throw new Error("Pi session creation was cancelled by an extension.");
359
+ throw new Error(t("adapter.pi.createCancelled"));
359
360
  }
360
361
  }
361
362
  finally {
@@ -364,11 +365,11 @@ export class PiTuiAdapter {
364
365
  }
365
366
  async listModels() {
366
367
  if (this.state.status === "busy") {
367
- throw new Error("Pi is still working. Wait for the current turn before listing models.");
368
+ throw new Error(t("adapter.pi.listModelsBusy"));
368
369
  }
369
370
  const response = await this.sendCommand("list_models");
370
371
  if (!isRecord(response.data) || !Array.isArray(response.data.models)) {
371
- throw new Error("Pi TUI returned an invalid model list.");
372
+ throw new Error(t("adapter.pi.invalidModels"));
372
373
  }
373
374
  return response.data.models
374
375
  .filter((model) => isRecord(model))
@@ -381,15 +382,15 @@ export class PiTuiAdapter {
381
382
  }
382
383
  async selectModel(modelId) {
383
384
  if (this.state.status === "busy") {
384
- throw new Error("Pi is still working. Wait for the current turn before switching models.");
385
+ throw new Error(t("adapter.pi.selectModelBusy"));
385
386
  }
386
387
  const response = await this.sendCommand("select_model", { modelId });
387
388
  if (!isRecord(response.data) || !isRecord(response.data.model)) {
388
- throw new Error("Pi TUI returned an invalid selected model.");
389
+ throw new Error(t("adapter.pi.invalidModel"));
389
390
  }
390
391
  const model = response.data.model;
391
392
  if (typeof model.id !== "string" || typeof model.displayName !== "string") {
392
- throw new Error("Pi TUI returned an invalid selected model.");
393
+ throw new Error(t("adapter.pi.invalidModel"));
393
394
  }
394
395
  return {
395
396
  id: model.id,
@@ -565,7 +566,7 @@ export class PiTuiAdapter {
565
566
  }
566
567
  break;
567
568
  case "agent_settled":
568
- void this.finalizeTurn();
569
+ void this.finalizeTurn(frame.aborted === true);
569
570
  break;
570
571
  }
571
572
  }
@@ -581,7 +582,7 @@ export class PiTuiAdapter {
581
582
  if (this.state.activeTurnOrigin === "wechat") {
582
583
  this.emit({
583
584
  type: "task_failed",
584
- message: "The active WeChat task was interrupted because the local Pi terminal switched sessions.",
585
+ message: t("adapter.pi.localSwitchInterrupted"),
585
586
  timestamp: nowIso(),
586
587
  });
587
588
  }
@@ -598,7 +599,7 @@ export class PiTuiAdapter {
598
599
  this.state.lastInputAt = nowIso();
599
600
  this.setStatus("busy", origin === "wechat" ? "Pi TUI is handling a WeChat request." : "Pi TUI is working.");
600
601
  }
601
- async finalizeTurn() {
602
+ async finalizeTurn(aborted = false) {
602
603
  if (this.finalizingTurn || !this.state.activeTurnOrigin) {
603
604
  return;
604
605
  }
@@ -613,10 +614,14 @@ export class PiTuiAdapter {
613
614
  // Mirror the other adapters: a settled turn always reports completion so
614
615
  // bridge/daemon ownership state is released even when the turn produced
615
616
  // no visible reply.
616
- this.emit({ type: "task_complete", timestamp: nowIso() });
617
+ this.emit({
618
+ type: "task_complete",
619
+ ...(aborted ? { summary: t("task.interrupted") } : {}),
620
+ timestamp: nowIso(),
621
+ });
617
622
  this.state.lastOutputAt = nowIso();
618
623
  this.clearActiveTurn();
619
- this.setStatus("idle");
624
+ this.setStatus("idle", aborted ? "Pi task interrupted." : undefined);
620
625
  }
621
626
  finally {
622
627
  this.finalizingTurn = false;
@@ -645,7 +650,7 @@ export class PiTuiAdapter {
645
650
  this.assertConnected();
646
651
  const socket = this.socket;
647
652
  if (!socket || socket.destroyed) {
648
- return Promise.reject(new Error("Native Pi TUI is not connected to the WeChat bridge."));
653
+ return Promise.reject(new Error(t("adapter.pi.disconnected")));
649
654
  }
650
655
  const id = `pi-tui-${++this.requestCounter}`;
651
656
  return new Promise((resolve, reject) => {
@@ -685,7 +690,7 @@ export class PiTuiAdapter {
685
690
  }
686
691
  assertConnected() {
687
692
  if (!this.child || !this.socket || this.socket.destroyed) {
688
- throw new Error("Native Pi TUI is not connected to the WeChat bridge.");
693
+ throw new Error(t("adapter.pi.disconnected"));
689
694
  }
690
695
  }
691
696
  clearActiveTurn() {
@@ -715,7 +720,7 @@ export class PiTuiAdapter {
715
720
  this.setStatus("idle", "The Pi TUI extension disconnected; the bridge is waiting for a reconnect.");
716
721
  this.emit({
717
722
  type: "fatal_error",
718
- message: "The native Pi TUI bridge extension disconnected and did not reconnect.",
723
+ message: t("adapter.pi.extensionDisconnected"),
719
724
  timestamp: nowIso(),
720
725
  });
721
726
  }
@@ -141,24 +141,24 @@ export function buildCodexApprovalRequest(method, params) {
141
141
  const reason = typeof params.reason === "string" ? params.reason : "";
142
142
  const preview = command && cwd
143
143
  ? `${command} (${cwd})`
144
- : command || reason || "Command execution approval requested.";
144
+ : command || reason || t("approval.codex.commandPreview");
145
145
  return {
146
146
  source: "cli",
147
147
  summary: reason
148
- ? `Codex needs approval before running a command: ${truncatePreview(reason, 160)}`
149
- : "Codex needs approval before running a command.",
148
+ ? t("approval.codex.commandReason", { reason: truncatePreview(reason, 160) })
149
+ : t("approval.codex.command"),
150
150
  commandPreview: truncatePreview(preview, 180),
151
151
  };
152
152
  }
153
153
  if (method === "item/fileChange/requestApproval") {
154
154
  const grantRoot = typeof params.grantRoot === "string" ? params.grantRoot : "";
155
155
  const reason = typeof params.reason === "string" ? params.reason : "";
156
- const preview = grantRoot || reason || "File change approval requested.";
156
+ const preview = grantRoot || reason || t("approval.codex.filePreview");
157
157
  return {
158
158
  source: "cli",
159
159
  summary: reason
160
- ? `Codex needs approval before applying a file change: ${truncatePreview(reason, 160)}`
161
- : "Codex needs approval before applying a file change.",
160
+ ? t("approval.codex.fileReason", { reason: truncatePreview(reason, 160) })
161
+ : t("approval.codex.file"),
162
162
  commandPreview: truncatePreview(preview, 180),
163
163
  };
164
164
  }
@@ -168,8 +168,8 @@ export function buildCodexApprovalRequest(method, params) {
168
168
  return {
169
169
  source: "cli",
170
170
  summary: reason
171
- ? `Codex needs approval before granting extra permissions: ${truncatePreview(reason, 160)}`
172
- : "Codex needs approval before granting extra permissions.",
171
+ ? t("approval.codex.permissionsReason", { reason: truncatePreview(reason, 160) })
172
+ : t("approval.codex.permissions"),
173
173
  commandPreview: truncatePreview(preview, 180),
174
174
  };
175
175
  }
@@ -428,7 +428,7 @@ export function buildCodexUserInputRequest(params) {
428
428
  return null;
429
429
  }
430
430
  return {
431
- summary: "Codex needs more information before the tool can continue.",
431
+ summary: t("input.request.codex"),
432
432
  questions,
433
433
  };
434
434
  }
@@ -1,3 +1,10 @@
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 { t } from "../i18n/index.js";
1
8
  import { formatResumeSessionList, formatSessionSwitchMessage, } from "./bridge-utils.js";
2
9
  export const RESUME_SESSION_LIST_LIMIT = 8;
3
10
  export const RESUME_SESSION_SNAPSHOT_TTL_MS = 5 * 60 * 1_000;
@@ -18,7 +25,7 @@ function adapterDisplayName(adapter) {
18
25
  }
19
26
  }
20
27
  function adapterSessionNoun(adapter) {
21
- return adapter === "codex" ? "thread" : "session";
28
+ return t(adapter === "codex" ? "session.thread" : "session.noun");
22
29
  }
23
30
  export function isWechatResumeEnabled(adapter) {
24
31
  return (adapter === "codex" ||
@@ -67,7 +74,7 @@ export class ResumeSessionCoordinator {
67
74
  return {
68
75
  kind: "already_active",
69
76
  sessionId: candidate.sessionId,
70
- message: `${adapterDisplayName(this.adapter)} ${adapterSessionNoun(this.adapter)} ${candidate.sessionId.slice(0, 12)} is already active.`,
77
+ message: t("session.alreadyActive", { adapter: adapterDisplayName(this.adapter), noun: adapterSessionNoun(this.adapter), id: candidate.sessionId.slice(0, 12) }),
71
78
  };
72
79
  }
73
80
  await this.runtime.resumeSession(candidate.sessionId);
@@ -86,12 +93,12 @@ export class ResumeSessionCoordinator {
86
93
  if (/^\d+$/.test(target)) {
87
94
  const snapshot = this.getCurrentSnapshot();
88
95
  if (!snapshot) {
89
- throw new Error("The recent session list has expired. Send /resume again before choosing a number.");
96
+ throw new Error(t("session.list.expired"));
90
97
  }
91
98
  const index = Number(target) - 1;
92
99
  const candidate = snapshot.candidates[index];
93
100
  if (!candidate) {
94
- throw new Error(`Resume selection ${target} is outside the displayed range. Send /resume again to refresh the list.`);
101
+ throw new Error(t("session.list.range", { target }));
95
102
  }
96
103
  return candidate;
97
104
  }
@@ -122,7 +129,7 @@ export class ResumeSessionCoordinator {
122
129
  }
123
130
  const prefixMatches = candidates.filter((candidate) => candidate.sessionId.toLowerCase().startsWith(normalizedTarget));
124
131
  if (prefixMatches.length > 1) {
125
- throw new Error(`Session ID prefix ${target} is ambiguous. Send /resume again and use a longer ID prefix.`);
132
+ throw new Error(t("session.list.ambiguous", { target }));
126
133
  }
127
134
  return prefixMatches[0] ?? null;
128
135
  }
@@ -145,7 +152,7 @@ export class ResumeSessionCoordinator {
145
152
  if (/^Failed to list\s/i.test(message)) {
146
153
  throw error;
147
154
  }
148
- throw new Error(`Failed to list ${adapterDisplayName(this.adapter)} ${adapterSessionNoun(this.adapter)}s: ${message}`, { cause: error });
155
+ throw new Error(t("session.list.failed", { adapter: adapterDisplayName(this.adapter), noun: t(this.adapter === "codex" ? "session.threads" : "session.sessions"), message }), { cause: error });
149
156
  }
150
157
  }
151
158
  }
@@ -85,7 +85,9 @@ export function normalizeBridgeLockPayload(value) {
85
85
  startedAt: record.startedAt,
86
86
  lifecycle: record.lifecycle === "companion_bound" ? "companion_bound" : "persistent",
87
87
  legacyLifecycleFallback: hasExplicitLifecycle ? undefined : true,
88
- ...(record.channelId === "wechat" || record.channelId === "wecom"
88
+ ...(record.channelId === "wechat" ||
89
+ record.channelId === "wecom" ||
90
+ record.channelId === "telegram"
89
91
  ? { channelId: record.channelId }
90
92
  : {}),
91
93
  };