@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
@@ -0,0 +1,33 @@
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 { getLocale, parseLocale, setLocale, t } from "../i18n/index.js";
8
+ import { writeJsonFileAtomic } from "../utils/atomic-file.js";
9
+ let languageChangeChain = Promise.resolve();
10
+ /** Language changes never touch turn ownership or pending interactions. */
11
+ export function handleLanguageControl(params) {
12
+ const run = languageChangeChain.then(() => applyLanguageControl(params));
13
+ languageChangeChain = run.catch(() => undefined);
14
+ return run;
15
+ }
16
+ async function applyLanguageControl(params) {
17
+ if (!params.target)
18
+ return t("language.current", { locale: getLocale() });
19
+ const locale = parseLocale(params.target);
20
+ if (!locale)
21
+ return t("language.usage");
22
+ try {
23
+ writeJsonFileAtomic(params.settingsFile, { locale });
24
+ }
25
+ catch (error) {
26
+ return t("language.saveFailed", { error: error instanceof Error ? error.message : String(error) });
27
+ }
28
+ setLocale(locale);
29
+ process.env.CLI_BRIDGE_LANG = locale;
30
+ const results = await Promise.allSettled((params.adapters ?? []).map((adapter) => adapter.updateLocale?.(locale) ?? true));
31
+ const needsRestart = results.some((result) => result.status === "rejected" || result.value === false);
32
+ return t("language.changed", { locale }) + (needsRestart ? `\n${t("language.companionRestart")}` : "");
33
+ }
@@ -5,6 +5,7 @@
5
5
  // must be released under AGPL-3.0-or-later with full source code; see
6
6
  // LICENSE.txt. Network services built on it must offer source to users.
7
7
  import xterm from "@xterm/headless";
8
+ import { t } from "../i18n/index.js";
8
9
  const { Terminal } = xterm;
9
10
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
11
  /** A screen mirror, never a second input source: only explicit control operations write keys. */
@@ -117,20 +118,20 @@ export class NativeTerminalControl {
117
118
  check() {
118
119
  const operation = this.operation;
119
120
  if (!operation)
120
- throw new Error("No terminal control operation is active.");
121
+ throw new Error(t("native.control.inactive"));
121
122
  if (operation.error)
122
123
  throw operation.error;
123
124
  if (operation.identity !== this.options.identity()) {
124
- operation.error = new Error("The active CLI session changed during control. Try again.");
125
+ operation.error = new Error(t("native.control.sessionChanged"));
125
126
  throw operation.error;
126
127
  }
127
128
  this.options.assertReady();
128
129
  if (Date.now() > operation.deadline)
129
- throw new Error("Native CLI control timed out; the result could not be confirmed.");
130
+ throw new Error(t("native.control.timeout"));
130
131
  }
131
132
  async run(action) {
132
133
  if (this.operation)
133
- throw new Error("Another CLI control operation is in progress.");
134
+ throw new Error(t("native.control.busy"));
134
135
  this.options.assertReady();
135
136
  this.operation = { identity: this.options.identity(), deadline: Date.now() + 25_000 };
136
137
  try {
@@ -143,7 +144,7 @@ export class NativeTerminalControl {
143
144
  else if (this.draft && this.options.isPromptEmpty?.())
144
145
  this.draft = false;
145
146
  if (this.draft)
146
- throw new Error("The local terminal has an input draft. Submit or clear it before using remote controls.");
147
+ throw new Error(t("native.control.draft"));
147
148
  const result = await action();
148
149
  this.check();
149
150
  return result;
@@ -183,7 +184,7 @@ export class NativeTerminalControl {
183
184
  }
184
185
  async command(command) {
185
186
  if (!/^\/[a-z]+$/.test(command))
186
- throw new Error("Invalid native control command.");
187
+ throw new Error(t("native.control.invalid"));
187
188
  await this.key(`\u001b[200~${command}\u001b[201~`);
188
189
  await this.key("\r");
189
190
  // Claude can consume the first Enter to accept a slash completion while its
@@ -211,7 +212,7 @@ export class NativeTerminalControl {
211
212
  }
212
213
  async openMenu(command, title) {
213
214
  if (this.ownedMenu || /Select model|Select agent|Select variant/.test(this.text)) {
214
- throw new Error("A native CLI menu is already open. Close it before using remote controls.");
215
+ throw new Error(t("native.control.menuOpen"));
215
216
  }
216
217
  await this.command(command);
217
218
  await this.waitFor(() => this.hasMenu(title));
@@ -220,7 +221,7 @@ export class NativeTerminalControl {
220
221
  async closeMenu() {
221
222
  const title = this.ownedMenu;
222
223
  if (!title || !this.hasMenu(title))
223
- throw new Error("The native selector changed unexpectedly.");
224
+ throw new Error(t("native.control.selectorChanged"));
224
225
  await this.key("\u001b");
225
226
  await this.waitFor(() => !this.hasMenu(title));
226
227
  this.ownedMenu = null;
@@ -228,7 +229,7 @@ export class NativeTerminalControl {
228
229
  async submitMenu(nextTitle, key = "\r") {
229
230
  const title = this.ownedMenu;
230
231
  if (!title || !this.hasMenu(title))
231
- throw new Error("The native selector changed unexpectedly.");
232
+ throw new Error(t("native.control.selectorChanged"));
232
233
  await this.key(key);
233
234
  await this.waitFor(() => !this.hasMenu(title));
234
235
  this.ownedMenu = nextTitle && this.hasMenu(nextTitle) ? nextTitle : null;
@@ -260,7 +261,7 @@ export function parseClaudeModelSwitchConfirmation(text) {
260
261
  export function readClaudePermissionMode(text) {
261
262
  const lines = text.split("\n");
262
263
  const prompt = lines.findLastIndex((line) => /^\s*❯/.test(line));
263
- if (prompt < 0 || !/^\s*❯\s*$/.test(lines[prompt]))
264
+ if (prompt < 0 || !isClaudePromptVisuallyEmpty(lines[prompt]))
264
265
  return null;
265
266
  const footer = lines.slice(prompt + 1).join("\n");
266
267
  for (const [label, mode] of [["plan mode", "plan"], ["accept edits", "acceptEdits"], ["bypass permissions", "bypassPermissions"], ["auto", "auto"], ["don't ask", "dontAsk"]]) {
@@ -269,3 +270,18 @@ export function readClaudePermissionMode(text) {
269
270
  }
270
271
  return "default";
271
272
  }
273
+ /**
274
+ * Claude renders idle decorations inside the input box: an automatic context
275
+ * chip like `[⧉ In file.ts]` and a suggestion placeholder like
276
+ * Try "how does <filepath> work?". Both appear on a freshly started session
277
+ * before any user input; neither is a draft. Real typed input still fails
278
+ * this check (and the local-input mirror blocks controls independently).
279
+ */
280
+ function isClaudePromptVisuallyEmpty(line) {
281
+ const body = line
282
+ .replace(/^\s*❯\s*/, "")
283
+ .replace(/^\[⧉ [^\]]*\]\s*/, "")
284
+ .replace(/^Try "[^"]*"\s*$/, "")
285
+ .replace(/\s+$/, "");
286
+ return body.length === 0;
287
+ }
@@ -110,9 +110,14 @@ function normalizeFormQuestions(form) {
110
110
  });
111
111
  }
112
112
  function normalizeFormAnswer(field, values) {
113
+ const options = Array.isArray(field.options) ? field.options.filter(isRecord) : [];
114
+ const normalizedValues = values.map((value) => {
115
+ const option = options.find((option) => option.label === value);
116
+ return typeof option?.value === "string" ? option.value : value;
117
+ });
113
118
  if (field.type === "multiselect")
114
- return values;
115
- const value = values[0] ?? "";
119
+ return normalizedValues;
120
+ const value = normalizedValues[0] ?? "";
116
121
  if (field.type === "boolean") {
117
122
  return /^(1|true|yes|y)$/i.test(String(value));
118
123
  }
@@ -407,7 +412,10 @@ export async function createOpenCodeV2CompatClient(options) {
407
412
  sessionID: input.sessionID ?? permissionSessions.get(String(input.requestID)),
408
413
  requestID: input.requestID,
409
414
  decision: input.reply === "reject" ? "reject" : input.reply,
410
- }, requestOptions).finally(() => permissionSessions.delete(String(input.requestID)))),
415
+ }, requestOptions).then((response) => {
416
+ permissionSessions.delete(String(input.requestID));
417
+ return response;
418
+ })),
411
419
  },
412
420
  question: {
413
421
  reply: (input, requestOptions) => result(async () => {