@unlinearity/cli-wechat-bridge 1.1.6 → 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 +467 -475
- 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 +10 -0
- package/bin/wecom-claude.mjs +10 -0
- package/bin/wecom-codex.mjs +10 -0
- package/bin/wecom-daemon.mjs +10 -0
- package/bin/wecom-opencode.mjs +10 -0
- package/bin/wecom-pi.mjs +10 -0
- package/bin/wecom-setup.mjs +10 -0
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +161 -4
- 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 +41 -2
- package/dist/bridge/bridge-state.js +16 -3
- 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 +380 -171
- 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 +49 -3
- package/dist/channels/wecom/bridge-final-reply.js +32 -0
- package/dist/channels/wecom/setup-cli.js +12 -0
- package/dist/channels/wecom/setup.js +182 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +72 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +147 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +314 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +30 -3
- 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 +62 -11
- package/dist/companion/local-companion.js +25 -1
- 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/channel-types.js +3 -1
- package/dist/core/conversation-routing.js +58 -0
- package/dist/core/direct-run.js +23 -0
- package/dist/core/inbound-message-claims.js +67 -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 +9 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +675 -122
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +9 -1
- package/dist/i18n/messages-zh.js +9 -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 +36 -10
- package/dist/utils/version-checker.js +17 -11
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +119 -13
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +169 -58
- package/package.json +13 -3
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -1,10 +1,18 @@
|
|
|
1
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.
|
|
2
8
|
import net from "node:net";
|
|
3
9
|
import path from "node:path";
|
|
4
10
|
import { spawn } from "node:child_process";
|
|
5
11
|
import { fileURLToPath } from "node:url";
|
|
6
12
|
import { BRIDGE_LOG_FILE } from "../wechat/channel-config.js";
|
|
7
13
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
14
|
+
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
15
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
8
16
|
import { readBridgeLockFile, shouldAutoReclaimBridgeLock, } from "../bridge/bridge-state.js";
|
|
9
17
|
import { clearLocalCompanionOccupancy, clearLocalCompanionEndpoint, readLocalCompanionEndpoint, } from "./local-companion-link.js";
|
|
10
18
|
import { runCodexRemoteClient } from "./codex-remote-client.js";
|
|
@@ -15,8 +23,8 @@ const MODULE_DIR = path.dirname(MODULE_FILE);
|
|
|
15
23
|
const RUNTIME_ENTRY_EXTENSION = path.extname(MODULE_FILE) === ".ts" ? ".ts" : ".js";
|
|
16
24
|
const DEFAULT_WAIT_TIMEOUT_MS = 15_000;
|
|
17
25
|
const DEFAULT_ADAPTER = "codex";
|
|
18
|
-
function log(adapter, message) {
|
|
19
|
-
process.stderr.write(`[
|
|
26
|
+
function log(adapter, message, channelId = "wechat") {
|
|
27
|
+
process.stderr.write(`[${channelId}-${adapter}] ${message}\n`);
|
|
20
28
|
}
|
|
21
29
|
export function normalizeComparablePath(cwd) {
|
|
22
30
|
const normalized = path.resolve(cwd);
|
|
@@ -37,8 +45,13 @@ export function formatSwitchFailureMessage(cwd) {
|
|
|
37
45
|
export function formatRestartUnhealthyMessage(cwd) {
|
|
38
46
|
return `Detected unhealthy companion state for ${cwd}. Restarting bridge...`;
|
|
39
47
|
}
|
|
40
|
-
export function defaultSessionStartMode(
|
|
41
|
-
|
|
48
|
+
export function defaultSessionStartMode(_adapter) {
|
|
49
|
+
// Codex 0.155+ persists app-server threads lazily, so launching the visible
|
|
50
|
+
// client with a freshly prepared (not yet persisted) thread id crashes its
|
|
51
|
+
// bootstrap with "no rollout found". Start every adapter fresh by default;
|
|
52
|
+
// explicit --session-start-mode restore remains available for deliberate
|
|
53
|
+
// restores of already persisted threads.
|
|
54
|
+
return "new";
|
|
42
55
|
}
|
|
43
56
|
export function decideLaunchAction(input) {
|
|
44
57
|
if (input.lockShouldAutoReclaim) {
|
|
@@ -48,6 +61,8 @@ export function decideLaunchAction(input) {
|
|
|
48
61
|
};
|
|
49
62
|
}
|
|
50
63
|
const sameWorkspace = input.runningLock.adapter === input.requestedAdapter &&
|
|
64
|
+
(input.runningLock.channelId ?? "wechat") ===
|
|
65
|
+
(input.requestedChannelId ?? "wechat") &&
|
|
51
66
|
isSameWorkspaceCwd(input.runningLock.cwd, input.requestedCwd);
|
|
52
67
|
if (!sameWorkspace) {
|
|
53
68
|
return {
|
|
@@ -99,6 +114,7 @@ export function parseCliArgs(argv) {
|
|
|
99
114
|
let profile;
|
|
100
115
|
let timeoutMs = DEFAULT_WAIT_TIMEOUT_MS;
|
|
101
116
|
let sessionStartMode;
|
|
117
|
+
let channelId = "wechat";
|
|
102
118
|
const cliArgs = [];
|
|
103
119
|
for (let i = 0; i < argv.length; i += 1) {
|
|
104
120
|
const arg = argv[i];
|
|
@@ -108,7 +124,7 @@ export function parseCliArgs(argv) {
|
|
|
108
124
|
const next = argv[i + 1];
|
|
109
125
|
if (arg === "--help" || arg === "-h") {
|
|
110
126
|
const helpAdapter = adapter;
|
|
111
|
-
const commandName =
|
|
127
|
+
const commandName = `${channelId}-${helpAdapter}`;
|
|
112
128
|
const adapterLabel = {
|
|
113
129
|
codex: "Codex",
|
|
114
130
|
claude: "Claude Code",
|
|
@@ -118,7 +134,7 @@ export function parseCliArgs(argv) {
|
|
|
118
134
|
process.stdout.write([
|
|
119
135
|
`Usage: ${commandName} [--cwd <path>] [--profile <name-or-path>] [--timeout-ms <ms>] [--session-start-mode <restore|new>] [...${helpAdapter} args]`,
|
|
120
136
|
"",
|
|
121
|
-
|
|
137
|
+
`Ensures the ${channelId === "wecom" ? "WeCom" : "WeChat"} runtime for the current directory, then opens or reuses the visible CLI.`,
|
|
122
138
|
"If a same-workspace daemon is running, the command delegates to that daemon.",
|
|
123
139
|
helpAdapter === "codex"
|
|
124
140
|
? "Codex restores the current session by default."
|
|
@@ -136,6 +152,14 @@ export function parseCliArgs(argv) {
|
|
|
136
152
|
if (arg === "--doctor") {
|
|
137
153
|
continue;
|
|
138
154
|
}
|
|
155
|
+
if (arg === "--channel") {
|
|
156
|
+
if (!next || (next !== "wechat" && next !== "wecom")) {
|
|
157
|
+
throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
|
|
158
|
+
}
|
|
159
|
+
channelId = next;
|
|
160
|
+
i += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
139
163
|
if (arg === "--adapter") {
|
|
140
164
|
if (!next || !["codex", "claude", "opencode", "pi"].includes(next)) {
|
|
141
165
|
throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
|
|
@@ -189,6 +213,7 @@ export function parseCliArgs(argv) {
|
|
|
189
213
|
timeoutMs,
|
|
190
214
|
sessionStartMode: sessionStartMode ?? defaultSessionStartMode(adapter),
|
|
191
215
|
cliArgs,
|
|
216
|
+
channelId,
|
|
192
217
|
};
|
|
193
218
|
}
|
|
194
219
|
function isPidAlive(pid) {
|
|
@@ -295,6 +320,9 @@ export function buildBackgroundBridgeArgs(entryPath, options) {
|
|
|
295
320
|
args.push("--experimental-strip-types");
|
|
296
321
|
}
|
|
297
322
|
args.push(entryPath, "--adapter", options.adapter, "--cwd", options.cwd, "--lifecycle", lifecycle);
|
|
323
|
+
if (options.channelId === "wecom") {
|
|
324
|
+
args.push("--channel", "wecom");
|
|
325
|
+
}
|
|
298
326
|
if (options.sessionStartMode !== "restore") {
|
|
299
327
|
args.push("--session-start-mode", options.sessionStartMode);
|
|
300
328
|
}
|
|
@@ -361,6 +389,7 @@ async function ensureBridgeReady(options) {
|
|
|
361
389
|
endpointIsReachable: Boolean(endpointResult.endpoint),
|
|
362
390
|
companionIsAlive: isCompanionAlive(endpointResult.endpoint),
|
|
363
391
|
sessionStartMode: options.sessionStartMode,
|
|
392
|
+
requestedChannelId: options.channelId,
|
|
364
393
|
});
|
|
365
394
|
log(options.adapter, decision.message);
|
|
366
395
|
if (decision.kind === "already_active") {
|
|
@@ -408,8 +437,13 @@ export async function tryDelegateToDaemon(options, deps = {}) {
|
|
|
408
437
|
clearEndpoint(endpoint.pid);
|
|
409
438
|
return false;
|
|
410
439
|
}
|
|
440
|
+
const requestedChannel = options.channelId ?? "wechat";
|
|
441
|
+
const endpointChannel = endpoint.channelId ?? "wechat";
|
|
442
|
+
if (requestedChannel !== endpointChannel) {
|
|
443
|
+
throw new Error(`${endpointChannel}-daemon is running for ${endpoint.cwd}. Stop it before starting the ${requestedChannel} channel.`);
|
|
444
|
+
}
|
|
411
445
|
if (!isSameWorkspaceCwd(endpoint.cwd, options.cwd)) {
|
|
412
|
-
throw new Error(
|
|
446
|
+
throw new Error(`${requestedChannel}-daemon is running for ${endpoint.cwd}. This launcher requested ${options.cwd}; v1 daemon switching is limited to its startup cwd.`);
|
|
413
447
|
}
|
|
414
448
|
const response = await sendRequest(endpoint, {
|
|
415
449
|
command: "ensure_slot",
|
|
@@ -429,7 +463,7 @@ export async function tryDelegateToDaemon(options, deps = {}) {
|
|
|
429
463
|
log(options.adapter, `Daemon delegation failed (${response.error ?? "unknown error"}). Falling back to a local bridge.`);
|
|
430
464
|
return false;
|
|
431
465
|
}
|
|
432
|
-
log(options.adapter, `Delegated to running
|
|
466
|
+
log(options.adapter, `Delegated to running ${requestedChannel}-daemon for ${options.cwd}.`, requestedChannel);
|
|
433
467
|
return true;
|
|
434
468
|
}
|
|
435
469
|
export async function runVisibleClient(options, runners = {}) {
|
|
@@ -456,6 +490,15 @@ export async function ensureCompanionStartWechatCredentials(adapter, ensureCrede
|
|
|
456
490
|
log: (message) => log(adapter, message),
|
|
457
491
|
});
|
|
458
492
|
}
|
|
493
|
+
export async function ensureCompanionStartChannelCredentials(options) {
|
|
494
|
+
if (options.channelId === "wecom") {
|
|
495
|
+
await ensureWecomAccount({
|
|
496
|
+
log: (message) => log(options.adapter, message, "wecom"),
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
await ensureCompanionStartWechatCredentials(options.adapter);
|
|
501
|
+
}
|
|
459
502
|
export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
|
|
460
503
|
if (argv.includes("--doctor")) {
|
|
461
504
|
const { runDoctorCheck } = await import("../utils/doctor.js");
|
|
@@ -463,7 +506,7 @@ export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
|
|
|
463
506
|
return 0;
|
|
464
507
|
}
|
|
465
508
|
const options = parseCliArgs(argv);
|
|
466
|
-
await
|
|
509
|
+
await ensureCompanionStartChannelCredentials(options);
|
|
467
510
|
if (await tryDelegateToDaemon(options)) {
|
|
468
511
|
return 0;
|
|
469
512
|
}
|
|
@@ -487,11 +530,19 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
487
530
|
return DEFAULT_ADAPTER;
|
|
488
531
|
}
|
|
489
532
|
})();
|
|
490
|
-
|
|
533
|
+
const channelId = (() => {
|
|
534
|
+
try {
|
|
535
|
+
return parseCliArgs(argv).channelId ?? "wechat";
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return "wechat";
|
|
539
|
+
}
|
|
540
|
+
})();
|
|
541
|
+
log(adapter, error instanceof Error ? error.message : String(error), channelId);
|
|
491
542
|
process.exit(1);
|
|
492
543
|
}
|
|
493
544
|
}
|
|
494
|
-
const isDirectRun =
|
|
545
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
495
546
|
if (isDirectRun) {
|
|
496
547
|
void main();
|
|
497
548
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
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.
|
|
2
8
|
import net from "node:net";
|
|
3
9
|
import path from "node:path";
|
|
4
10
|
import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
|
|
@@ -6,6 +12,7 @@ import { LOCAL_COMPANION_RECONNECT_GRACE_MS, } from "../bridge/bridge-adapters.s
|
|
|
6
12
|
import { runCodexRemoteClientFromEndpoint } from "./codex-remote-client.js";
|
|
7
13
|
import { attachLocalCompanionMessageListener, readLocalCompanionEndpoint, sendLocalCompanionMessage, } from "./local-companion-link.js";
|
|
8
14
|
import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
|
|
15
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
9
16
|
export const LOCAL_COMPANION_RECONNECT_RETRY_MS = 250;
|
|
10
17
|
function log(adapter, message) {
|
|
11
18
|
process.stderr.write(`[${adapter}-companion] ${message}\n`);
|
|
@@ -230,6 +237,23 @@ export async function runLocalCompanion(options) {
|
|
|
230
237
|
publishState();
|
|
231
238
|
sendResponse(socket, message.id, true);
|
|
232
239
|
break;
|
|
240
|
+
case "list_models":
|
|
241
|
+
if (!adapter.listModels)
|
|
242
|
+
throw new Error("Model listing is not supported by this companion.");
|
|
243
|
+
sendResponse(socket, message.id, true, await adapter.listModels());
|
|
244
|
+
break;
|
|
245
|
+
case "select_model":
|
|
246
|
+
if (!adapter.selectModel)
|
|
247
|
+
throw new Error("Model selection is not supported by this companion.");
|
|
248
|
+
sendResponse(socket, message.id, true, await adapter.selectModel(message.payload.modelId));
|
|
249
|
+
publishState();
|
|
250
|
+
break;
|
|
251
|
+
case "set_plan_mode":
|
|
252
|
+
if (!adapter.setPlanMode)
|
|
253
|
+
throw new Error("Plan mode is not supported by this companion.");
|
|
254
|
+
sendResponse(socket, message.id, true, await adapter.setPlanMode(message.payload.enabled));
|
|
255
|
+
publishState();
|
|
256
|
+
break;
|
|
233
257
|
case "interrupt":
|
|
234
258
|
sendResponse(socket, message.id, true, await adapter.interrupt());
|
|
235
259
|
break;
|
|
@@ -439,7 +463,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
439
463
|
process.exit(1);
|
|
440
464
|
}
|
|
441
465
|
}
|
|
442
|
-
const isDirectRun =
|
|
466
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
443
467
|
if (isDirectRun) {
|
|
444
468
|
void main();
|
|
445
469
|
}
|
|
@@ -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 net from "node:net";
|
|
2
8
|
const ROUTE_POLL_INTERVAL_MS = 100;
|
|
3
9
|
const MAX_BUFFER_SIZE = 1024 * 1024;
|
|
@@ -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 net from "node:net";
|
|
3
9
|
import path from "node:path";
|
|
@@ -167,7 +173,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
167
173
|
}
|
|
168
174
|
},
|
|
169
175
|
});
|
|
170
|
-
const executeCommand = (command) => {
|
|
176
|
+
const executeCommand = async (command) => {
|
|
171
177
|
try {
|
|
172
178
|
if (command.type === "prompt" && typeof command.text === "string") {
|
|
173
179
|
if (!latestContext?.isIdle()) {
|
|
@@ -180,6 +186,59 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
180
186
|
latestContext?.abort();
|
|
181
187
|
writeFrame({ type: "response", id: command.id, success: true });
|
|
182
188
|
}
|
|
189
|
+
else if (command.type === "list_models") {
|
|
190
|
+
if (!latestContext) {
|
|
191
|
+
throw new Error("Pi session context is not ready.");
|
|
192
|
+
}
|
|
193
|
+
const models = latestContext.scopedModels.length > 0
|
|
194
|
+
? latestContext.scopedModels.map((entry) => entry.model)
|
|
195
|
+
: latestContext.modelRegistry.getAvailable();
|
|
196
|
+
writeFrame({
|
|
197
|
+
type: "response",
|
|
198
|
+
id: command.id,
|
|
199
|
+
success: true,
|
|
200
|
+
data: {
|
|
201
|
+
models: models.map((model) => ({
|
|
202
|
+
id: `${model.provider}/${model.id}`,
|
|
203
|
+
displayName: model.name
|
|
204
|
+
? `${model.name} (${model.provider})`
|
|
205
|
+
: `${model.id} (${model.provider})`,
|
|
206
|
+
isCurrent: latestContext?.model?.provider === model.provider &&
|
|
207
|
+
latestContext.model.id === model.id,
|
|
208
|
+
})),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else if (command.type === "select_model" && typeof command.modelId === "string") {
|
|
213
|
+
if (!latestContext?.isIdle()) {
|
|
214
|
+
throw new Error("Pi TUI is already processing a turn.");
|
|
215
|
+
}
|
|
216
|
+
const models = latestContext.scopedModels.length > 0
|
|
217
|
+
? latestContext.scopedModels.map((entry) => entry.model)
|
|
218
|
+
: latestContext.modelRegistry.getAvailable();
|
|
219
|
+
const model = models.find((candidate) => `${candidate.provider}/${candidate.id}` === command.modelId);
|
|
220
|
+
if (!model) {
|
|
221
|
+
throw new Error(`Pi model is no longer available: ${command.modelId}`);
|
|
222
|
+
}
|
|
223
|
+
const selected = await pi.setModel(model);
|
|
224
|
+
if (!selected) {
|
|
225
|
+
throw new Error(`Pi could not switch to model: ${command.modelId}`);
|
|
226
|
+
}
|
|
227
|
+
writeFrame({
|
|
228
|
+
type: "response",
|
|
229
|
+
id: command.id,
|
|
230
|
+
success: true,
|
|
231
|
+
data: {
|
|
232
|
+
model: {
|
|
233
|
+
id: `${model.provider}/${model.id}`,
|
|
234
|
+
displayName: model.name
|
|
235
|
+
? `${model.name} (${model.provider})`
|
|
236
|
+
: `${model.id} (${model.provider})`,
|
|
237
|
+
isCurrent: true,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
183
242
|
else if (command.type === "new_session" && typeof command.id === "string") {
|
|
184
243
|
pi.sendUserMessage(`/${INTERNAL_NEW_COMMAND} ${command.id}`, {
|
|
185
244
|
expandPromptTemplates: true,
|
|
@@ -245,7 +304,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
245
304
|
try {
|
|
246
305
|
const parsed = JSON.parse(line);
|
|
247
306
|
if (isRecord(parsed)) {
|
|
248
|
-
dispatchCommand(parsed);
|
|
307
|
+
void dispatchCommand(parsed);
|
|
249
308
|
}
|
|
250
309
|
}
|
|
251
310
|
catch {
|
|
@@ -260,7 +319,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
260
319
|
latestContext = context;
|
|
261
320
|
writeSessionState(context, typeof event.reason === "string" ? event.reason : "startup");
|
|
262
321
|
for (const command of queuedCommands.splice(0)) {
|
|
263
|
-
executeCommand(command);
|
|
322
|
+
void executeCommand(command);
|
|
264
323
|
}
|
|
265
324
|
});
|
|
266
325
|
pi.on("input", (event, context) => {
|
|
@@ -26,6 +26,16 @@ export function parseBridgeControlCommand(text) {
|
|
|
26
26
|
return { type: "deny" };
|
|
27
27
|
case "/answer":
|
|
28
28
|
return argument ? { type: "answer", raw: argument } : null;
|
|
29
|
+
case "/model":
|
|
30
|
+
return { type: "model", target: argument || undefined };
|
|
31
|
+
case "/plan":
|
|
32
|
+
if (!argument || argument.toLowerCase() === "on")
|
|
33
|
+
return { type: "plan", enabled: true };
|
|
34
|
+
if (argument.toLowerCase() === "off")
|
|
35
|
+
return { type: "plan", enabled: false };
|
|
36
|
+
return null;
|
|
37
|
+
case "/all":
|
|
38
|
+
return argument ? { type: "broadcast", text: argument } : null;
|
|
29
39
|
default:
|
|
30
40
|
return null;
|
|
31
41
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function shouldDeferCodexInboundMessage(params) {
|
|
2
|
+
return (params.adapter === "codex" &&
|
|
3
|
+
!params.hasPendingConfirmation &&
|
|
4
|
+
!params.hasSystemCommand &&
|
|
5
|
+
params.activeTurnOrigin === "local" &&
|
|
6
|
+
(params.status === "busy" || params.status === "awaiting_approval"));
|
|
7
|
+
}
|
|
8
|
+
export function canDrainDeferredCodexInboundQueue(params) {
|
|
9
|
+
return (params.adapter === "codex" &&
|
|
10
|
+
params.deferredCount > 0 &&
|
|
11
|
+
!params.hasPendingConfirmation &&
|
|
12
|
+
!params.hasPendingUserInput &&
|
|
13
|
+
!params.hasPendingApproval &&
|
|
14
|
+
!params.hasActiveTask &&
|
|
15
|
+
!params.activeTurnId &&
|
|
16
|
+
params.status !== "busy" &&
|
|
17
|
+
params.status !== "awaiting_approval" &&
|
|
18
|
+
params.status !== "awaiting_input");
|
|
19
|
+
}
|
|
20
|
+
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
21
|
+
return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
|
|
22
|
+
}
|
|
23
|
+
export function isRetryableDeferredCodexDrainError(errorText) {
|
|
24
|
+
return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
+
/**
|
|
9
|
+
* Keeps the reply target scoped to one inbound async call chain. Different
|
|
10
|
+
* conversations may run concurrently without overwriting each other's target.
|
|
11
|
+
*/
|
|
12
|
+
export class InboundConversationContext {
|
|
13
|
+
storage = new AsyncLocalStorage();
|
|
14
|
+
run(conversation, callback) {
|
|
15
|
+
return this.storage.run(conversation, callback);
|
|
16
|
+
}
|
|
17
|
+
get() {
|
|
18
|
+
return this.storage.getStore();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function tryBeginTurn(state, task, conversation) {
|
|
22
|
+
if (state.activeTask) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const lease = {
|
|
26
|
+
task,
|
|
27
|
+
previousConversation: state.activeConversation,
|
|
28
|
+
previousLastConversation: state.lastConversation,
|
|
29
|
+
};
|
|
30
|
+
state.activeTask = task;
|
|
31
|
+
if (conversation) {
|
|
32
|
+
state.activeConversation = conversation;
|
|
33
|
+
state.lastConversation = conversation;
|
|
34
|
+
}
|
|
35
|
+
return lease;
|
|
36
|
+
}
|
|
37
|
+
export function rollbackTurn(state, lease, options = {}) {
|
|
38
|
+
if (state.activeTask !== lease.task) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
state.activeTask = null;
|
|
42
|
+
state.activeConversation = lease.previousConversation;
|
|
43
|
+
if (options.restoreLastConversation) {
|
|
44
|
+
state.lastConversation = lease.previousLastConversation;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
export function clearTurn(state, expectedTask) {
|
|
49
|
+
if (expectedTask && state.activeTask !== expectedTask) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
state.activeTask = null;
|
|
53
|
+
state.activeConversation = undefined;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
export function resolveConversationTarget(params) {
|
|
57
|
+
return params.active ?? params.last ?? params.fallback;
|
|
58
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
function normalizeComparablePath(filePath) {
|
|
10
|
+
const normalized = path.resolve(filePath);
|
|
11
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
12
|
+
}
|
|
13
|
+
export function isDirectModuleRun(importMetaUrl, argv = process.argv, importMetaMain) {
|
|
14
|
+
if (typeof importMetaMain === "boolean") {
|
|
15
|
+
return importMetaMain;
|
|
16
|
+
}
|
|
17
|
+
const entryPath = argv[1];
|
|
18
|
+
if (!entryPath) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return normalizeComparablePath(fileURLToPath(importMetaUrl)) ===
|
|
22
|
+
normalizeComparablePath(entryPath);
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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 crypto from "node:crypto";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
export const DEFAULT_INBOUND_MESSAGE_CLAIM_TTL_MS = 10 * 60 * 1000;
|
|
11
|
+
export function buildInboundMessageClaimPath(messageKey, claimsDir) {
|
|
12
|
+
const fileName = `${crypto.createHash("sha1").update(messageKey).digest("hex")}.json`;
|
|
13
|
+
return path.join(claimsDir, fileName);
|
|
14
|
+
}
|
|
15
|
+
export function tryClaimChannelInboundMessage(messageKey, options) {
|
|
16
|
+
if (!messageKey) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
20
|
+
const ttlMs = options.ttlMs ?? DEFAULT_INBOUND_MESSAGE_CLAIM_TTL_MS;
|
|
21
|
+
const claimPath = buildInboundMessageClaimPath(messageKey, options.claimsDir);
|
|
22
|
+
const attemptClaim = () => {
|
|
23
|
+
fs.mkdirSync(options.claimsDir, { recursive: true });
|
|
24
|
+
const handle = fs.openSync(claimPath, "wx");
|
|
25
|
+
try {
|
|
26
|
+
fs.writeFileSync(handle, JSON.stringify({
|
|
27
|
+
key: messageKey,
|
|
28
|
+
claimedAt: new Date(nowMs).toISOString(),
|
|
29
|
+
pid: process.pid,
|
|
30
|
+
}, null, 2), "utf-8");
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
fs.closeSync(handle);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
return attemptClaim();
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const code = typeof error === "object" &&
|
|
42
|
+
error !== null &&
|
|
43
|
+
"code" in error &&
|
|
44
|
+
typeof error.code === "string"
|
|
45
|
+
? error.code
|
|
46
|
+
: "";
|
|
47
|
+
if (code !== "EEXIST") {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const stat = fs.statSync(claimPath);
|
|
53
|
+
if (Number.isFinite(stat.mtimeMs) && nowMs - stat.mtimeMs > ttlMs) {
|
|
54
|
+
fs.rmSync(claimPath, { force: true });
|
|
55
|
+
return attemptClaim();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
try {
|
|
60
|
+
return attemptClaim();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound target priority for standalone bridges:
|
|
3
|
+
* 1. an explicit override captured at enqueue time;
|
|
4
|
+
* 2. for the operator on multi-conversation channels, the pre-resolved turn
|
|
5
|
+
* target (callers pass `TurnCoordinator.resolveTarget(operatorConversation)`);
|
|
6
|
+
* 3. otherwise a direct conversation to the sender.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveOutboundConversationTarget(params) {
|
|
9
|
+
if (params.override) {
|
|
10
|
+
return params.override;
|
|
11
|
+
}
|
|
12
|
+
if (params.multiConversation && params.senderId === params.operatorId) {
|
|
13
|
+
return params.operatorTarget;
|
|
14
|
+
}
|
|
15
|
+
return params.directConversation(params.senderId);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Outbound target priority for daemon sends. Multi-conversation operators
|
|
19
|
+
* resolve through the current inbound conversation first, then the active
|
|
20
|
+
* slot's remembered target, then the daemon fallback conversation.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveDaemonOutboundTarget(params) {
|
|
23
|
+
if (params.override) {
|
|
24
|
+
return params.override;
|
|
25
|
+
}
|
|
26
|
+
if (params.multiConversation && params.senderId === params.operatorId) {
|
|
27
|
+
return params.inboundConversation
|
|
28
|
+
?? params.activeSlotTarget
|
|
29
|
+
?? params.fallbackConversation;
|
|
30
|
+
}
|
|
31
|
+
return params.directConversation(params.senderId);
|
|
32
|
+
}
|
package/dist/core/text-utils.js
CHANGED
|
@@ -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
|
const ANSI_ESCAPE_RE =
|
|
2
8
|
// eslint-disable-next-line no-control-regex
|
|
3
9
|
/\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|