@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.1

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 (88) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +468 -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 +206 -14
  21. package/dist/bridge/bridge-adapters.core.js +25 -1
  22. package/dist/bridge/bridge-adapters.opencode.js +298 -20
  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 +145 -0
  32. package/dist/bridge/codex-visible-thread-proxy.js +140 -0
  33. package/dist/bridge/native-terminal-control.js +271 -0
  34. package/dist/bridge/opencode-v2-compat.js +450 -0
  35. package/dist/bridge/wechat-bridge.js +380 -171
  36. package/dist/bridge/wechat-forwarding.js +6 -0
  37. package/dist/bridge/wechat-outbound-queue.js +6 -0
  38. package/dist/channels/wechat/bridge-final-reply.js +13 -22
  39. package/dist/channels/wechat/index.js +6 -0
  40. package/dist/channels/wechat/wechat-driver.js +191 -0
  41. package/dist/channels/wechat/wechat-forwarding.js +18 -0
  42. package/dist/channels/wechat/wechat-outbound-queue.js +49 -3
  43. package/dist/channels/wecom/bridge-final-reply.js +32 -0
  44. package/dist/channels/wecom/setup-cli.js +12 -0
  45. package/dist/channels/wecom/setup.js +182 -0
  46. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  47. package/dist/channels/wecom/wecom-config.js +72 -0
  48. package/dist/channels/wecom/wecom-driver.js +106 -0
  49. package/dist/channels/wecom/wecom-media.js +147 -0
  50. package/dist/channels/wecom/wecom-message.js +149 -0
  51. package/dist/channels/wecom/wecom-transport.js +314 -0
  52. package/dist/commands/check-update.js +6 -0
  53. package/dist/companion/codex-remote-client.js +30 -3
  54. package/dist/companion/codex-visible-client-link.js +107 -0
  55. package/dist/companion/local-companion-link.js +6 -0
  56. package/dist/companion/local-companion-start.js +62 -11
  57. package/dist/companion/local-companion.js +25 -1
  58. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  59. package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
  60. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  61. package/dist/core/bridge-control.js +10 -0
  62. package/dist/core/bridge-defer.js +25 -0
  63. package/dist/core/channel-driver.js +1 -0
  64. package/dist/core/channel-types.js +3 -1
  65. package/dist/core/conversation-routing.js +58 -0
  66. package/dist/core/direct-run.js +23 -0
  67. package/dist/core/inbound-message-claims.js +67 -0
  68. package/dist/core/outbound-target.js +32 -0
  69. package/dist/core/text-utils.js +6 -0
  70. package/dist/core/turn-coordinator.js +85 -0
  71. package/dist/daemon/daemon-link.js +9 -0
  72. package/dist/daemon/emoji-bindings.js +6 -0
  73. package/dist/daemon/wechat-daemon.js +675 -122
  74. package/dist/i18n/index.js +6 -0
  75. package/dist/i18n/messages-en.js +9 -1
  76. package/dist/i18n/messages-zh.js +9 -1
  77. package/dist/runtime/create-runtime-host.js +6 -0
  78. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  79. package/dist/runtime/runtime-types.js +3 -0
  80. package/dist/utils/atomic-file.js +6 -0
  81. package/dist/utils/doctor.js +36 -10
  82. package/dist/utils/version-checker.js +17 -11
  83. package/dist/wechat/channel-config.js +6 -0
  84. package/dist/wechat/setup.js +119 -13
  85. package/dist/wechat/wechat-channel.js +6 -0
  86. package/dist/wechat/wechat-transport.js +169 -58
  87. package/package.json +16 -4
  88. 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");
@@ -0,0 +1,10 @@
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.
8
+ import { runJsEntry } from "./_run-entry.mjs";
9
+
10
+ runJsEntry("dist/commands/check-update.js");
@@ -0,0 +1,10 @@
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.
8
+ import { runJsEntry } from "./_run-entry.mjs";
9
+
10
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "claude", "--channel", "wecom"]);
@@ -0,0 +1,10 @@
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.
8
+ import { runJsEntry } from "./_run-entry.mjs";
9
+
10
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "codex", "--channel", "wecom"]);
@@ -0,0 +1,10 @@
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.
8
+ import { runJsEntry } from "./_run-entry.mjs";
9
+
10
+ runJsEntry("dist/daemon/wechat-daemon.js", ["--channel", "wecom"]);
@@ -0,0 +1,10 @@
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.
8
+ import { runJsEntry } from "./_run-entry.mjs";
9
+
10
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "opencode", "--channel", "wecom"]);
@@ -0,0 +1,10 @@
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.
8
+ import { runJsEntry } from "./_run-entry.mjs";
9
+
10
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "pi", "--channel", "wecom"]);
@@ -0,0 +1,10 @@
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.
8
+ import { runJsEntry } from "./_run-entry.mjs";
9
+
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";
@@ -18,6 +26,8 @@ const CLAUDE_COMPACT_DEDUP_MS = 2_000;
18
26
  const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
19
27
  const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
20
28
  const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
29
+ const CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS = [200, 500, 1_000];
30
+ const CLAUDE_INTERACTIVE_READY_TIMEOUT_MS = 10_000;
21
31
  const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
22
32
  const CLAUDE_SESSION_METADATA_READ_BYTES = 64 * 1024;
23
33
  const CLAUDE_MAX_SANITIZED_PROJECT_PATH = 200;
@@ -337,6 +347,26 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
337
347
  polledTranscriptSessionId = null;
338
348
  pendingResume = null;
339
349
  localResumeSessionEndObserved = false;
350
+ interactiveReady = true;
351
+ interactiveReadyTimer = null;
352
+ interactiveReadyWaiters = new Set();
353
+ remoteSubmitRetryTimer = null;
354
+ remoteSubmitRetryIndex = 0;
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); }
340
370
  constructor(options) {
341
371
  super(options);
342
372
  const shouldRestoreInitialSession = options.sessionStartMode !== "new";
@@ -364,6 +394,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
364
394
  if (this.pty) {
365
395
  return;
366
396
  }
397
+ this.beginInteractiveReadinessWait();
367
398
  this.startupOutputBuffer = "";
368
399
  this.hasAutoConfirmedWorkspaceTrustPrompt = false;
369
400
  ensureClaudeWorkspaceTrustAccepted(this.options.cwd);
@@ -385,16 +416,22 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
385
416
  this.state.activeRuntimeSessionId = undefined;
386
417
  }
387
418
  }
388
- await this.startHookServer();
389
419
  try {
420
+ await this.startHookServer();
390
421
  await super.start();
422
+ if (!this.interactiveReady) {
423
+ this.setStatus("starting", "Waiting for Claude interactive session...");
424
+ }
391
425
  }
392
426
  catch (error) {
427
+ this.rejectInteractiveReadiness(error instanceof Error ? error : new Error(String(error)));
393
428
  await this.stopHookServer();
394
429
  throw error;
395
430
  }
396
431
  }
397
432
  async sendInput(text) {
433
+ if (this.terminalControl.active)
434
+ throw new Error("A Claude control operation is in progress.");
398
435
  if (!this.pty) {
399
436
  throw new Error("claude adapter is not running.");
400
437
  }
@@ -404,7 +441,18 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
404
441
  if (this.pendingApproval) {
405
442
  throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
406
443
  }
444
+ await this.waitForInteractiveReadiness();
445
+ if (!this.pty) {
446
+ throw new Error("claude adapter is not running.");
447
+ }
448
+ if (this.getState().status === "busy") {
449
+ throw new Error("claude is still working. Wait for the current reply or use /stop.");
450
+ }
451
+ if (this.pendingApproval) {
452
+ throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
453
+ }
407
454
  const normalizedText = normalizeOutput(text).trim();
455
+ this.clearRemoteSubmitRetry();
408
456
  this.pendingInjectedInputs.push({
409
457
  normalizedText,
410
458
  createdAtMs: Date.now(),
@@ -421,12 +469,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
421
469
  this.writeToPty(this.buildRemoteInputPayload(text));
422
470
  await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
423
471
  this.writeToPty("\r");
472
+ this.armRemoteSubmitRetry(normalizedText);
424
473
  this.armWechatWorkingNotice();
425
474
  }
426
475
  async listResumeSessions(limit = 10) {
427
476
  return listClaudeResumeSessions(this.options.cwd, limit);
428
477
  }
429
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.");
430
481
  if (!this.pty) {
431
482
  throw new Error("Claude is not running yet.");
432
483
  }
@@ -523,6 +574,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
523
574
  return findClaudeResumeSessionFile(this.options.cwd, sessionId);
524
575
  }
525
576
  async interrupt() {
577
+ this.terminalControl.cancel("The remote control operation was interrupted.");
526
578
  if (!this.pty) {
527
579
  return false;
528
580
  }
@@ -530,6 +582,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
530
582
  return false;
531
583
  }
532
584
  this.clearWechatWorkingNotice(true);
585
+ this.clearRemoteSubmitRetry();
533
586
  this.pendingCliApprovalHints = null;
534
587
  this.flushPendingClaudeHookApprovals();
535
588
  if (this.pendingApproval) {
@@ -552,6 +605,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
552
605
  return true;
553
606
  }
554
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.");
555
610
  this.rejectPendingResume(new Error("Claude was reset during session switching."));
556
611
  this.clearWechatWorkingNotice(true);
557
612
  this.pendingCliApprovalHints = null;
@@ -566,7 +621,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
566
621
  this.state.lastSessionSwitchAt = undefined;
567
622
  this.state.lastSessionSwitchSource = undefined;
568
623
  this.state.lastSessionSwitchReason = undefined;
569
- await super.reset();
624
+ await this.dispose();
625
+ await this.start();
570
626
  }
571
627
  async resolveApproval(action) {
572
628
  if (!this.pendingApproval) {
@@ -620,9 +676,12 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
620
676
  return ok ? 1 : 0;
621
677
  }
622
678
  async dispose() {
679
+ this.terminalControl.cancel("Claude is shutting down.");
623
680
  this.rejectPendingResume(new Error("Claude is shutting down during session switching."));
624
681
  this.detachLocalTerminal();
625
682
  this.clearWechatWorkingNotice(true);
683
+ this.clearRemoteSubmitRetry();
684
+ this.rejectInteractiveReadiness(new Error("Claude is shutting down."));
626
685
  this.pendingCliApprovalHints = null;
627
686
  this.flushPendingClaudeHookApprovals();
628
687
  this.stopTranscriptThinkingWatch();
@@ -647,6 +706,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
647
706
  this.startHookHealthCheck();
648
707
  }
649
708
  handleData(rawText) {
709
+ this.terminalControl.feed(rawText);
650
710
  this.renderLocalOutput(rawText);
651
711
  const text = normalizeOutput(rawText);
652
712
  if (!text) {
@@ -718,10 +778,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
718
778
  return true;
719
779
  }
720
780
  handleExit(exitCode) {
781
+ this.terminalControl.cancel("Claude exited during the control operation.");
721
782
  this.detachLocalTerminal();
722
783
  this.clearWechatWorkingNotice(true);
784
+ this.clearRemoteSubmitRetry();
723
785
  this.pendingCliApprovalHints = null;
724
786
  this.rejectPendingResume(new Error("Claude exited before the requested session switch completed."));
787
+ if (!this.recoveringInvalidResume) {
788
+ this.rejectInteractiveReadiness(new Error("Claude exited before its interactive session became ready."));
789
+ }
725
790
  void this.stopHookServer();
726
791
  if (this.recoveringInvalidResume && !this.shuttingDown) {
727
792
  this.clearCompletionTimer();
@@ -870,6 +935,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
870
935
  }
871
936
  this.localTerminalInputListener = (chunk) => {
872
937
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
938
+ this.terminalControl.localInput(text);
873
939
  this.writeToPty(text);
874
940
  };
875
941
  process.stdin.on("data", this.localTerminalInputListener);
@@ -898,10 +964,11 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
898
964
  }
899
965
  }
900
966
  resizePtyToTerminal() {
901
- if (!this.pty || !process.stdout.isTTY) {
967
+ if (!this.pty) {
902
968
  return;
903
969
  }
904
970
  try {
971
+ this.terminalControl.resize(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
905
972
  this.pty.resize?.(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
906
973
  }
907
974
  catch {
@@ -996,6 +1063,91 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
996
1063
  }
997
1064
  return `${CLAUDE_BRACKETED_PASTE_START}${normalizedText}${CLAUDE_BRACKETED_PASTE_END}`;
998
1065
  }
1066
+ beginInteractiveReadinessWait() {
1067
+ this.clearInteractiveReadinessTimer();
1068
+ this.interactiveReady = false;
1069
+ this.interactiveReadyTimer = setTimeout(() => {
1070
+ this.interactiveReadyTimer = null;
1071
+ this.markInteractiveReady();
1072
+ }, CLAUDE_INTERACTIVE_READY_TIMEOUT_MS);
1073
+ this.interactiveReadyTimer.unref?.();
1074
+ }
1075
+ waitForInteractiveReadiness() {
1076
+ if (this.interactiveReady) {
1077
+ return Promise.resolve();
1078
+ }
1079
+ return new Promise((resolve, reject) => {
1080
+ this.interactiveReadyWaiters.add({ resolve, reject });
1081
+ });
1082
+ }
1083
+ markInteractiveReady() {
1084
+ if (this.interactiveReady) {
1085
+ return;
1086
+ }
1087
+ this.interactiveReady = true;
1088
+ this.clearInteractiveReadinessTimer();
1089
+ for (const waiter of this.interactiveReadyWaiters) {
1090
+ waiter.resolve();
1091
+ }
1092
+ this.interactiveReadyWaiters.clear();
1093
+ if (this.state.status === "starting" && !this.hasAcceptedInput) {
1094
+ this.setStatus("idle", "Claude interactive session is ready.");
1095
+ }
1096
+ }
1097
+ rejectInteractiveReadiness(error) {
1098
+ this.clearInteractiveReadinessTimer();
1099
+ for (const waiter of this.interactiveReadyWaiters) {
1100
+ waiter.reject(error);
1101
+ }
1102
+ this.interactiveReadyWaiters.clear();
1103
+ this.interactiveReady = true;
1104
+ }
1105
+ clearInteractiveReadinessTimer() {
1106
+ if (!this.interactiveReadyTimer) {
1107
+ return;
1108
+ }
1109
+ clearTimeout(this.interactiveReadyTimer);
1110
+ this.interactiveReadyTimer = null;
1111
+ }
1112
+ armRemoteSubmitRetry(normalizedText) {
1113
+ this.clearRemoteSubmitRetry();
1114
+ this.remoteSubmitText = normalizedText;
1115
+ this.remoteSubmitRetryIndex = 0;
1116
+ this.scheduleRemoteSubmitRetry();
1117
+ }
1118
+ scheduleRemoteSubmitRetry() {
1119
+ const delayMs = CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS[this.remoteSubmitRetryIndex];
1120
+ if (delayMs === undefined || !this.remoteSubmitText) {
1121
+ return;
1122
+ }
1123
+ this.remoteSubmitRetryTimer = setTimeout(() => {
1124
+ this.remoteSubmitRetryTimer = null;
1125
+ if (!this.remoteSubmitText ||
1126
+ !this.pty ||
1127
+ this.state.status !== "busy" ||
1128
+ !this.pendingInjectedInputs.some((input) => input.normalizedText === this.remoteSubmitText)) {
1129
+ this.clearRemoteSubmitRetry();
1130
+ return;
1131
+ }
1132
+ this.writeToPty("\r");
1133
+ this.remoteSubmitRetryIndex += 1;
1134
+ if (this.remoteSubmitRetryIndex >= CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS.length) {
1135
+ this.clearRemoteSubmitRetry();
1136
+ this.emitClaudeNotice("Claude did not confirm the WeChat message submission. Press Enter in the local Claude terminal to retry.", "warning");
1137
+ return;
1138
+ }
1139
+ this.scheduleRemoteSubmitRetry();
1140
+ }, delayMs);
1141
+ this.remoteSubmitRetryTimer.unref?.();
1142
+ }
1143
+ clearRemoteSubmitRetry() {
1144
+ if (this.remoteSubmitRetryTimer) {
1145
+ clearTimeout(this.remoteSubmitRetryTimer);
1146
+ this.remoteSubmitRetryTimer = null;
1147
+ }
1148
+ this.remoteSubmitRetryIndex = 0;
1149
+ this.remoteSubmitText = null;
1150
+ }
999
1151
  // Fallback heuristic for older Claude Code versions that lack PostCompact hooks.
1000
1152
  // The structured PostCompact hook event (handled in handleClaudeHookEnvelope) is
1001
1153
  // the reliable signal; this regex match serves as a best-effort fallback.
@@ -1044,6 +1196,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1044
1196
  }
1045
1197
  this.clearCompletionTimer();
1046
1198
  this.clearWechatWorkingNotice(true);
1199
+ this.clearRemoteSubmitRetry();
1047
1200
  this.pendingCliApprovalHints = null;
1048
1201
  this.flushPendingClaudeHookApprovals();
1049
1202
  this.pendingApproval = null;
@@ -1079,6 +1232,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1079
1232
  const completedPreview = this.currentPreview;
1080
1233
  this.clearCompletionTimer();
1081
1234
  this.clearWechatWorkingNotice(true);
1235
+ this.clearRemoteSubmitRetry();
1082
1236
  this.pendingCliApprovalHints = null;
1083
1237
  this.flushPendingClaudeHookApprovals();
1084
1238
  this.pendingApproval = null;
@@ -1168,6 +1322,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1168
1322
  }
1169
1323
  }
1170
1324
  handleClaudeSessionStart(payload) {
1325
+ this.markInteractiveReady();
1171
1326
  if (!payload.session_id) {
1172
1327
  return;
1173
1328
  }
@@ -1290,6 +1445,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1290
1445
  }
1291
1446
  const injectedIndex = findInjectedClaudePromptIndex(prompt, this.pendingInjectedInputs);
1292
1447
  if (injectedIndex >= 0) {
1448
+ this.clearRemoteSubmitRetry();
1293
1449
  this.pendingInjectedInputs.splice(injectedIndex, 1);
1294
1450
  return;
1295
1451
  }
@@ -1332,7 +1488,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1332
1488
  this.state.lastSessionSwitchReason = undefined;
1333
1489
  this.emitClaudeNotice(`Saved Claude conversation ${failedResumeConversationId} is no longer available. Starting a fresh Claude session.`, "warning");
1334
1490
  try {
1335
- await super.reset();
1491
+ await this.reset();
1336
1492
  }
1337
1493
  catch (error) {
1338
1494
  // recoverFromInvalidResume is invoked fire-and-forget (void) and there is
@@ -1441,6 +1597,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1441
1597
  this.currentPreview === "(idle)") {
1442
1598
  return;
1443
1599
  }
1600
+ this.clearRemoteSubmitRetry();
1444
1601
  this.clearWechatWorkingNotice(true);
1445
1602
  this.pendingCliApprovalHints = null;
1446
1603
  this.lastAutoApprovedPayload = null;