cli-wechat-bridge 1.1.7 → 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 (82) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +461 -434
  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 +6 -1
  12. package/bin/wecom-claude.mjs +6 -1
  13. package/bin/wecom-codex.mjs +6 -1
  14. package/bin/wecom-daemon.mjs +6 -1
  15. package/bin/wecom-opencode.mjs +6 -1
  16. package/bin/wecom-pi.mjs +6 -1
  17. package/bin/wecom-setup.mjs +6 -1
  18. package/dist/bridge/adapter-control.js +58 -0
  19. package/dist/bridge/bridge-adapters.claude.js +35 -1
  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 +6 -0
  28. package/dist/bridge/bridge-state.js +6 -0
  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 +250 -274
  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 +39 -0
  41. package/dist/channels/wecom/bridge-final-reply.js +6 -0
  42. package/dist/channels/wecom/setup-cli.js +6 -0
  43. package/dist/channels/wecom/setup.js +6 -0
  44. package/dist/channels/wecom/wecom-config.js +6 -0
  45. package/dist/channels/wecom/wecom-driver.js +106 -0
  46. package/dist/channels/wecom/wecom-media.js +6 -0
  47. package/dist/channels/wecom/wecom-transport.js +6 -0
  48. package/dist/commands/check-update.js +6 -0
  49. package/dist/companion/codex-remote-client.js +28 -2
  50. package/dist/companion/codex-visible-client-link.js +107 -0
  51. package/dist/companion/local-companion-link.js +6 -0
  52. package/dist/companion/local-companion-start.js +13 -2
  53. package/dist/companion/local-companion.js +23 -0
  54. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  55. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  56. package/dist/core/bridge-control.js +10 -0
  57. package/dist/core/bridge-defer.js +25 -0
  58. package/dist/core/channel-driver.js +1 -0
  59. package/dist/core/conversation-routing.js +58 -0
  60. package/dist/core/direct-run.js +6 -0
  61. package/dist/core/inbound-message-claims.js +6 -0
  62. package/dist/core/outbound-target.js +32 -0
  63. package/dist/core/text-utils.js +6 -0
  64. package/dist/core/turn-coordinator.js +85 -0
  65. package/dist/daemon/daemon-link.js +6 -0
  66. package/dist/daemon/emoji-bindings.js +6 -0
  67. package/dist/daemon/wechat-daemon.js +529 -205
  68. package/dist/i18n/index.js +6 -0
  69. package/dist/i18n/messages-en.js +7 -1
  70. package/dist/i18n/messages-zh.js +7 -1
  71. package/dist/runtime/create-runtime-host.js +6 -0
  72. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  73. package/dist/runtime/runtime-types.js +3 -0
  74. package/dist/utils/atomic-file.js +6 -0
  75. package/dist/utils/doctor.js +6 -0
  76. package/dist/utils/version-checker.js +17 -11
  77. package/dist/wechat/channel-config.js +6 -0
  78. package/dist/wechat/setup.js +6 -0
  79. package/dist/wechat/wechat-channel.js +6 -0
  80. package/dist/wechat/wechat-transport.js +169 -58
  81. package/package.json +108 -106
  82. package/scripts/ensure-node-pty-permissions.mjs +73 -73
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { spawn } from "node:child_process";
4
9
  import path from "node:path";
5
10
  import { fileURLToPath } from "node:url";
@@ -8,6 +13,8 @@ const BIN_DIR = path.dirname(fileURLToPath(import.meta.url));
8
13
  const PROJECT_DIR = path.resolve(BIN_DIR, "..");
9
14
  export const MIN_NODE_VERSION = Object.freeze([22, 13, 0]);
10
15
  export const MIN_NODE_VERSION_TEXT = MIN_NODE_VERSION.join(".");
16
+ export const MIN_PI_NODE_VERSION = Object.freeze([22, 19, 0]);
17
+ export const MIN_PI_NODE_VERSION_TEXT = MIN_PI_NODE_VERSION.join(".");
11
18
 
12
19
  export function isSupportedNodeVersion(version) {
13
20
  const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version);
@@ -24,19 +31,21 @@ export function isSupportedNodeVersion(version) {
24
31
  return true;
25
32
  }
26
33
 
27
- function ensureSupportedNodeVersion() {
34
+ function ensureSupportedNodeVersion(extraArgs = []) {
28
35
  if (process.env.CLI_BRIDGE_SKIP_NODE_CHECK === "1") {
29
36
  return;
30
37
  }
31
38
 
39
+ const piRequested = extraArgs.some((arg, index) => arg === "pi" && extraArgs[index - 1] === "--adapter");
32
40
  if (isSupportedNodeVersion(process.versions.node)) {
33
- return;
41
+ if (!piRequested || compareNodeVersion(process.versions.node, MIN_PI_NODE_VERSION) >= 0) return;
34
42
  }
35
43
 
44
+ const minimumText = piRequested ? MIN_PI_NODE_VERSION_TEXT : MIN_NODE_VERSION_TEXT;
36
45
  process.stderr.write(
37
46
  [
38
- `[cli-wechat-bridge] Node.js >= ${MIN_NODE_VERSION_TEXT} is required, but you are running ${process.version}.`,
39
- `[cli-wechat-bridge] 需要 Node.js >= ${MIN_NODE_VERSION_TEXT},当前版本为 ${process.version}。`,
47
+ `[cli-wechat-bridge] Node.js >= ${minimumText} is required, but you are running ${process.version}.`,
48
+ `[cli-wechat-bridge] 需要 Node.js >= ${minimumText},当前版本为 ${process.version}。`,
40
49
  "Install the latest LTS from https://nodejs.org/ (or via nvm), then retry.",
41
50
  "Set CLI_BRIDGE_SKIP_NODE_CHECK=1 to bypass this check at your own risk.",
42
51
  "",
@@ -45,8 +54,22 @@ function ensureSupportedNodeVersion() {
45
54
  process.exit(1);
46
55
  }
47
56
 
57
+ function compareNodeVersion(version, minimum) {
58
+ const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version);
59
+ if (!match) return -1;
60
+ const current = [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)];
61
+ for (let index = 0; index < minimum.length; index += 1) {
62
+ if (current[index] !== minimum[index]) return current[index] > minimum[index] ? 1 : -1;
63
+ }
64
+ return 0;
65
+ }
66
+
67
+ export function isSupportedPiNodeVersion(version) {
68
+ return compareNodeVersion(version, MIN_PI_NODE_VERSION) >= 0;
69
+ }
70
+
48
71
  export function runJsEntry(relativeEntryPath, extraArgs = []) {
49
- ensureSupportedNodeVersion();
72
+ ensureSupportedNodeVersion([...extraArgs, ...process.argv.slice(2)]);
50
73
  const entryPath = path.join(PROJECT_DIR, relativeEntryPath);
51
74
  const child = spawn(
52
75
  process.execPath,
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/commands/check-update.js", []);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "claude"]);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "codex"]);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/daemon/wechat-daemon.js");
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "opencode"]);
package/bin/wechat-pi.mjs CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "pi"]);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/wechat/setup.js");
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/commands/check-update.js");
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "claude", "--channel", "wecom"]);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "codex", "--channel", "wecom"]);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/daemon/wechat-daemon.js", ["--channel", "wecom"]);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "opencode", "--channel", "wecom"]);
package/bin/wecom-pi.mjs CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "pi", "--channel", "wecom"]);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
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.
3
8
  import { runJsEntry } from "./_run-entry.mjs";
4
9
 
5
10
  runJsEntry("dist/channels/wecom/setup-cli.js");
@@ -0,0 +1,58 @@
1
+ const snapshots = new WeakMap();
2
+ export const MODEL_SNAPSHOT_TTL_MS = 5 * 60_000;
3
+ export function invalidateModelSnapshot(adapter) {
4
+ snapshots.delete(adapter);
5
+ }
6
+ function snapshotOwner(adapter, senderId) {
7
+ const state = adapter.getState();
8
+ return JSON.stringify([
9
+ senderId, state.kind, state.cwd, state.pid, state.startedAt,
10
+ state.resumeConversationId ?? state.sharedSessionId ?? state.sharedThreadId,
11
+ state.lastSessionSwitchAt ?? state.lastThreadSwitchAt,
12
+ ]);
13
+ }
14
+ export async function handleAdapterControl(adapter, senderId, command) {
15
+ const kind = adapter.getState().kind;
16
+ const name = kind === "claude" ? "Claude Code" : kind === "opencode" ? "OpenCode" : kind === "codex" ? "Codex" : "Pi";
17
+ try {
18
+ if (command.type === "plan") {
19
+ if (!adapter.setPlanMode)
20
+ throw new Error(`Plan mode is not available for ${name}.`);
21
+ const enabled = await adapter.setPlanMode(command.enabled);
22
+ return enabled
23
+ ? `${name} plan mode enabled. Send /plan off to restore the previous mode.`
24
+ : `${name} plan mode disabled.`;
25
+ }
26
+ if (!adapter.listModels || !adapter.selectModel)
27
+ throw new Error(`Model selection is not available for ${name}.`);
28
+ const owner = snapshotOwner(adapter, senderId);
29
+ if (!command.target) {
30
+ snapshots.delete(adapter);
31
+ const models = await adapter.listModels();
32
+ if (snapshotOwner(adapter, senderId) !== owner)
33
+ throw new Error("The active session changed. Send /model again.");
34
+ if (!models.length)
35
+ return `${name} did not return any available models.`;
36
+ snapshots.set(adapter, { owner, createdAtMs: Date.now(), models });
37
+ return [
38
+ `Available ${name} models:`, "",
39
+ ...models.map((model, index) => `${index + 1}. ${model.displayName}${model.isCurrent ? " (current)" : ""}`),
40
+ "", "Reply with /model <number> to switch. The list is valid for 5 minutes.",
41
+ ].join("\n");
42
+ }
43
+ const snapshot = snapshots.get(adapter);
44
+ if (!snapshot || snapshot.owner !== owner || Date.now() - snapshot.createdAtMs > MODEL_SNAPSHOT_TTL_MS) {
45
+ throw new Error("The model list has expired or belongs to another session. Send /model again.");
46
+ }
47
+ const index = Number(command.target);
48
+ if (!/^\d+$/.test(command.target) || !Number.isSafeInteger(index) || index < 1 || index > snapshot.models.length) {
49
+ throw new Error("Model selection is outside the displayed range. Send /model again to refresh the list.");
50
+ }
51
+ const model = await adapter.selectModel(snapshot.models[index - 1].id);
52
+ snapshots.delete(adapter);
53
+ return `${name} model switched to ${model.displayName}.`;
54
+ }
55
+ catch (error) {
56
+ return error instanceof Error ? error.message : String(error);
57
+ }
58
+ }
@@ -1,3 +1,11 @@
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 { NativeTerminalControl, readClaudePermissionMode } from "./native-terminal-control.js";
8
+ import { ClaudeNativeControls } from "./claude-native-controls.js";
1
9
  import fs from "node:fs";
2
10
  import net from "node:net";
3
11
  import os from "node:os";
@@ -345,6 +353,20 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
345
353
  remoteSubmitRetryTimer = null;
346
354
  remoteSubmitRetryIndex = 0;
347
355
  remoteSubmitText = null;
356
+ terminalControl = new NativeTerminalControl({
357
+ write: (text) => this.writeToPty(text),
358
+ identity: () => JSON.stringify([this.state.pid, this.runtimeSessionId, this.resumeConversationId]),
359
+ isPromptEmpty: () => readClaudePermissionMode(this.terminalControl.text) !== null,
360
+ assertReady: () => {
361
+ if (!this.pty || this.state.status !== "idle" || this.pendingApproval || this.pendingResume) {
362
+ throw new Error("Claude must be idle with no pending approval before using remote controls.");
363
+ }
364
+ },
365
+ });
366
+ nativeControls = new ClaudeNativeControls(this.terminalControl, () => this.resumeConversationId ?? this.runtimeSessionId ?? "");
367
+ async listModels() { return this.nativeControls.listModels(); }
368
+ async selectModel(modelId) { return this.nativeControls.selectModel(modelId); }
369
+ async setPlanMode(enabled) { return this.nativeControls.setPlanMode(enabled); }
348
370
  constructor(options) {
349
371
  super(options);
350
372
  const shouldRestoreInitialSession = options.sessionStartMode !== "new";
@@ -408,6 +430,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
408
430
  }
409
431
  }
410
432
  async sendInput(text) {
433
+ if (this.terminalControl.active)
434
+ throw new Error("A Claude control operation is in progress.");
411
435
  if (!this.pty) {
412
436
  throw new Error("claude adapter is not running.");
413
437
  }
@@ -452,6 +476,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
452
476
  return listClaudeResumeSessions(this.options.cwd, limit);
453
477
  }
454
478
  async resumeSession(sessionId) {
479
+ if (this.terminalControl.active)
480
+ throw new Error("A CLI control operation is in progress. Wait for it to finish before switching sessions.");
455
481
  if (!this.pty) {
456
482
  throw new Error("Claude is not running yet.");
457
483
  }
@@ -548,6 +574,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
548
574
  return findClaudeResumeSessionFile(this.options.cwd, sessionId);
549
575
  }
550
576
  async interrupt() {
577
+ this.terminalControl.cancel("The remote control operation was interrupted.");
551
578
  if (!this.pty) {
552
579
  return false;
553
580
  }
@@ -578,6 +605,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
578
605
  return true;
579
606
  }
580
607
  async reset() {
608
+ if (this.terminalControl.active)
609
+ throw new Error("A CLI control operation is in progress. Wait for it to finish before switching sessions.");
581
610
  this.rejectPendingResume(new Error("Claude was reset during session switching."));
582
611
  this.clearWechatWorkingNotice(true);
583
612
  this.pendingCliApprovalHints = null;
@@ -647,6 +676,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
647
676
  return ok ? 1 : 0;
648
677
  }
649
678
  async dispose() {
679
+ this.terminalControl.cancel("Claude is shutting down.");
650
680
  this.rejectPendingResume(new Error("Claude is shutting down during session switching."));
651
681
  this.detachLocalTerminal();
652
682
  this.clearWechatWorkingNotice(true);
@@ -676,6 +706,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
676
706
  this.startHookHealthCheck();
677
707
  }
678
708
  handleData(rawText) {
709
+ this.terminalControl.feed(rawText);
679
710
  this.renderLocalOutput(rawText);
680
711
  const text = normalizeOutput(rawText);
681
712
  if (!text) {
@@ -747,6 +778,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
747
778
  return true;
748
779
  }
749
780
  handleExit(exitCode) {
781
+ this.terminalControl.cancel("Claude exited during the control operation.");
750
782
  this.detachLocalTerminal();
751
783
  this.clearWechatWorkingNotice(true);
752
784
  this.clearRemoteSubmitRetry();
@@ -903,6 +935,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
903
935
  }
904
936
  this.localTerminalInputListener = (chunk) => {
905
937
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
938
+ this.terminalControl.localInput(text);
906
939
  this.writeToPty(text);
907
940
  };
908
941
  process.stdin.on("data", this.localTerminalInputListener);
@@ -931,10 +964,11 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
931
964
  }
932
965
  }
933
966
  resizePtyToTerminal() {
934
- if (!this.pty || !process.stdout.isTTY) {
967
+ if (!this.pty) {
935
968
  return;
936
969
  }
937
970
  try {
971
+ this.terminalControl.resize(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
938
972
  this.pty.resize?.(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
939
973
  }
940
974
  catch {
@@ -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 path from "node:path";
3
9
  import crypto from "node:crypto";
@@ -7,7 +13,8 @@ import { normalizeOutput, nowIso, summarizeOutput, truncatePreview, } from "../c
7
13
  import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
8
14
  import { killProcessTreeSync } from "./bridge-process-reaper.js";
9
15
  import * as shared from "./bridge-adapters.shared.js";
10
- import { requestCodexVisibleThreadSwitch } from "../companion/codex-visible-client-link.js";
16
+ import { requestCodexVisibleClientShutdown, requestCodexVisibleThreadSwitch, } from "../companion/codex-visible-client-link.js";
17
+ import { readLocalCompanionEndpoint } from "../companion/local-companion-link.js";
11
18
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
12
19
  import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
13
20
  export function parseCodexCliVersion(output) {
@@ -21,7 +28,7 @@ export function isCodexVersionInCompatibilityRange(version) {
21
28
  }
22
29
  const major = parts[0] ?? -1;
23
30
  const minor = parts[1] ?? -1;
24
- return major === 0 && minor >= 149 && minor <= 151;
31
+ return major === 0 && minor >= 149 && minor <= 155;
25
32
  }
26
33
  const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexDynamicToolCallFailureResponse, buildCodexMcpServerElicitationDeclineResponse, buildCodexPermissionsRequestApprovalResponse, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getCodexApprovalAutoResponse, getCodexWechatOutboundAttachmentDenyMessage, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
27
34
  const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
@@ -78,6 +85,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
78
85
  appServerAuthToken = null;
79
86
  appServerAuthTokenFilePath = null;
80
87
  rpcSocket = null;
88
+ lastRpcCloseDetail = "";
81
89
  rpcShuttingDown = false;
82
90
  rpcReconnectPromise = null;
83
91
  cleanPanelExitInProgress = false;
@@ -86,6 +94,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
86
94
  subscribedThreadIds = new Set();
87
95
  pendingThreadSubscriptions = new Map();
88
96
  sharedThreadId = null;
97
+ previousCollaborationMode = null;
89
98
  announcedThreadId = null;
90
99
  localThreadFollowBlockedUntilMs = 0;
91
100
  pendingThreadStatusChecks = new Map();
@@ -182,6 +191,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
182
191
  throw err;
183
192
  }
184
193
  }
194
+ async prepareVisibleClientSession() {
195
+ if (this.isNativePanelMode() || this.sharedThreadId) {
196
+ return Boolean(this.sharedThreadId);
197
+ }
198
+ const threadId = await this.ensureThreadStarted();
199
+ return Boolean(threadId);
200
+ }
185
201
  buildSpawnArgs() {
186
202
  if (!this.appServerPort) {
187
203
  throw new Error("Codex app-server is not ready.");
@@ -248,6 +264,74 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
248
264
  }
249
265
  await this.resumeVisibleSharedThread(threadId);
250
266
  }
267
+ async listModels() {
268
+ if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
269
+ throw new Error("Codex model selection requires an active app-server thread.");
270
+ }
271
+ const response = await this.sendRpcRequest("model/list", { includeHidden: false });
272
+ const data = isRecord(response) && Array.isArray(response.data) ? response.data : [];
273
+ const current = await this.readCodexThreadSettings();
274
+ const models = data.flatMap((item) => {
275
+ if (!isRecord(item))
276
+ return [];
277
+ const id = typeof item.id === "string" ? item.id : typeof item.model === "string" ? item.model : "";
278
+ if (!id)
279
+ return [];
280
+ const displayName = typeof item.displayName === "string" ? item.displayName : id;
281
+ const efforts = Array.isArray(item.supportedReasoningEfforts)
282
+ ? item.supportedReasoningEfforts.filter((value) => typeof value === "string")
283
+ : undefined;
284
+ return [{ id, displayName, isCurrent: id === current.model, supportedReasoningEfforts: efforts }];
285
+ });
286
+ return models;
287
+ }
288
+ async selectModel(modelId) {
289
+ const models = await this.listModels();
290
+ const selected = models.find((model) => model.id === modelId);
291
+ if (!selected)
292
+ throw new Error(`Codex model ${modelId} is not available.`);
293
+ await this.sendRpcRequest("thread/settings/update", { threadId: this.sharedThreadId, model: selected.id });
294
+ return selected;
295
+ }
296
+ async setPlanMode(enabled) {
297
+ if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
298
+ throw new Error("Codex plan mode requires an active app-server thread.");
299
+ }
300
+ const settings = await this.readCodexThreadSettings();
301
+ if (enabled) {
302
+ const response = await this.sendRpcRequest("collaborationMode/list", {});
303
+ const modes = isRecord(response) && Array.isArray(response.data) ? response.data : [];
304
+ const plan = modes.find((item) => isRecord(item) && (item.mode === "plan" || item.name?.toString().toLowerCase() === "plan"));
305
+ if (!isRecord(plan))
306
+ throw new Error("Codex plan mode is not available in this app-server.");
307
+ this.previousCollaborationMode = isRecord(settings.collaborationMode) ? settings.collaborationMode : null;
308
+ const mode = typeof plan.mode === "string" ? plan.mode : "plan";
309
+ const model = typeof plan.model === "string" ? plan.model : settings.model;
310
+ const effort = plan.reasoning_effort ?? plan.reasoningEffort ?? null;
311
+ await this.sendRpcRequest("thread/settings/update", {
312
+ threadId: this.sharedThreadId,
313
+ collaborationMode: { mode, settings: { model, reasoning_effort: effort, developer_instructions: null } },
314
+ });
315
+ return true;
316
+ }
317
+ const previous = this.previousCollaborationMode ?? {
318
+ mode: "default",
319
+ settings: { model: settings.model, reasoning_effort: settings.effort ?? null, developer_instructions: null },
320
+ };
321
+ await this.sendRpcRequest("thread/settings/update", {
322
+ threadId: this.sharedThreadId,
323
+ collaborationMode: previous,
324
+ });
325
+ this.previousCollaborationMode = null;
326
+ return false;
327
+ }
328
+ async readCodexThreadSettings() {
329
+ const response = await this.sendRpcRequest("thread/read", { threadId: this.sharedThreadId, includeTurns: false });
330
+ const thread = isRecord(response) && isRecord(response.thread) ? response.thread : null;
331
+ if (!thread || typeof thread.model !== "string")
332
+ throw new Error("Codex did not return the current thread model.");
333
+ return { model: thread.model, effort: thread.reasoningEffort, collaborationMode: thread.collaborationMode };
334
+ }
251
335
  async interrupt() {
252
336
  if (this.usesRpcTurnTransport()) {
253
337
  return await this.interruptPanelTurn();
@@ -315,6 +399,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
315
399
  return true;
316
400
  }
317
401
  async dispose() {
402
+ await this.shutdownVisibleClient();
318
403
  this.resetTurnTracking({ preserveThread: false });
319
404
  if (this.isEmbeddedCliMode()) {
320
405
  this.detachLocalInputForwarding();
@@ -346,6 +431,25 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
346
431
  }
347
432
  await this.stopAppServer();
348
433
  }
434
+ async shutdownVisibleClient() {
435
+ const endpoint = readLocalCompanionEndpoint(this.options.cwd, { adapter: "codex" });
436
+ if (!endpoint?.companionPid ||
437
+ endpoint.instanceId !== this.localClientInstanceId ||
438
+ !endpoint.codexControlPort ||
439
+ !endpoint.codexControlToken) {
440
+ return;
441
+ }
442
+ try {
443
+ await requestCodexVisibleClientShutdown({
444
+ cwd: this.options.cwd,
445
+ instanceId: endpoint.instanceId,
446
+ });
447
+ }
448
+ catch {
449
+ // The visible client may already be exiting. Its endpoint is cleared
450
+ // by the supervisor when the child process finishes.
451
+ }
452
+ }
349
453
  getLocalClientEndpoint() {
350
454
  if (!this.isHeadlessRuntimeMode() || !this.appServerPort || !this.appServerAuthToken) {
351
455
  return null;
@@ -1192,8 +1296,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1192
1296
  socket.addEventListener("message", (event) => {
1193
1297
  this.handleRpcMessageData(event.data);
1194
1298
  });
1195
- socket.addEventListener("close", () => {
1196
- this.handleRpcSocketClosed();
1299
+ socket.addEventListener("close", (event) => {
1300
+ this.handleRpcSocketClosed(event.code, event.reason);
1197
1301
  });
1198
1302
  }
1199
1303
  async disconnectRpcClient() {
@@ -1231,13 +1335,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1231
1335
  });
1232
1336
  this.rpcShuttingDown = false;
1233
1337
  }
1234
- handleRpcSocketClosed() {
1338
+ handleRpcSocketClosed(code, reason) {
1235
1339
  const expectedShutdown = shouldSuppressCodexTransportFatalError({
1236
1340
  transportShuttingDown: this.rpcShuttingDown,
1237
1341
  shuttingDown: this.shuttingDown,
1238
1342
  cleanPanelExitInProgress: this.cleanPanelExitInProgress,
1239
1343
  });
1240
1344
  this.rpcSocket = null;
1345
+ this.lastRpcCloseDetail = `close_code=${code ?? "unknown"}${reason ? ` reason=${truncatePreview(reason, 240)}` : ""}`;
1241
1346
  this.subscribedThreadIds.clear();
1242
1347
  this.rejectPendingRpcRequests("Codex websocket connection closed.");
1243
1348
  this.rpcShuttingDown = false;
@@ -1269,7 +1374,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1269
1374
  const details = this.describeAppServerLog();
1270
1375
  this.emit({
1271
1376
  type: "fatal_error",
1272
- message: `codex app-server websocket closed unexpectedly.${details}`,
1377
+ message: `codex app-server websocket closed unexpectedly (${this.lastRpcCloseDetail || "no close details"}).${details}`,
1273
1378
  timestamp: nowIso(),
1274
1379
  });
1275
1380
  this.terminateCodexClient();
@@ -1307,7 +1412,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1307
1412
  }
1308
1413
  this.emit({
1309
1414
  type: "fatal_error",
1310
- message: `codex app-server websocket closed unexpectedly and could not reconnect: ${lastError}.${details}`,
1415
+ message: `codex app-server websocket closed unexpectedly and could not reconnect (${this.lastRpcCloseDetail || "no close details"}): ${lastError}.${details}`,
1311
1416
  timestamp: nowIso(),
1312
1417
  });
1313
1418
  this.terminateCodexClient();
@@ -1460,6 +1565,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1460
1565
  if (typeof thread.parentThreadId === "string") {
1461
1566
  throw new Error(`Codex thread ${targetThreadId} is a subagent thread and cannot be resumed.`);
1462
1567
  }
1568
+ if (thread.canAcceptDirectInput === false) {
1569
+ throw new Error(`Codex thread ${targetThreadId} is owned by a parent agent and cannot accept direct input.`);
1570
+ }
1463
1571
  if (thread.ephemeral === true) {
1464
1572
  throw new Error(`Codex thread ${targetThreadId} is ephemeral and cannot be resumed.`);
1465
1573
  }
@@ -1483,6 +1591,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1483
1591
  if (typeof thread.parentThreadId === "string") {
1484
1592
  throw new Error(`Codex thread ${targetThreadId} is a subagent thread.`);
1485
1593
  }
1594
+ if (thread.canAcceptDirectInput === false) {
1595
+ throw new Error(`Codex thread ${targetThreadId} is owned by a parent agent.`);
1596
+ }
1486
1597
  if (thread.ephemeral === true) {
1487
1598
  throw new Error(`Codex thread ${targetThreadId} is ephemeral.`);
1488
1599
  }