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.
Files changed (52) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +454 -475
  3. package/bin/_run-entry.mjs +23 -5
  4. package/bin/wecom-check-update.mjs +5 -0
  5. package/bin/wecom-claude.mjs +5 -0
  6. package/bin/wecom-codex.mjs +5 -0
  7. package/bin/wecom-daemon.mjs +5 -0
  8. package/bin/wecom-opencode.mjs +5 -0
  9. package/bin/wecom-pi.mjs +5 -0
  10. package/bin/wecom-setup.mjs +5 -0
  11. package/dist/bridge/adapter-control.js +58 -0
  12. package/dist/bridge/bridge-adapters.claude.js +155 -4
  13. package/dist/bridge/bridge-adapters.codex.js +112 -7
  14. package/dist/bridge/bridge-adapters.core.js +19 -1
  15. package/dist/bridge/bridge-adapters.opencode.js +150 -1
  16. package/dist/bridge/bridge-adapters.pi.js +39 -0
  17. package/dist/bridge/bridge-process-reaper.js +35 -2
  18. package/dist/bridge/bridge-state.js +10 -3
  19. package/dist/bridge/claude-native-controls.js +133 -0
  20. package/dist/bridge/native-terminal-control.js +253 -0
  21. package/dist/bridge/wechat-bridge.js +389 -87
  22. package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
  23. package/dist/channels/wecom/bridge-final-reply.js +26 -0
  24. package/dist/channels/wecom/setup-cli.js +6 -0
  25. package/dist/channels/wecom/setup.js +176 -0
  26. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  27. package/dist/channels/wecom/wecom-config.js +66 -0
  28. package/dist/channels/wecom/wecom-media.js +141 -0
  29. package/dist/channels/wecom/wecom-message.js +149 -0
  30. package/dist/channels/wecom/wecom-transport.js +308 -0
  31. package/dist/companion/codex-remote-client.js +24 -3
  32. package/dist/companion/codex-visible-client-link.js +101 -0
  33. package/dist/companion/local-companion-start.js +56 -11
  34. package/dist/companion/local-companion.js +19 -1
  35. package/dist/companion/pi-tui-bridge-extension.js +56 -3
  36. package/dist/core/bridge-control.js +8 -0
  37. package/dist/core/channel-types.js +3 -1
  38. package/dist/core/conversation-routing.js +48 -0
  39. package/dist/core/direct-run.js +17 -0
  40. package/dist/core/inbound-message-claims.js +61 -0
  41. package/dist/daemon/daemon-link.js +3 -0
  42. package/dist/daemon/wechat-daemon.js +335 -60
  43. package/dist/i18n/messages-en.js +3 -1
  44. package/dist/i18n/messages-zh.js +3 -1
  45. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  46. package/dist/runtime/runtime-types.js +3 -0
  47. package/dist/utils/doctor.js +30 -10
  48. package/dist/utils/version-checker.js +11 -11
  49. package/dist/wechat/setup.js +113 -13
  50. package/dist/wechat/wechat-transport.js +96 -58
  51. package/package.json +116 -104
  52. 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 = Boolean(import.meta.main);
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
  }
@@ -1 +1,3 @@
1
- export {};
1
+ export function normalizeBridgeChannelId(value) {
2
+ return value === "wecom" ? "wecom" : "wechat";
3
+ }
@@ -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() {