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
|
@@ -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 crypto from "node:crypto";
|
|
2
8
|
import net from "node:net";
|
|
3
9
|
import { readLocalCompanionEndpoint, } from "./local-companion-link.js";
|
|
@@ -23,12 +29,37 @@ export function parseCodexVisibleSwitchRequest(value) {
|
|
|
23
29
|
threadId: value.threadId,
|
|
24
30
|
};
|
|
25
31
|
}
|
|
32
|
+
export function parseCodexVisibleControlRequest(value) {
|
|
33
|
+
const switchRequest = parseCodexVisibleSwitchRequest(value);
|
|
34
|
+
if (switchRequest) {
|
|
35
|
+
return switchRequest;
|
|
36
|
+
}
|
|
37
|
+
if (!isRecord(value)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (value.type !== "shutdown" ||
|
|
41
|
+
typeof value.id !== "string" ||
|
|
42
|
+
typeof value.token !== "string") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type: "shutdown",
|
|
47
|
+
id: value.id,
|
|
48
|
+
token: value.token,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
26
51
|
export function sendCodexVisibleSwitchResponse(socket, response) {
|
|
27
52
|
if (socket.destroyed || socket.writableEnded) {
|
|
28
53
|
return;
|
|
29
54
|
}
|
|
30
55
|
socket.end(`${JSON.stringify(response)}\n`);
|
|
31
56
|
}
|
|
57
|
+
export function sendCodexVisibleShutdownResponse(socket, response) {
|
|
58
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
socket.end(`${JSON.stringify(response)}\n`);
|
|
62
|
+
}
|
|
32
63
|
function resolveCodexControlEndpoint(cwd, instanceId) {
|
|
33
64
|
const endpoint = readLocalCompanionEndpoint(cwd, { adapter: "codex" });
|
|
34
65
|
if (!endpoint || endpoint.kind !== "codex" || endpoint.instanceId !== instanceId) {
|
|
@@ -120,3 +151,79 @@ export async function requestCodexVisibleThreadSwitch(params) {
|
|
|
120
151
|
});
|
|
121
152
|
});
|
|
122
153
|
}
|
|
154
|
+
export async function requestCodexVisibleClientShutdown(params) {
|
|
155
|
+
const endpoint = resolveCodexControlEndpoint(params.cwd, params.instanceId);
|
|
156
|
+
const requestId = crypto.randomUUID();
|
|
157
|
+
const timeoutMs = params.timeoutMs ?? 5_000;
|
|
158
|
+
await new Promise((resolve, reject) => {
|
|
159
|
+
const socket = net.connect({
|
|
160
|
+
host: CODEX_VISIBLE_CONTROL_HOST,
|
|
161
|
+
port: endpoint.codexControlPort,
|
|
162
|
+
});
|
|
163
|
+
let buffer = "";
|
|
164
|
+
let settled = false;
|
|
165
|
+
const finish = (error) => {
|
|
166
|
+
if (settled) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
settled = true;
|
|
170
|
+
socket.destroy();
|
|
171
|
+
if (error) {
|
|
172
|
+
reject(error);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
resolve();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
socket.setEncoding("utf8");
|
|
179
|
+
socket.setTimeout(timeoutMs);
|
|
180
|
+
socket.once("connect", () => {
|
|
181
|
+
const request = {
|
|
182
|
+
type: "shutdown",
|
|
183
|
+
id: requestId,
|
|
184
|
+
token: endpoint.codexControlToken,
|
|
185
|
+
};
|
|
186
|
+
socket.write(`${JSON.stringify(request)}\n`);
|
|
187
|
+
});
|
|
188
|
+
socket.on("data", (chunk) => {
|
|
189
|
+
buffer += chunk;
|
|
190
|
+
if (buffer.length > 65_536) {
|
|
191
|
+
finish(new Error("The visible Codex client returned an oversized shutdown response."));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
195
|
+
if (newlineIndex < 0) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
199
|
+
let parsed;
|
|
200
|
+
try {
|
|
201
|
+
parsed = JSON.parse(line);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
finish(new Error("The visible Codex client returned an invalid shutdown response."));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!isRecord(parsed) || parsed.type !== "shutdown_result" || parsed.id !== requestId) {
|
|
208
|
+
finish(new Error("The visible Codex client returned an unexpected shutdown response."));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (parsed.ok !== true) {
|
|
212
|
+
finish(new Error(typeof parsed.error === "string"
|
|
213
|
+
? parsed.error
|
|
214
|
+
: "The visible Codex client could not shut down."));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
finish();
|
|
218
|
+
});
|
|
219
|
+
socket.once("timeout", () => {
|
|
220
|
+
finish(new Error("Timed out waiting for the visible Codex client to shut down."));
|
|
221
|
+
});
|
|
222
|
+
socket.once("error", (error) => finish(error));
|
|
223
|
+
socket.once("close", () => {
|
|
224
|
+
if (!settled) {
|
|
225
|
+
finish(new Error("The visible Codex client closed the shutdown connection."));
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -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 crypto from "node:crypto";
|
|
2
8
|
import fs from "node:fs";
|
|
3
9
|
import { ensureWorkspaceChannelDir, getWorkspaceAdapterEndpointFile, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
|
|
@@ -1,4 +1,10 @@
|
|
|
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";
|
|
@@ -39,8 +45,13 @@ export function formatSwitchFailureMessage(cwd) {
|
|
|
39
45
|
export function formatRestartUnhealthyMessage(cwd) {
|
|
40
46
|
return `Detected unhealthy companion state for ${cwd}. Restarting bridge...`;
|
|
41
47
|
}
|
|
42
|
-
export function defaultSessionStartMode(
|
|
43
|
-
|
|
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";
|
|
44
55
|
}
|
|
45
56
|
export function decideLaunchAction(input) {
|
|
46
57
|
if (input.lockShouldAutoReclaim) {
|
|
@@ -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";
|
|
@@ -231,6 +237,23 @@ export async function runLocalCompanion(options) {
|
|
|
231
237
|
publishState();
|
|
232
238
|
sendResponse(socket, message.id, true);
|
|
233
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;
|
|
234
257
|
case "interrupt":
|
|
235
258
|
sendResponse(socket, message.id, true, await adapter.interrupt());
|
|
236
259
|
break;
|
|
@@ -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
|
+
}
|
package/dist/core/direct-run.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
|
import path from "node:path";
|
|
2
8
|
import { fileURLToPath } from "node:url";
|
|
3
9
|
function normalizeComparablePath(filePath) {
|
|
@@ -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 crypto from "node:crypto";
|
|
2
8
|
import fs from "node:fs";
|
|
3
9
|
import path from "node:path";
|
|
@@ -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;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { clearTurn, resolveConversationTarget, rollbackTurn, tryBeginTurn, } from "./conversation-routing.js";
|
|
2
|
+
/**
|
|
3
|
+
* Owns the active remote turn for one dispatch surface: a standalone bridge
|
|
4
|
+
* or one daemon adapter slot. Wraps the turn ownership state machine (busy
|
|
5
|
+
* rejection, dispatch rollback, conditional completion) so standalone and
|
|
6
|
+
* daemon share identical gating semantics instead of re-implementing them.
|
|
7
|
+
*/
|
|
8
|
+
export class TurnCoordinator {
|
|
9
|
+
state;
|
|
10
|
+
restoreLastConversationOnFailure;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.state = {
|
|
13
|
+
activeTask: null,
|
|
14
|
+
lastConversation: options.initialLastConversation ?? null,
|
|
15
|
+
};
|
|
16
|
+
this.restoreLastConversationOnFailure = options.restoreLastConversationOnFailure ?? false;
|
|
17
|
+
}
|
|
18
|
+
get activeTask() {
|
|
19
|
+
return this.state.activeTask;
|
|
20
|
+
}
|
|
21
|
+
get hasActiveTask() {
|
|
22
|
+
return this.state.activeTask !== null;
|
|
23
|
+
}
|
|
24
|
+
get activeConversation() {
|
|
25
|
+
return this.state.activeConversation;
|
|
26
|
+
}
|
|
27
|
+
get lastConversation() {
|
|
28
|
+
return this.state.lastConversation;
|
|
29
|
+
}
|
|
30
|
+
beginTurn(task, conversation) {
|
|
31
|
+
return tryBeginTurn(this.state, task, conversation);
|
|
32
|
+
}
|
|
33
|
+
rollback(lease, options = {}) {
|
|
34
|
+
return rollbackTurn(this.state, lease, options);
|
|
35
|
+
}
|
|
36
|
+
complete(expectedTask) {
|
|
37
|
+
return clearTurn(this.state, expectedTask);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Replaces the active task unconditionally. Used when a command path
|
|
41
|
+
* (approval confirmation, resume) takes over a continuing turn rather
|
|
42
|
+
* than dispatching fresh input.
|
|
43
|
+
*/
|
|
44
|
+
setActiveTask(task) {
|
|
45
|
+
this.state.activeTask = task;
|
|
46
|
+
}
|
|
47
|
+
/** Binds a conversation as both active and remembered target. */
|
|
48
|
+
bindConversation(conversation) {
|
|
49
|
+
this.state.activeConversation = conversation;
|
|
50
|
+
this.state.lastConversation = conversation;
|
|
51
|
+
}
|
|
52
|
+
/** Records a conversation as the remembered fallback target only. */
|
|
53
|
+
observeConversation(conversation) {
|
|
54
|
+
this.state.lastConversation = conversation;
|
|
55
|
+
}
|
|
56
|
+
resolveTarget(fallback) {
|
|
57
|
+
return resolveConversationTarget({
|
|
58
|
+
active: this.state.activeConversation,
|
|
59
|
+
last: this.state.lastConversation,
|
|
60
|
+
fallback,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Busy-rejecting dispatch transaction: acquire the turn lease, forward the
|
|
65
|
+
* input, and roll the lease back when forwarding throws. A busy input runs
|
|
66
|
+
* `onBusy` (reminder) and returns without touching ownership.
|
|
67
|
+
*/
|
|
68
|
+
async dispatch(params) {
|
|
69
|
+
const lease = this.beginTurn(params.task, params.conversation);
|
|
70
|
+
if (!lease) {
|
|
71
|
+
await params.onBusy?.();
|
|
72
|
+
return { status: "busy" };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await params.forward();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
this.rollback(lease, {
|
|
79
|
+
restoreLastConversation: this.restoreLastConversationOnFailure,
|
|
80
|
+
});
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
return { status: "dispatched", task: params.task };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -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 crypto from "node:crypto";
|
|
2
8
|
import fs from "node:fs";
|
|
3
9
|
import net from "node:net";
|
|
@@ -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 { EMOJI_BINDINGS_FILE } from "../wechat/channel-config.js";
|
|
3
9
|
import { t } from "../i18n/index.js";
|