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.
- package/LICENSE.txt +661 -661
- package/README.md +461 -434
- package/bin/_run-entry.mjs +29 -6
- package/bin/wechat-check-update.mjs +6 -1
- package/bin/wechat-claude.mjs +6 -1
- package/bin/wechat-codex.mjs +6 -1
- package/bin/wechat-daemon.mjs +6 -1
- package/bin/wechat-opencode.mjs +6 -1
- package/bin/wechat-pi.mjs +6 -1
- package/bin/wechat-setup.mjs +6 -1
- package/bin/wecom-check-update.mjs +6 -1
- package/bin/wecom-claude.mjs +6 -1
- package/bin/wecom-codex.mjs +6 -1
- package/bin/wecom-daemon.mjs +6 -1
- package/bin/wecom-opencode.mjs +6 -1
- package/bin/wecom-pi.mjs +6 -1
- package/bin/wecom-setup.mjs +6 -1
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +35 -1
- package/dist/bridge/bridge-adapters.codex.js +118 -7
- package/dist/bridge/bridge-adapters.core.js +25 -1
- package/dist/bridge/bridge-adapters.opencode.js +156 -1
- package/dist/bridge/bridge-adapters.pi.js +45 -0
- package/dist/bridge/bridge-adapters.shared.js +6 -0
- package/dist/bridge/bridge-controller.js +6 -0
- package/dist/bridge/bridge-final-reply.js +6 -0
- package/dist/bridge/bridge-process-reaper.js +6 -0
- package/dist/bridge/bridge-state.js +6 -0
- package/dist/bridge/bridge-utils.js +6 -31
- package/dist/bridge/claude-hook.js +6 -0
- package/dist/bridge/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +259 -0
- package/dist/bridge/wechat-bridge.js +250 -274
- package/dist/bridge/wechat-forwarding.js +6 -0
- package/dist/bridge/wechat-outbound-queue.js +6 -0
- package/dist/channels/wechat/bridge-final-reply.js +13 -22
- package/dist/channels/wechat/index.js +6 -0
- package/dist/channels/wechat/wechat-driver.js +191 -0
- package/dist/channels/wechat/wechat-forwarding.js +18 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +39 -0
- package/dist/channels/wecom/bridge-final-reply.js +6 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +6 -0
- package/dist/channels/wecom/wecom-config.js +6 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +6 -0
- package/dist/channels/wecom/wecom-transport.js +6 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +28 -2
- package/dist/companion/codex-visible-client-link.js +107 -0
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion-start.js +13 -2
- package/dist/companion/local-companion.js +23 -0
- package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
- package/dist/companion/pi-tui-bridge-extension.js +62 -3
- package/dist/core/bridge-control.js +10 -0
- package/dist/core/bridge-defer.js +25 -0
- package/dist/core/channel-driver.js +1 -0
- package/dist/core/conversation-routing.js +58 -0
- package/dist/core/direct-run.js +6 -0
- package/dist/core/inbound-message-claims.js +6 -0
- package/dist/core/outbound-target.js +32 -0
- package/dist/core/text-utils.js +6 -0
- package/dist/core/turn-coordinator.js +85 -0
- package/dist/daemon/daemon-link.js +6 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +529 -205
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +7 -1
- package/dist/i18n/messages-zh.js +7 -1
- package/dist/runtime/create-runtime-host.js +6 -0
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/atomic-file.js +6 -0
- package/dist/utils/doctor.js +6 -0
- package/dist/utils/version-checker.js +17 -11
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +6 -0
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +169 -58
- package/package.json +108 -106
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -0,0 +1,259 @@
|
|
|
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 xterm from "@xterm/headless";
|
|
8
|
+
const { Terminal } = xterm;
|
|
9
|
+
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
/** A screen mirror, never a second input source: only explicit control operations write keys. */
|
|
11
|
+
export class NativeTerminalControl {
|
|
12
|
+
terminal = new Terminal({ cols: 120, rows: 40, allowProposedApi: true, scrollback: 0, logLevel: "off" });
|
|
13
|
+
writes = Promise.resolve();
|
|
14
|
+
lastFrameAt = 0;
|
|
15
|
+
operation = null;
|
|
16
|
+
ownedMenu = null;
|
|
17
|
+
draft = false;
|
|
18
|
+
inputSequence = "";
|
|
19
|
+
inputEscapeTimer;
|
|
20
|
+
pasting = false;
|
|
21
|
+
options;
|
|
22
|
+
constructor(options) { this.options = options; }
|
|
23
|
+
get active() { return this.operation !== null; }
|
|
24
|
+
feed(text) {
|
|
25
|
+
this.writes = this.writes.then(() => new Promise((resolve) => this.terminal.write(text, () => {
|
|
26
|
+
this.lastFrameAt = Date.now();
|
|
27
|
+
resolve();
|
|
28
|
+
})));
|
|
29
|
+
}
|
|
30
|
+
resize(cols, rows) {
|
|
31
|
+
this.terminal.resize(Math.max(20, cols), Math.max(8, rows));
|
|
32
|
+
}
|
|
33
|
+
localInput(text) {
|
|
34
|
+
// stdin also carries terminal replies (colors, capabilities, cursor position).
|
|
35
|
+
// Parse across chunks before deciding that anything came from the keyboard.
|
|
36
|
+
// The adapter still forwards every original byte to the child PTY unchanged.
|
|
37
|
+
for (const char of text) {
|
|
38
|
+
clearTimeout(this.inputEscapeTimer);
|
|
39
|
+
if (!this.inputSequence) {
|
|
40
|
+
if (char !== "\u001b") {
|
|
41
|
+
this.keyboardInput(char);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
this.inputSequence = char;
|
|
45
|
+
// A lone Escape is a key; a following introducer belongs to a sequence.
|
|
46
|
+
this.inputEscapeTimer = setTimeout(() => {
|
|
47
|
+
this.inputSequence = "";
|
|
48
|
+
this.keyboardInput("\u001b");
|
|
49
|
+
}, 50);
|
|
50
|
+
this.inputEscapeTimer.unref?.();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
this.inputSequence += char;
|
|
54
|
+
const sequence = this.inputSequence;
|
|
55
|
+
if (sequence.length === 2 && "[]P_^XO".includes(char))
|
|
56
|
+
continue;
|
|
57
|
+
const kind = sequence[1];
|
|
58
|
+
if (kind === "[" && !/[@-~]/.test(char))
|
|
59
|
+
continue;
|
|
60
|
+
if (kind && "]P_^X".includes(kind) && char !== "\u0007" && !sequence.endsWith("\u001b\\")) {
|
|
61
|
+
// Bound malformed control strings without treating their payload as text.
|
|
62
|
+
if (sequence.length > 65_536)
|
|
63
|
+
this.inputSequence = sequence.slice(0, 2);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
this.inputSequence = "";
|
|
67
|
+
if (kind && "]P_^X".includes(kind))
|
|
68
|
+
continue;
|
|
69
|
+
if (kind === "[") {
|
|
70
|
+
const body = sequence.slice(2);
|
|
71
|
+
if (body === "200~" || body === "201~") {
|
|
72
|
+
this.pasting = body === "200~";
|
|
73
|
+
this.keyboardInput("");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Device/status/window reports, Kitty capability replies and focus events.
|
|
77
|
+
if (/^(?:[?>]?[\d;]*c|\??\d+;\d+R|[\d;]+[nt]|\??[\d;]+\$y|\?[\d;]+u|[IO])$/.test(body))
|
|
78
|
+
continue;
|
|
79
|
+
// Printable keys in Kitty's extended keyboard protocol are still drafts.
|
|
80
|
+
const key = /^(\d+)(?::[\d:]*)?(?:;([\d:;]+))?u$/.exec(body);
|
|
81
|
+
if (key) {
|
|
82
|
+
const code = Number(key[1]);
|
|
83
|
+
const modifiers = key[2]?.split(";")[0] ?? "1";
|
|
84
|
+
if (modifiers.endsWith(":3"))
|
|
85
|
+
continue; // Key release.
|
|
86
|
+
if (code === 13 && modifiers === "1")
|
|
87
|
+
this.keyboardInput("\r");
|
|
88
|
+
else if (code >= 32 && code < 57344)
|
|
89
|
+
this.keyboardInput("x");
|
|
90
|
+
else
|
|
91
|
+
this.keyboardInput("");
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
this.keyboardInput(""); // Navigation and mouse input cancel controls.
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
this.keyboardInput(kind === "O" ? "" : char);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
keyboardInput(char) {
|
|
101
|
+
this.cancel("The local keyboard interrupted the remote control operation.");
|
|
102
|
+
if (!this.pasting && ["\r", "\n", "\u0003", "\u0015"].includes(char))
|
|
103
|
+
this.draft = false;
|
|
104
|
+
else if (char && (char >= " " && char !== "\u007f" || this.pasting))
|
|
105
|
+
this.draft = true;
|
|
106
|
+
}
|
|
107
|
+
cancel(message) {
|
|
108
|
+
if (this.operation)
|
|
109
|
+
this.operation.error = new Error(message);
|
|
110
|
+
}
|
|
111
|
+
async flush() { await this.writes; }
|
|
112
|
+
get lines() {
|
|
113
|
+
const buffer = this.terminal.buffer.active;
|
|
114
|
+
return Array.from({ length: this.terminal.rows }, (_, row) => buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? "");
|
|
115
|
+
}
|
|
116
|
+
get text() { return this.lines.join("\n"); }
|
|
117
|
+
check() {
|
|
118
|
+
const operation = this.operation;
|
|
119
|
+
if (!operation)
|
|
120
|
+
throw new Error("No terminal control operation is active.");
|
|
121
|
+
if (operation.error)
|
|
122
|
+
throw operation.error;
|
|
123
|
+
if (operation.identity !== this.options.identity()) {
|
|
124
|
+
operation.error = new Error("The active CLI session changed during control. Try again.");
|
|
125
|
+
throw operation.error;
|
|
126
|
+
}
|
|
127
|
+
this.options.assertReady();
|
|
128
|
+
if (Date.now() > operation.deadline)
|
|
129
|
+
throw new Error("Native CLI control timed out; the result could not be confirmed.");
|
|
130
|
+
}
|
|
131
|
+
async run(action) {
|
|
132
|
+
if (this.operation)
|
|
133
|
+
throw new Error("Another CLI control operation is in progress.");
|
|
134
|
+
this.options.assertReady();
|
|
135
|
+
this.operation = { identity: this.options.identity(), deadline: Date.now() + 25_000 };
|
|
136
|
+
try {
|
|
137
|
+
await this.flush();
|
|
138
|
+
if (this.options.prepare) {
|
|
139
|
+
await this.options.prepare();
|
|
140
|
+
this.check();
|
|
141
|
+
this.draft = false; // The native input state is authoritative when available.
|
|
142
|
+
}
|
|
143
|
+
else if (this.draft && this.options.isPromptEmpty?.())
|
|
144
|
+
this.draft = false;
|
|
145
|
+
if (this.draft)
|
|
146
|
+
throw new Error("The local terminal has an input draft. Submit or clear it before using remote controls.");
|
|
147
|
+
const result = await action();
|
|
148
|
+
this.check();
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
const operation = this.operation;
|
|
153
|
+
let ready = false;
|
|
154
|
+
try {
|
|
155
|
+
this.options.assertReady();
|
|
156
|
+
ready = true;
|
|
157
|
+
}
|
|
158
|
+
catch { /* A task or approval may have taken over. */ }
|
|
159
|
+
// Never send an Escape into a different session or after local input has taken over.
|
|
160
|
+
if (ready && this.ownedMenu && !operation?.error && operation?.identity === this.options.identity() && this.hasMenu(this.ownedMenu)) {
|
|
161
|
+
this.options.write("\u001b");
|
|
162
|
+
await pause(100);
|
|
163
|
+
}
|
|
164
|
+
this.ownedMenu = null;
|
|
165
|
+
this.operation = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async key(text) {
|
|
169
|
+
this.check();
|
|
170
|
+
this.options.write(text);
|
|
171
|
+
await pause(100);
|
|
172
|
+
await this.flush();
|
|
173
|
+
this.check();
|
|
174
|
+
}
|
|
175
|
+
async waitFor(predicate) {
|
|
176
|
+
while (true) {
|
|
177
|
+
this.check();
|
|
178
|
+
await this.flush();
|
|
179
|
+
if (predicate())
|
|
180
|
+
return;
|
|
181
|
+
await pause(50);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async command(command) {
|
|
185
|
+
if (!/^\/[a-z]+$/.test(command))
|
|
186
|
+
throw new Error("Invalid native control command.");
|
|
187
|
+
await this.key(`\u001b[200~${command}\u001b[201~`);
|
|
188
|
+
await this.key("\r");
|
|
189
|
+
// Claude can consume the first Enter to accept a slash completion while its
|
|
190
|
+
// interactive UI is finishing startup. Retry only while that exact command
|
|
191
|
+
// remains at the *last* prompt; never submit an arbitrary draft or a dialog.
|
|
192
|
+
for (let retry = 0; retry < 3; retry++) {
|
|
193
|
+
// A lazy-loaded command can briefly leave its autocomplete on screen.
|
|
194
|
+
// Let the initial render complete before deciding whether Enter was consumed.
|
|
195
|
+
const until = Date.now() + 1_500;
|
|
196
|
+
do {
|
|
197
|
+
await pause(100);
|
|
198
|
+
await this.flush();
|
|
199
|
+
this.check();
|
|
200
|
+
} while (Date.now() < until && Date.now() - this.lastFrameAt < 500);
|
|
201
|
+
if (Date.now() - this.lastFrameAt < 500)
|
|
202
|
+
break;
|
|
203
|
+
const prompt = this.lines.findLast((line) => /^\s*❯/.test(line))?.trim();
|
|
204
|
+
if (prompt?.replace(/^❯\s*/, "").trim() !== command || /Select model|Select agent|Select variant/.test(this.text))
|
|
205
|
+
break;
|
|
206
|
+
await this.key("\r");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
hasMenu(title) {
|
|
210
|
+
return this.lines.some((line) => line.trim() === title || new RegExp(`^${title}\\s+esc$`, "i").test(line.trim()));
|
|
211
|
+
}
|
|
212
|
+
async openMenu(command, title) {
|
|
213
|
+
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
|
+
}
|
|
216
|
+
await this.command(command);
|
|
217
|
+
await this.waitFor(() => this.hasMenu(title));
|
|
218
|
+
this.ownedMenu = title;
|
|
219
|
+
}
|
|
220
|
+
async closeMenu() {
|
|
221
|
+
const title = this.ownedMenu;
|
|
222
|
+
if (!title || !this.hasMenu(title))
|
|
223
|
+
throw new Error("The native selector changed unexpectedly.");
|
|
224
|
+
await this.key("\u001b");
|
|
225
|
+
await this.waitFor(() => !this.hasMenu(title));
|
|
226
|
+
this.ownedMenu = null;
|
|
227
|
+
}
|
|
228
|
+
async submitMenu(nextTitle, key = "\r") {
|
|
229
|
+
const title = this.ownedMenu;
|
|
230
|
+
if (!title || !this.hasMenu(title))
|
|
231
|
+
throw new Error("The native selector changed unexpectedly.");
|
|
232
|
+
await this.key(key);
|
|
233
|
+
await this.waitFor(() => !this.hasMenu(title));
|
|
234
|
+
this.ownedMenu = nextTitle && this.hasMenu(nextTitle) ? nextTitle : null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export function parseClaudeModelPicker(text) {
|
|
238
|
+
const start = text.lastIndexOf("Select model");
|
|
239
|
+
if (start < 0)
|
|
240
|
+
return [];
|
|
241
|
+
return text.slice(start).split("\n").flatMap((line) => {
|
|
242
|
+
const match = /^\s*([❯>↑↓ ]*)\s*(\d+)\.\s+(.+?)\s*$/.exec(line);
|
|
243
|
+
if (!match)
|
|
244
|
+
return [];
|
|
245
|
+
return [{ index: Number(match[2]), label: match[3].replace(/[✓✔]/g, "").replace(/\s+/g, " ").trim(), current: /[✓✔]/.test(line), focused: /[❯>]/.test(match[1]) }];
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
export function readClaudePermissionMode(text) {
|
|
249
|
+
const lines = text.split("\n");
|
|
250
|
+
const prompt = lines.findLastIndex((line) => /^\s*❯/.test(line));
|
|
251
|
+
if (prompt < 0 || !/^\s*❯\s*$/.test(lines[prompt]))
|
|
252
|
+
return null;
|
|
253
|
+
const footer = lines.slice(prompt + 1).join("\n");
|
|
254
|
+
for (const [label, mode] of [["plan mode", "plan"], ["accept edits", "acceptEdits"], ["bypass permissions", "bypassPermissions"], ["auto", "auto"], ["don't ask", "dontAsk"]]) {
|
|
255
|
+
if (footer.includes(`${label} on`))
|
|
256
|
+
return mode;
|
|
257
|
+
}
|
|
258
|
+
return "default";
|
|
259
|
+
}
|