cli-wechat-bridge 1.1.6 → 1.1.8
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 +454 -475
- package/bin/_run-entry.mjs +23 -5
- package/bin/wecom-check-update.mjs +5 -0
- package/bin/wecom-claude.mjs +5 -0
- package/bin/wecom-codex.mjs +5 -0
- package/bin/wecom-daemon.mjs +5 -0
- package/bin/wecom-opencode.mjs +5 -0
- package/bin/wecom-pi.mjs +5 -0
- package/bin/wecom-setup.mjs +5 -0
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +155 -4
- package/dist/bridge/bridge-adapters.codex.js +112 -7
- package/dist/bridge/bridge-adapters.core.js +19 -1
- package/dist/bridge/bridge-adapters.opencode.js +150 -1
- package/dist/bridge/bridge-adapters.pi.js +39 -0
- package/dist/bridge/bridge-process-reaper.js +35 -2
- package/dist/bridge/bridge-state.js +10 -3
- package/dist/bridge/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +253 -0
- package/dist/bridge/wechat-bridge.js +389 -87
- package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
- package/dist/channels/wecom/bridge-final-reply.js +26 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +176 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +66 -0
- package/dist/channels/wecom/wecom-media.js +141 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +308 -0
- package/dist/companion/codex-remote-client.js +24 -3
- package/dist/companion/codex-visible-client-link.js +101 -0
- package/dist/companion/local-companion-start.js +56 -11
- package/dist/companion/local-companion.js +19 -1
- package/dist/companion/pi-tui-bridge-extension.js +56 -3
- package/dist/core/bridge-control.js +8 -0
- package/dist/core/channel-types.js +3 -1
- package/dist/core/conversation-routing.js +48 -0
- package/dist/core/direct-run.js +17 -0
- package/dist/core/inbound-message-claims.js +61 -0
- package/dist/daemon/daemon-link.js +3 -0
- package/dist/daemon/wechat-daemon.js +335 -60
- package/dist/i18n/messages-en.js +3 -1
- package/dist/i18n/messages-zh.js +3 -1
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/doctor.js +30 -10
- package/dist/utils/version-checker.js +11 -11
- package/dist/wechat/setup.js +113 -13
- package/dist/wechat/wechat-transport.js +96 -58
- package/package.json +116 -104
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -6,6 +6,7 @@ import { LOCAL_COMPANION_RECONNECT_GRACE_MS, } from "../bridge/bridge-adapters.s
|
|
|
6
6
|
import { runCodexRemoteClientFromEndpoint } from "./codex-remote-client.js";
|
|
7
7
|
import { attachLocalCompanionMessageListener, readLocalCompanionEndpoint, sendLocalCompanionMessage, } from "./local-companion-link.js";
|
|
8
8
|
import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
|
|
9
|
+
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
9
10
|
export const LOCAL_COMPANION_RECONNECT_RETRY_MS = 250;
|
|
10
11
|
function log(adapter, message) {
|
|
11
12
|
process.stderr.write(`[${adapter}-companion] ${message}\n`);
|
|
@@ -230,6 +231,23 @@ export async function runLocalCompanion(options) {
|
|
|
230
231
|
publishState();
|
|
231
232
|
sendResponse(socket, message.id, true);
|
|
232
233
|
break;
|
|
234
|
+
case "list_models":
|
|
235
|
+
if (!adapter.listModels)
|
|
236
|
+
throw new Error("Model listing is not supported by this companion.");
|
|
237
|
+
sendResponse(socket, message.id, true, await adapter.listModels());
|
|
238
|
+
break;
|
|
239
|
+
case "select_model":
|
|
240
|
+
if (!adapter.selectModel)
|
|
241
|
+
throw new Error("Model selection is not supported by this companion.");
|
|
242
|
+
sendResponse(socket, message.id, true, await adapter.selectModel(message.payload.modelId));
|
|
243
|
+
publishState();
|
|
244
|
+
break;
|
|
245
|
+
case "set_plan_mode":
|
|
246
|
+
if (!adapter.setPlanMode)
|
|
247
|
+
throw new Error("Plan mode is not supported by this companion.");
|
|
248
|
+
sendResponse(socket, message.id, true, await adapter.setPlanMode(message.payload.enabled));
|
|
249
|
+
publishState();
|
|
250
|
+
break;
|
|
233
251
|
case "interrupt":
|
|
234
252
|
sendResponse(socket, message.id, true, await adapter.interrupt());
|
|
235
253
|
break;
|
|
@@ -439,7 +457,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
439
457
|
process.exit(1);
|
|
440
458
|
}
|
|
441
459
|
}
|
|
442
|
-
const isDirectRun =
|
|
460
|
+
const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
|
|
443
461
|
if (isDirectRun) {
|
|
444
462
|
void main();
|
|
445
463
|
}
|
|
@@ -167,7 +167,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
167
167
|
}
|
|
168
168
|
},
|
|
169
169
|
});
|
|
170
|
-
const executeCommand = (command) => {
|
|
170
|
+
const executeCommand = async (command) => {
|
|
171
171
|
try {
|
|
172
172
|
if (command.type === "prompt" && typeof command.text === "string") {
|
|
173
173
|
if (!latestContext?.isIdle()) {
|
|
@@ -180,6 +180,59 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
180
180
|
latestContext?.abort();
|
|
181
181
|
writeFrame({ type: "response", id: command.id, success: true });
|
|
182
182
|
}
|
|
183
|
+
else if (command.type === "list_models") {
|
|
184
|
+
if (!latestContext) {
|
|
185
|
+
throw new Error("Pi session context is not ready.");
|
|
186
|
+
}
|
|
187
|
+
const models = latestContext.scopedModels.length > 0
|
|
188
|
+
? latestContext.scopedModels.map((entry) => entry.model)
|
|
189
|
+
: latestContext.modelRegistry.getAvailable();
|
|
190
|
+
writeFrame({
|
|
191
|
+
type: "response",
|
|
192
|
+
id: command.id,
|
|
193
|
+
success: true,
|
|
194
|
+
data: {
|
|
195
|
+
models: models.map((model) => ({
|
|
196
|
+
id: `${model.provider}/${model.id}`,
|
|
197
|
+
displayName: model.name
|
|
198
|
+
? `${model.name} (${model.provider})`
|
|
199
|
+
: `${model.id} (${model.provider})`,
|
|
200
|
+
isCurrent: latestContext?.model?.provider === model.provider &&
|
|
201
|
+
latestContext.model.id === model.id,
|
|
202
|
+
})),
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else if (command.type === "select_model" && typeof command.modelId === "string") {
|
|
207
|
+
if (!latestContext?.isIdle()) {
|
|
208
|
+
throw new Error("Pi TUI is already processing a turn.");
|
|
209
|
+
}
|
|
210
|
+
const models = latestContext.scopedModels.length > 0
|
|
211
|
+
? latestContext.scopedModels.map((entry) => entry.model)
|
|
212
|
+
: latestContext.modelRegistry.getAvailable();
|
|
213
|
+
const model = models.find((candidate) => `${candidate.provider}/${candidate.id}` === command.modelId);
|
|
214
|
+
if (!model) {
|
|
215
|
+
throw new Error(`Pi model is no longer available: ${command.modelId}`);
|
|
216
|
+
}
|
|
217
|
+
const selected = await pi.setModel(model);
|
|
218
|
+
if (!selected) {
|
|
219
|
+
throw new Error(`Pi could not switch to model: ${command.modelId}`);
|
|
220
|
+
}
|
|
221
|
+
writeFrame({
|
|
222
|
+
type: "response",
|
|
223
|
+
id: command.id,
|
|
224
|
+
success: true,
|
|
225
|
+
data: {
|
|
226
|
+
model: {
|
|
227
|
+
id: `${model.provider}/${model.id}`,
|
|
228
|
+
displayName: model.name
|
|
229
|
+
? `${model.name} (${model.provider})`
|
|
230
|
+
: `${model.id} (${model.provider})`,
|
|
231
|
+
isCurrent: true,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
183
236
|
else if (command.type === "new_session" && typeof command.id === "string") {
|
|
184
237
|
pi.sendUserMessage(`/${INTERNAL_NEW_COMMAND} ${command.id}`, {
|
|
185
238
|
expandPromptTemplates: true,
|
|
@@ -245,7 +298,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
245
298
|
try {
|
|
246
299
|
const parsed = JSON.parse(line);
|
|
247
300
|
if (isRecord(parsed)) {
|
|
248
|
-
dispatchCommand(parsed);
|
|
301
|
+
void dispatchCommand(parsed);
|
|
249
302
|
}
|
|
250
303
|
}
|
|
251
304
|
catch {
|
|
@@ -260,7 +313,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
260
313
|
latestContext = context;
|
|
261
314
|
writeSessionState(context, typeof event.reason === "string" ? event.reason : "startup");
|
|
262
315
|
for (const command of queuedCommands.splice(0)) {
|
|
263
|
-
executeCommand(command);
|
|
316
|
+
void executeCommand(command);
|
|
264
317
|
}
|
|
265
318
|
});
|
|
266
319
|
pi.on("input", (event, context) => {
|
|
@@ -26,6 +26,14 @@ 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;
|
|
29
37
|
default:
|
|
30
38
|
return null;
|
|
31
39
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/**
|
|
3
|
+
* Keeps the reply target scoped to one inbound async call chain. Different
|
|
4
|
+
* conversations may run concurrently without overwriting each other's target.
|
|
5
|
+
*/
|
|
6
|
+
export class InboundConversationContext {
|
|
7
|
+
storage = new AsyncLocalStorage();
|
|
8
|
+
run(conversation, callback) {
|
|
9
|
+
return this.storage.run(conversation, callback);
|
|
10
|
+
}
|
|
11
|
+
get() {
|
|
12
|
+
return this.storage.getStore();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function tryBeginTurn(state, task, conversation) {
|
|
16
|
+
if (state.activeTask) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const lease = {
|
|
20
|
+
task,
|
|
21
|
+
previousConversation: state.activeConversation,
|
|
22
|
+
};
|
|
23
|
+
state.activeTask = task;
|
|
24
|
+
if (conversation) {
|
|
25
|
+
state.activeConversation = conversation;
|
|
26
|
+
state.lastConversation = conversation;
|
|
27
|
+
}
|
|
28
|
+
return lease;
|
|
29
|
+
}
|
|
30
|
+
export function rollbackTurn(state, lease) {
|
|
31
|
+
if (state.activeTask !== lease.task) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
state.activeTask = null;
|
|
35
|
+
state.activeConversation = lease.previousConversation;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
export function clearTurn(state, expectedTask) {
|
|
39
|
+
if (expectedTask && state.activeTask !== expectedTask) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
state.activeTask = null;
|
|
43
|
+
state.activeConversation = undefined;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export function resolveConversationTarget(params) {
|
|
47
|
+
return params.active ?? params.last ?? params.fallback;
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
function normalizeComparablePath(filePath) {
|
|
4
|
+
const normalized = path.resolve(filePath);
|
|
5
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
6
|
+
}
|
|
7
|
+
export function isDirectModuleRun(importMetaUrl, argv = process.argv, importMetaMain) {
|
|
8
|
+
if (typeof importMetaMain === "boolean") {
|
|
9
|
+
return importMetaMain;
|
|
10
|
+
}
|
|
11
|
+
const entryPath = argv[1];
|
|
12
|
+
if (!entryPath) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return normalizeComparablePath(fileURLToPath(importMetaUrl)) ===
|
|
16
|
+
normalizeComparablePath(entryPath);
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const DEFAULT_INBOUND_MESSAGE_CLAIM_TTL_MS = 10 * 60 * 1000;
|
|
5
|
+
export function buildInboundMessageClaimPath(messageKey, claimsDir) {
|
|
6
|
+
const fileName = `${crypto.createHash("sha1").update(messageKey).digest("hex")}.json`;
|
|
7
|
+
return path.join(claimsDir, fileName);
|
|
8
|
+
}
|
|
9
|
+
export function tryClaimChannelInboundMessage(messageKey, options) {
|
|
10
|
+
if (!messageKey) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
14
|
+
const ttlMs = options.ttlMs ?? DEFAULT_INBOUND_MESSAGE_CLAIM_TTL_MS;
|
|
15
|
+
const claimPath = buildInboundMessageClaimPath(messageKey, options.claimsDir);
|
|
16
|
+
const attemptClaim = () => {
|
|
17
|
+
fs.mkdirSync(options.claimsDir, { recursive: true });
|
|
18
|
+
const handle = fs.openSync(claimPath, "wx");
|
|
19
|
+
try {
|
|
20
|
+
fs.writeFileSync(handle, JSON.stringify({
|
|
21
|
+
key: messageKey,
|
|
22
|
+
claimedAt: new Date(nowMs).toISOString(),
|
|
23
|
+
pid: process.pid,
|
|
24
|
+
}, null, 2), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
fs.closeSync(handle);
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
return attemptClaim();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const code = typeof error === "object" &&
|
|
36
|
+
error !== null &&
|
|
37
|
+
"code" in error &&
|
|
38
|
+
typeof error.code === "string"
|
|
39
|
+
? error.code
|
|
40
|
+
: "";
|
|
41
|
+
if (code !== "EEXIST") {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(claimPath);
|
|
47
|
+
if (Number.isFinite(stat.mtimeMs) && nowMs - stat.mtimeMs > ttlMs) {
|
|
48
|
+
fs.rmSync(claimPath, { force: true });
|
|
49
|
+
return attemptClaim();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
try {
|
|
54
|
+
return attemptClaim();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
@@ -23,6 +23,9 @@ function normalizeDaemonEndpoint(value) {
|
|
|
23
23
|
token: record.token,
|
|
24
24
|
cwd: record.cwd,
|
|
25
25
|
startedAt: record.startedAt,
|
|
26
|
+
...(record.channelId === "wechat" || record.channelId === "wecom"
|
|
27
|
+
? { channelId: record.channelId }
|
|
28
|
+
: {}),
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
export function buildDaemonToken() {
|