cli-wechat-bridge 1.0.5

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 (54) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +637 -0
  3. package/bin/_run-entry.mjs +35 -0
  4. package/bin/wechat-bridge-claude.mjs +5 -0
  5. package/bin/wechat-bridge-codex.mjs +5 -0
  6. package/bin/wechat-bridge-opencode.mjs +5 -0
  7. package/bin/wechat-bridge-shell.mjs +5 -0
  8. package/bin/wechat-bridge.mjs +5 -0
  9. package/bin/wechat-check-update.mjs +5 -0
  10. package/bin/wechat-claude-start.mjs +5 -0
  11. package/bin/wechat-claude.mjs +5 -0
  12. package/bin/wechat-codex-start.mjs +5 -0
  13. package/bin/wechat-codex.mjs +5 -0
  14. package/bin/wechat-daemon.mjs +5 -0
  15. package/bin/wechat-opencode-start.mjs +5 -0
  16. package/bin/wechat-opencode.mjs +5 -0
  17. package/bin/wechat-setup.mjs +5 -0
  18. package/dist/bridge/bridge-adapter-common.js +95 -0
  19. package/dist/bridge/bridge-adapters.claude.js +829 -0
  20. package/dist/bridge/bridge-adapters.codex.js +2228 -0
  21. package/dist/bridge/bridge-adapters.core.js +717 -0
  22. package/dist/bridge/bridge-adapters.js +26 -0
  23. package/dist/bridge/bridge-adapters.opencode.js +2129 -0
  24. package/dist/bridge/bridge-adapters.shared.js +1005 -0
  25. package/dist/bridge/bridge-adapters.shell.js +363 -0
  26. package/dist/bridge/bridge-controller.js +48 -0
  27. package/dist/bridge/bridge-final-reply.js +46 -0
  28. package/dist/bridge/bridge-process-reaper.js +348 -0
  29. package/dist/bridge/bridge-state.js +362 -0
  30. package/dist/bridge/bridge-types.js +1 -0
  31. package/dist/bridge/bridge-utils.js +1240 -0
  32. package/dist/bridge/claude-hook.js +82 -0
  33. package/dist/bridge/claude-hooks.js +267 -0
  34. package/dist/bridge/wechat-bridge.js +1026 -0
  35. package/dist/commands/check-update.js +30 -0
  36. package/dist/companion/codex-panel-link.js +72 -0
  37. package/dist/companion/codex-panel.js +179 -0
  38. package/dist/companion/codex-remote-client.js +124 -0
  39. package/dist/companion/local-companion-link.js +240 -0
  40. package/dist/companion/local-companion-start.js +420 -0
  41. package/dist/companion/local-companion.js +424 -0
  42. package/dist/daemon/daemon-link.js +175 -0
  43. package/dist/daemon/wechat-daemon.js +1202 -0
  44. package/dist/media/media-types.js +1 -0
  45. package/dist/runtime/create-runtime-host.js +12 -0
  46. package/dist/runtime/legacy-adapter-runtime.js +46 -0
  47. package/dist/runtime/runtime-types.js +5 -0
  48. package/dist/utils/version-checker.js +161 -0
  49. package/dist/wechat/channel-config.js +196 -0
  50. package/dist/wechat/setup.js +283 -0
  51. package/dist/wechat/standalone-bot.js +355 -0
  52. package/dist/wechat/wechat-channel.js +492 -0
  53. package/dist/wechat/wechat-transport.js +1213 -0
  54. package/package.json +101 -0
@@ -0,0 +1,1005 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import net from "node:net";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawn as spawnChild, spawnSync } from "node:child_process";
6
+ import { spawn as spawnPty } from "node-pty";
7
+ import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, sendLocalCompanionMessage, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
8
+ import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
9
+ import { buildClaudeFailureMessage, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeResumeConversationId, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
10
+ import { detectCliApproval, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
11
+ import { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
12
+ export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
13
+ export const DEFAULT_COLS = 120;
14
+ export const DEFAULT_ROWS = 30;
15
+ export const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
16
+ export const WINDOWS_DIRECT_EXECUTABLE_EXTENSIONS = [".exe", ".cmd", ".bat", ".com"];
17
+ export const WINDOWS_POWERSHELL_EXTENSION = ".ps1";
18
+ export const CODEX_SESSION_POLL_INTERVAL_MS = 500;
19
+ export const CODEX_SESSION_MATCH_WINDOW_MS = 30_000;
20
+ export const CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS = 5_000;
21
+ export const CODEX_THREAD_SIGNAL_TTL_MS = 30_000;
22
+ export const CODEX_RECENT_SESSION_KEY_LIMIT = 64;
23
+ export const INTERRUPT_SETTLE_DELAY_MS = 1_500;
24
+ export const CODEX_FINAL_REPLY_SETTLE_DELAY_MS = 1_000;
25
+ export const CODEX_STARTUP_WARMUP_MS = 1_200;
26
+ export const CODEX_APP_SERVER_HOST = "127.0.0.1";
27
+ export const CODEX_APP_SERVER_READY_TIMEOUT_MS = 10_000;
28
+ export const CODEX_APP_SERVER_LOG_LIMIT = 12_000;
29
+ export const CODEX_RPC_CONNECT_RETRY_MS = 150;
30
+ export const CODEX_RPC_RECONNECT_TIMEOUT_MS = 5_000;
31
+ export const CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS = 15_000;
32
+ export const LOCAL_COMPANION_RECONNECT_GRACE_MS = 15_000;
33
+ export const CLAUDE_HOOK_LISTEN_HOST = "127.0.0.1";
34
+ export const CLAUDE_HELP_PROBE_TIMEOUT_MS = 5_000;
35
+ export const CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS = 12_000;
36
+ export const DEFAULT_UNIX_SHELL_CANDIDATES = ["pwsh", "bash", "zsh", "sh"];
37
+ export const POSIX_SHELL_NAMES = new Set(["bash", "zsh", "sh", "dash", "ksh"]);
38
+ export const CLAUDE_FLAG_SUPPORT_CACHE = new Map();
39
+ export const OPENCODE_SERVER_HOST = "127.0.0.1";
40
+ export const OPENCODE_SERVER_READY_TIMEOUT_MS = 10_000;
41
+ export const OPENCODE_SSE_RECONNECT_DELAY_MS = 2_000;
42
+ export const OPENCODE_SESSION_IDLE_SETTLE_MS = 1_500;
43
+ export const OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS = 12_000;
44
+ export function buildCodexCliArgs(remoteUrl, options = {}) {
45
+ assertNoReservedExtraCliArgs(options.extraCliArgs ?? [], ["--remote", "--remote-auth-token-env"], "Codex remote connection");
46
+ const args = [];
47
+ if (options.resumeThreadId) {
48
+ args.push("resume", options.resumeThreadId);
49
+ }
50
+ args.push("--enable", "tui_app_server", "--remote", remoteUrl);
51
+ if (options.inlineMode) {
52
+ args.push("--no-alt-screen");
53
+ }
54
+ if (options.profile) {
55
+ args.push("--profile", options.profile);
56
+ }
57
+ return [...args, ...(options.extraCliArgs ?? [])];
58
+ }
59
+ export function hasClaudeNoAltScreenOption(helpText) {
60
+ return helpText.includes("--no-alt-screen");
61
+ }
62
+ export function buildClaudeCliArgs(options) {
63
+ assertNoReservedExtraCliArgs(options.extraCliArgs ?? [], ["--settings"], "Claude companion settings");
64
+ const args = [];
65
+ if (options.includeNoAltScreen) {
66
+ args.push("--no-alt-screen");
67
+ }
68
+ args.push("--settings", options.settingsFilePath);
69
+ if (options.resumeConversationId) {
70
+ args.push("--resume", options.resumeConversationId);
71
+ }
72
+ if (options.profile) {
73
+ args.push("--profile", options.profile);
74
+ }
75
+ return [...args, ...(options.extraCliArgs ?? [])];
76
+ }
77
+ export function assertNoReservedExtraCliArgs(args, reservedOptions, owner) {
78
+ const blocked = args.find((arg) => reservedOptions.some((option) => arg === option || arg.startsWith(`${option}=`)));
79
+ if (!blocked) {
80
+ return;
81
+ }
82
+ throw new Error(`${owner} is managed by the WeChat bridge; do not pass ${blocked} as an extra CLI argument.`);
83
+ }
84
+ export function isClaudeInvalidResumeError(text) {
85
+ const normalized = normalizeOutput(text);
86
+ if (!normalized) {
87
+ return false;
88
+ }
89
+ return (normalized.includes("No conversation found with session ID:") ||
90
+ normalized.includes("No conversation found with session name:") ||
91
+ normalized.includes("No conversation found with session:"));
92
+ }
93
+ export function shouldIncludeClaudeNoAltScreen(command) {
94
+ let spawnTarget;
95
+ try {
96
+ spawnTarget = resolveSpawnTarget(command, "claude");
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ const cacheKey = `${spawnTarget.file}\u0000${spawnTarget.args.join("\u0000")}`;
102
+ const cached = CLAUDE_FLAG_SUPPORT_CACHE.get(cacheKey);
103
+ if (cached !== undefined) {
104
+ return cached;
105
+ }
106
+ let supported;
107
+ try {
108
+ const probe = spawnSync(spawnTarget.file, [...spawnTarget.args, "--help"], {
109
+ cwd: process.cwd(),
110
+ env: buildCliEnvironment("claude"),
111
+ encoding: "utf8",
112
+ timeout: CLAUDE_HELP_PROBE_TIMEOUT_MS,
113
+ windowsHide: true,
114
+ });
115
+ const output = `${probe.stdout ?? ""}\n${probe.stderr ?? ""}`;
116
+ supported = hasClaudeNoAltScreenOption(output);
117
+ }
118
+ catch {
119
+ supported = false;
120
+ }
121
+ CLAUDE_FLAG_SUPPORT_CACHE.set(cacheKey, supported);
122
+ return supported;
123
+ }
124
+ export function buildCodexApprovalRequest(method, params) {
125
+ if (!isRecord(params)) {
126
+ return null;
127
+ }
128
+ if (method === "item/commandExecution/requestApproval") {
129
+ const command = typeof params.command === "string" ? params.command : "";
130
+ const cwd = typeof params.cwd === "string" ? params.cwd : "";
131
+ const reason = typeof params.reason === "string" ? params.reason : "";
132
+ const preview = command && cwd
133
+ ? `${command} (${cwd})`
134
+ : command || reason || "Command execution approval requested.";
135
+ return {
136
+ source: "cli",
137
+ summary: reason
138
+ ? `Codex needs approval before running a command: ${truncatePreview(reason, 160)}`
139
+ : "Codex needs approval before running a command.",
140
+ commandPreview: truncatePreview(preview, 180),
141
+ };
142
+ }
143
+ if (method === "item/fileChange/requestApproval") {
144
+ const grantRoot = typeof params.grantRoot === "string" ? params.grantRoot : "";
145
+ const reason = typeof params.reason === "string" ? params.reason : "";
146
+ const preview = grantRoot || reason || "File change approval requested.";
147
+ return {
148
+ source: "cli",
149
+ summary: reason
150
+ ? `Codex needs approval before applying a file change: ${truncatePreview(reason, 160)}`
151
+ : "Codex needs approval before applying a file change.",
152
+ commandPreview: truncatePreview(preview, 180),
153
+ };
154
+ }
155
+ return null;
156
+ }
157
+ export function buildCodexUserInputRequest(params) {
158
+ if (!isRecord(params) || !Array.isArray(params.questions)) {
159
+ return null;
160
+ }
161
+ const questions = params.questions
162
+ .map((question) => {
163
+ if (!isRecord(question)) {
164
+ return null;
165
+ }
166
+ const id = typeof question.id === "string" ? question.id.trim() : "";
167
+ const header = typeof question.header === "string" ? question.header.trim() : "";
168
+ const prompt = typeof question.question === "string" ? normalizeOutput(question.question).trim() : "";
169
+ if (!id || !header || !prompt) {
170
+ return null;
171
+ }
172
+ const options = Array.isArray(question.options)
173
+ ? question.options
174
+ .map((option) => {
175
+ if (!isRecord(option)) {
176
+ return null;
177
+ }
178
+ const label = typeof option.label === "string" ? option.label.trim() : "";
179
+ const description = typeof option.description === "string"
180
+ ? normalizeOutput(option.description).trim()
181
+ : "";
182
+ if (!label || !description) {
183
+ return null;
184
+ }
185
+ return {
186
+ label,
187
+ description,
188
+ };
189
+ })
190
+ .filter((option) => Boolean(option))
191
+ : null;
192
+ return {
193
+ id,
194
+ header,
195
+ question: prompt,
196
+ isOther: question.isOther === true,
197
+ isSecret: question.isSecret === true,
198
+ options,
199
+ };
200
+ })
201
+ .filter((question) => Boolean(question));
202
+ if (questions.length === 0) {
203
+ return null;
204
+ }
205
+ return {
206
+ summary: "Codex needs more information before the tool can continue.",
207
+ questions,
208
+ };
209
+ }
210
+ export function extractCodexFinalTextFromItem(item) {
211
+ if (!isRecord(item) || item.type !== "agentMessage" || item.phase !== "final_answer") {
212
+ return null;
213
+ }
214
+ const text = typeof item.text === "string" ? normalizeOutput(item.text).trim() : "";
215
+ return text || null;
216
+ }
217
+ export function extractCodexUserMessageText(item) {
218
+ if (!isRecord(item) || item.type !== "userMessage" || !Array.isArray(item.content)) {
219
+ return null;
220
+ }
221
+ const parts = item.content
222
+ .map((entry) => {
223
+ if (!isRecord(entry) || typeof entry.type !== "string") {
224
+ return "";
225
+ }
226
+ switch (entry.type) {
227
+ case "text":
228
+ return typeof entry.text === "string" ? entry.text : "";
229
+ case "image":
230
+ return "[image]";
231
+ case "localImage":
232
+ return typeof entry.path === "string" ? `[local image: ${entry.path}]` : "[local image]";
233
+ case "skill":
234
+ return typeof entry.name === "string" ? `[skill: ${entry.name}]` : "[skill]";
235
+ case "mention":
236
+ return typeof entry.name === "string" ? `[mention: ${entry.name}]` : "[mention]";
237
+ default:
238
+ return "";
239
+ }
240
+ })
241
+ .filter(Boolean);
242
+ const text = normalizeOutput(parts.join("\n")).trim();
243
+ return text || null;
244
+ }
245
+ export function extractCodexThreadFollowIdFromStatusChanged(params) {
246
+ if (!isRecord(params)) {
247
+ return null;
248
+ }
249
+ const threadId = getNotificationThreadId(params);
250
+ if (!threadId) {
251
+ return null;
252
+ }
253
+ const status = isRecord(params.status) ? params.status : null;
254
+ if (!status) {
255
+ return threadId;
256
+ }
257
+ const statusType = typeof status.type === "string" ? status.type : "";
258
+ if (statusType === "notLoaded") {
259
+ return null;
260
+ }
261
+ if (statusType === "active" || statusType === "idle" || statusType === "systemError") {
262
+ return threadId;
263
+ }
264
+ return threadId;
265
+ }
266
+ export function extractCodexThreadStartedThreadId(params) {
267
+ if (!isRecord(params) || !isRecord(params.thread)) {
268
+ return null;
269
+ }
270
+ return typeof params.thread.id === "string" ? params.thread.id : null;
271
+ }
272
+ export function shouldIgnoreCodexSessionReplayEntry(timestamp, ignoreBeforeMs) {
273
+ if (ignoreBeforeMs === null) {
274
+ return false;
275
+ }
276
+ if (typeof timestamp !== "string") {
277
+ return true;
278
+ }
279
+ const parsedTimestampMs = Date.parse(timestamp);
280
+ if (!Number.isFinite(parsedTimestampMs)) {
281
+ return true;
282
+ }
283
+ return parsedTimestampMs < ignoreBeforeMs;
284
+ }
285
+ export function shouldRecoverCodexStaleBusyState(params) {
286
+ return (params.status === "busy" &&
287
+ !params.pendingTurnStart &&
288
+ !params.hasActiveTurn &&
289
+ !params.hasPendingApproval &&
290
+ !params.activeTurnId);
291
+ }
292
+ export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
293
+ return (typeof params.candidateTurnId === "string" &&
294
+ params.activeTurnId === params.candidateTurnId &&
295
+ params.activeTurnOrigin === "wechat" &&
296
+ !params.pendingTurnStart &&
297
+ !params.hasPendingApproval &&
298
+ params.hasFinalOutput &&
299
+ !params.hasCompletedTurn &&
300
+ typeof params.lastActivityAtMs === "number" &&
301
+ Number.isFinite(params.lastActivityAtMs) &&
302
+ params.nowMs - params.lastActivityAtMs >= params.settleDelayMs);
303
+ }
304
+ export function getEnvValue(env, key) {
305
+ const direct = env[key];
306
+ if (direct !== undefined) {
307
+ return direct;
308
+ }
309
+ const matchedKey = Object.keys(env).find((candidate) => candidate.toLowerCase() === key.toLowerCase());
310
+ return matchedKey ? env[matchedKey] : undefined;
311
+ }
312
+ export function fileExists(filePath) {
313
+ try {
314
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
315
+ }
316
+ catch {
317
+ return false;
318
+ }
319
+ }
320
+ export function isPathLikeCommand(command) {
321
+ return (path.isAbsolute(command) ||
322
+ command.startsWith(".") ||
323
+ command.includes("/") ||
324
+ command.includes("\\"));
325
+ }
326
+ export function getWindowsCommandExtensions(env) {
327
+ const configured = (getEnvValue(env, "PATHEXT") ?? "")
328
+ .split(";")
329
+ .map((entry) => entry.trim().toLowerCase())
330
+ .filter(Boolean);
331
+ const ordered = [...WINDOWS_DIRECT_EXECUTABLE_EXTENSIONS, "", WINDOWS_POWERSHELL_EXTENSION];
332
+ for (const extension of configured) {
333
+ if (!ordered.includes(extension)) {
334
+ ordered.push(extension);
335
+ }
336
+ }
337
+ return ordered;
338
+ }
339
+ export function expandCommandCandidates(command, platform, env) {
340
+ if (platform !== "win32") {
341
+ return [command];
342
+ }
343
+ if (path.extname(command)) {
344
+ return [command];
345
+ }
346
+ return getWindowsCommandExtensions(env).map((extension) => `${command}${extension}`);
347
+ }
348
+ export function resolvePathLikeCommand(command, platform, env) {
349
+ const absoluteCommand = path.resolve(command);
350
+ for (const candidate of expandCommandCandidates(absoluteCommand, platform, env)) {
351
+ if (fileExists(candidate)) {
352
+ return candidate;
353
+ }
354
+ }
355
+ return undefined;
356
+ }
357
+ export function findCommandOnPath(command, platform, env) {
358
+ const pathEntries = (getEnvValue(env, "PATH") ?? "")
359
+ .split(path.delimiter)
360
+ .map((entry) => entry.trim())
361
+ .filter(Boolean);
362
+ const candidates = expandCommandCandidates(command, platform, env);
363
+ for (const directory of pathEntries) {
364
+ for (const candidate of candidates) {
365
+ const candidatePath = path.join(directory, candidate);
366
+ if (fileExists(candidatePath)) {
367
+ return candidatePath;
368
+ }
369
+ }
370
+ }
371
+ return undefined;
372
+ }
373
+ export function resolveCommandPath(command, platform, env) {
374
+ if (isPathLikeCommand(command)) {
375
+ return resolvePathLikeCommand(command, platform, env);
376
+ }
377
+ return findCommandOnPath(command, platform, env);
378
+ }
379
+ export function resolveCmdExe(env) {
380
+ const systemRoot = getEnvValue(env, "SystemRoot") ?? getEnvValue(env, "SYSTEMROOT");
381
+ const configured = getEnvValue(env, "ComSpec") ??
382
+ getEnvValue(env, "COMSPEC") ??
383
+ (systemRoot ? `${systemRoot.replace(/[\\/]$/, "")}\\System32\\cmd.exe` : undefined);
384
+ return configured || "cmd.exe";
385
+ }
386
+ export function quoteForCmd(argument) {
387
+ if (!argument) {
388
+ return '""';
389
+ }
390
+ if (!/[\s"]/u.test(argument)) {
391
+ return argument;
392
+ }
393
+ return `"${argument.replace(/"/g, '""')}"`;
394
+ }
395
+ export function wrapWithCmdExe(scriptPath, extraArgs, env) {
396
+ const commandLine = [quoteForCmd(scriptPath), ...extraArgs.map(quoteForCmd)].join(" ");
397
+ return {
398
+ file: resolveCmdExe(env),
399
+ args: ["/d", "/s", "/c", commandLine],
400
+ };
401
+ }
402
+ export function resolveBundledWindowsExe(kind, launcherPath) {
403
+ const launcherDirectory = path.dirname(launcherPath);
404
+ const openAiDirectory = path.join(launcherDirectory, "node_modules", "@openai");
405
+ if (!fs.existsSync(openAiDirectory)) {
406
+ return undefined;
407
+ }
408
+ const vendorSegments = [
409
+ "vendor",
410
+ "x86_64-pc-windows-msvc",
411
+ kind,
412
+ `${kind}.exe`,
413
+ ];
414
+ const directCandidate = path.join(openAiDirectory, `${kind}-win32-x64`, ...vendorSegments);
415
+ if (fileExists(directCandidate)) {
416
+ return directCandidate;
417
+ }
418
+ const packageCandidate = path.join(openAiDirectory, kind, "node_modules", "@openai", `${kind}-win32-x64`, ...vendorSegments);
419
+ if (fileExists(packageCandidate)) {
420
+ return packageCandidate;
421
+ }
422
+ const dirEntries = fs.readdirSync(openAiDirectory, { withFileTypes: true });
423
+ for (const entry of dirEntries) {
424
+ if (!entry.isDirectory() || !entry.name.startsWith(`.${kind}-`)) {
425
+ continue;
426
+ }
427
+ const nestedCandidate = path.join(openAiDirectory, entry.name, "node_modules", "@openai", `${kind}-win32-x64`, ...vendorSegments);
428
+ if (fileExists(nestedCandidate)) {
429
+ return nestedCandidate;
430
+ }
431
+ }
432
+ return undefined;
433
+ }
434
+ export function copyDefinedEnv(env) {
435
+ const result = {};
436
+ for (const [key, value] of Object.entries(env)) {
437
+ if (typeof value === "string") {
438
+ result[key] = value;
439
+ }
440
+ }
441
+ return result;
442
+ }
443
+ function mergeNoProxyValue(value) {
444
+ const requiredHosts = ["127.0.0.1", "localhost", "::1"];
445
+ const merged = new Set((value ?? "")
446
+ .split(",")
447
+ .map((entry) => entry.trim())
448
+ .filter(Boolean));
449
+ for (const host of requiredHosts) {
450
+ merged.add(host);
451
+ }
452
+ return Array.from(merged).join(",");
453
+ }
454
+ function applyLoopbackNoProxy(env) {
455
+ env.NO_PROXY = mergeNoProxyValue(env.NO_PROXY);
456
+ env.no_proxy = mergeNoProxyValue(env.no_proxy);
457
+ return env;
458
+ }
459
+ export function resolveDefaultAdapterCommand(kind, options = {}) {
460
+ const platform = options.platform ?? process.platform;
461
+ if (kind !== "shell") {
462
+ return kind;
463
+ }
464
+ if (platform === "win32") {
465
+ return "powershell.exe";
466
+ }
467
+ const env = options.env ?? process.env;
468
+ for (const candidate of DEFAULT_UNIX_SHELL_CANDIDATES) {
469
+ if (resolveCommandPath(candidate, platform, env)) {
470
+ return candidate;
471
+ }
472
+ }
473
+ throw new Error(`No default shell executable was found on ${platform}. Tried: ${DEFAULT_UNIX_SHELL_CANDIDATES.join(", ")}. Use --cmd <executable>.`);
474
+ }
475
+ export function buildCliEnvironment(kind, options = {}) {
476
+ const sourceEnv = options.env ?? process.env;
477
+ const platform = options.platform ?? process.platform;
478
+ if (kind === "codex" || kind === "claude" || kind === "opencode") {
479
+ if (platform !== "win32") {
480
+ return applyLoopbackNoProxy({
481
+ ...copyDefinedEnv(sourceEnv),
482
+ TERM: sourceEnv.TERM || "xterm-256color",
483
+ });
484
+ }
485
+ const env = {
486
+ TERM: sourceEnv.TERM || "xterm-256color",
487
+ };
488
+ const keys = [
489
+ "PATH",
490
+ "PATHEXT",
491
+ "ComSpec",
492
+ "COMSPEC",
493
+ "SystemRoot",
494
+ "SYSTEMROOT",
495
+ "USERPROFILE",
496
+ "HOME",
497
+ "APPDATA",
498
+ "LOCALAPPDATA",
499
+ "TEMP",
500
+ "TMP",
501
+ "OS",
502
+ "ProgramFiles",
503
+ "ProgramFiles(x86)",
504
+ "CommonProgramFiles",
505
+ "CommonProgramFiles(x86)",
506
+ "HTTP_PROXY",
507
+ "HTTPS_PROXY",
508
+ "ALL_PROXY",
509
+ "http_proxy",
510
+ "https_proxy",
511
+ "all_proxy",
512
+ "NO_PROXY",
513
+ "no_proxy",
514
+ ];
515
+ for (const key of keys) {
516
+ const value = sourceEnv[key];
517
+ if (value) {
518
+ env[key] = value;
519
+ }
520
+ }
521
+ if (!env.HOME && env.USERPROFILE) {
522
+ env.HOME = env.USERPROFILE;
523
+ }
524
+ return applyLoopbackNoProxy(env);
525
+ }
526
+ return {
527
+ ...copyDefinedEnv(sourceEnv),
528
+ TERM: sourceEnv.TERM || "xterm-256color",
529
+ };
530
+ }
531
+ export function buildPtySpawnOptions(params) {
532
+ const options = {
533
+ name: "xterm-color",
534
+ cols: DEFAULT_COLS,
535
+ rows: DEFAULT_ROWS,
536
+ cwd: params.cwd,
537
+ env: params.env,
538
+ };
539
+ if ((params.platform ?? process.platform) === "win32") {
540
+ options.useConpty = true;
541
+ }
542
+ return options;
543
+ }
544
+ export function normalizeShellCommandName(command) {
545
+ return path.parse(path.basename(command)).name.toLowerCase();
546
+ }
547
+ export function resolveShellRuntime(command, options = {}) {
548
+ const platform = options.platform ?? process.platform;
549
+ const name = normalizeShellCommandName(command);
550
+ if (name === "powershell" || name === "pwsh") {
551
+ return {
552
+ family: "powershell",
553
+ launchArgs: platform === "win32"
554
+ ? ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-NoExit"]
555
+ : ["-NoLogo", "-NoProfile", "-NoExit"],
556
+ };
557
+ }
558
+ if (POSIX_SHELL_NAMES.has(name)) {
559
+ return {
560
+ family: "posix",
561
+ launchArgs: ["-i"],
562
+ };
563
+ }
564
+ throw new Error(`Unsupported shell executable for shell adapter: ${command}. Supported shells: powershell, pwsh, bash, zsh, sh, dash, ksh.`);
565
+ }
566
+ export function escapePowerShellString(text) {
567
+ return text.replace(/`/g, "``").replace(/"/g, '`"');
568
+ }
569
+ export function escapePosixShellString(text) {
570
+ return `'${text.replace(/'/g, `'"'"'`)}'`;
571
+ }
572
+ export function buildShellProfileCommand(profilePath, family) {
573
+ const resolved = path.resolve(profilePath);
574
+ if (family === "powershell") {
575
+ return `. "${escapePowerShellString(resolved)}"`;
576
+ }
577
+ return `. ${escapePosixShellString(resolved)}`;
578
+ }
579
+ export function buildShellInputPayload(text, family, completionMarker = "__WECHAT_BRIDGE_DONE__") {
580
+ if (family === "powershell") {
581
+ const encodedCommand = Buffer.from(text, "utf8").toString("base64");
582
+ const script = [
583
+ "$__wechatBridgePreviousErrorActionPreference = $ErrorActionPreference",
584
+ "$ErrorActionPreference = 'Continue'",
585
+ "$global:LASTEXITCODE = 0",
586
+ "try {",
587
+ ` $decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${escapePowerShellString(encodedCommand)}"))`,
588
+ " $scriptBlock = [scriptblock]::Create($decoded)",
589
+ " & $scriptBlock",
590
+ "} catch {",
591
+ " Write-Error $_",
592
+ " $global:LASTEXITCODE = 1",
593
+ "} finally {",
594
+ " if (-not ($global:LASTEXITCODE -is [int])) { $global:LASTEXITCODE = 0 }",
595
+ ` Write-Output "${escapePowerShellString(completionMarker)}:$global:LASTEXITCODE"`,
596
+ " $ErrorActionPreference = $__wechatBridgePreviousErrorActionPreference",
597
+ "}",
598
+ "",
599
+ ];
600
+ return `${script.join("\r")}\r`;
601
+ }
602
+ const script = [
603
+ text,
604
+ "__wechat_bridge_status=$?",
605
+ `printf '%s:%s\\n' ${escapePosixShellString(completionMarker)} "$__wechat_bridge_status"`,
606
+ "",
607
+ ];
608
+ return `${script.join("\r")}\r`;
609
+ }
610
+ export async function reserveLocalPort() {
611
+ return await new Promise((resolve, reject) => {
612
+ const server = net.createServer();
613
+ server.unref();
614
+ server.on("error", reject);
615
+ server.listen(0, CODEX_APP_SERVER_HOST, () => {
616
+ const address = server.address();
617
+ if (!address || typeof address === "string") {
618
+ server.close(() => reject(new Error("Could not reserve a local app-server port.")));
619
+ return;
620
+ }
621
+ const { port } = address;
622
+ server.close((closeError) => {
623
+ if (closeError) {
624
+ reject(closeError);
625
+ return;
626
+ }
627
+ resolve(port);
628
+ });
629
+ });
630
+ });
631
+ }
632
+ export async function waitForTcpPort(host, port, timeoutMs) {
633
+ const deadline = Date.now() + timeoutMs;
634
+ while (Date.now() < deadline) {
635
+ const connected = await new Promise((resolve) => {
636
+ const socket = net.connect({ host, port });
637
+ const finish = (value) => {
638
+ socket.removeAllListeners();
639
+ socket.destroy();
640
+ resolve(value);
641
+ };
642
+ socket.once("connect", () => finish(true));
643
+ socket.once("timeout", () => finish(false));
644
+ socket.once("error", () => finish(false));
645
+ socket.setTimeout(500);
646
+ });
647
+ if (connected) {
648
+ return;
649
+ }
650
+ await new Promise((resolve) => setTimeout(resolve, 150));
651
+ }
652
+ throw new Error(`Timed out waiting for app-server on ${host}:${port}.`);
653
+ }
654
+ export async function delay(ms) {
655
+ if (ms <= 0) {
656
+ return;
657
+ }
658
+ await new Promise((resolve) => setTimeout(resolve, ms));
659
+ }
660
+ export function appendBoundedLog(existing, chunk) {
661
+ const next = existing ? `${existing}${chunk}` : chunk;
662
+ if (next.length <= CODEX_APP_SERVER_LOG_LIMIT) {
663
+ return next;
664
+ }
665
+ return next.slice(next.length - CODEX_APP_SERVER_LOG_LIMIT);
666
+ }
667
+ export function normalizeComparablePath(filePath) {
668
+ return path.resolve(filePath).replace(/\//g, "\\").toLowerCase();
669
+ }
670
+ export function buildCodexSessionDayPath(date) {
671
+ const homeDirectory = process.env.USERPROFILE ?? process.env.HOME;
672
+ if (!homeDirectory) {
673
+ return null;
674
+ }
675
+ return path.join(homeDirectory, ".codex", "sessions", String(date.getFullYear()), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0"));
676
+ }
677
+ export function buildCodexSessionsRoot() {
678
+ const homeDirectory = process.env.USERPROFILE ?? process.env.HOME;
679
+ if (!homeDirectory) {
680
+ return null;
681
+ }
682
+ return path.join(homeDirectory, ".codex", "sessions");
683
+ }
684
+ export function listCodexSessionFilesRecursively(rootDirectory) {
685
+ if (!fs.existsSync(rootDirectory)) {
686
+ return [];
687
+ }
688
+ const files = [];
689
+ const pending = [rootDirectory];
690
+ while (pending.length > 0) {
691
+ const current = pending.pop();
692
+ if (!current) {
693
+ continue;
694
+ }
695
+ let entries;
696
+ try {
697
+ entries = fs.readdirSync(current, { withFileTypes: true });
698
+ }
699
+ catch {
700
+ continue;
701
+ }
702
+ for (const entry of entries) {
703
+ const entryPath = path.join(current, entry.name);
704
+ if (entry.isDirectory()) {
705
+ pending.push(entryPath);
706
+ continue;
707
+ }
708
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
709
+ files.push(entryPath);
710
+ }
711
+ }
712
+ }
713
+ return files;
714
+ }
715
+ export function readCodexSessionMeta(filePath) {
716
+ try {
717
+ const firstLine = fs.readFileSync(filePath, "utf8").split(/\r?\n/, 1)[0]?.trim();
718
+ if (!firstLine) {
719
+ return null;
720
+ }
721
+ const parsed = JSON.parse(firstLine);
722
+ if (parsed.type !== "session_meta" || !parsed.payload) {
723
+ return null;
724
+ }
725
+ return parsed.payload;
726
+ }
727
+ catch {
728
+ return null;
729
+ }
730
+ }
731
+ export function getCodexSessionSource(meta) {
732
+ if (!meta) {
733
+ return null;
734
+ }
735
+ if (typeof meta.source === "string") {
736
+ return meta.source;
737
+ }
738
+ if (isRecord(meta.source) && typeof meta.source.custom === "string") {
739
+ return meta.source.custom;
740
+ }
741
+ return null;
742
+ }
743
+ export function isTrustedCodexFallbackSession(meta) {
744
+ const sessionSource = getCodexSessionSource(meta);
745
+ if (!sessionSource) {
746
+ return false;
747
+ }
748
+ if (sessionSource === "cli") {
749
+ return true;
750
+ }
751
+ const originator = normalizeOutput(meta?.originator ?? "").trim().toLowerCase();
752
+ return sessionSource === "vscode" && originator === "wechat-bridge";
753
+ }
754
+ export function parseCodexSessionUserMessage(line) {
755
+ const trimmed = line.trim();
756
+ if (!trimmed) {
757
+ return null;
758
+ }
759
+ try {
760
+ const parsed = JSON.parse(trimmed);
761
+ if (parsed.type !== "event_msg" || parsed.payload?.type !== "user_message") {
762
+ return null;
763
+ }
764
+ const message = typeof parsed.payload.message === "string"
765
+ ? normalizeOutput(parsed.payload.message).trim()
766
+ : "";
767
+ return message || null;
768
+ }
769
+ catch {
770
+ return null;
771
+ }
772
+ }
773
+ export function summarizeCodexSessionFile(filePath) {
774
+ let content;
775
+ try {
776
+ content = fs.readFileSync(filePath, "utf8");
777
+ }
778
+ catch {
779
+ return null;
780
+ }
781
+ const lines = content.split(/\r?\n/).filter(Boolean);
782
+ const meta = readCodexSessionMeta(filePath);
783
+ if (!meta?.id || !meta.cwd) {
784
+ return null;
785
+ }
786
+ let lastTimestamp = meta.timestamp ?? null;
787
+ let lastUserMessage = null;
788
+ for (const line of lines) {
789
+ const parsedUserMessage = parseCodexSessionUserMessage(line);
790
+ if (parsedUserMessage) {
791
+ lastUserMessage = parsedUserMessage;
792
+ }
793
+ try {
794
+ const parsed = JSON.parse(line);
795
+ if (typeof parsed.timestamp === "string") {
796
+ lastTimestamp = parsed.timestamp;
797
+ }
798
+ }
799
+ catch {
800
+ // Ignore malformed lines while summarizing persisted sessions.
801
+ }
802
+ }
803
+ const stats = fs.statSync(filePath);
804
+ const lastUpdatedAt = lastTimestamp && Number.isFinite(Date.parse(lastTimestamp))
805
+ ? lastTimestamp
806
+ : new Date(stats.mtimeMs).toISOString();
807
+ return {
808
+ threadId: meta.id,
809
+ title: truncatePreview(lastUserMessage ?? meta.id, 120),
810
+ lastUpdatedAt,
811
+ source: getCodexSessionSource(meta) ?? undefined,
812
+ filePath,
813
+ };
814
+ }
815
+ export function matchesCodexSessionMeta(meta, options) {
816
+ if (!meta?.cwd || !meta.id) {
817
+ return false;
818
+ }
819
+ if (normalizeComparablePath(meta.cwd) !== normalizeComparablePath(options.cwd)) {
820
+ return false;
821
+ }
822
+ if (options.threadId && meta.id !== options.threadId) {
823
+ return false;
824
+ }
825
+ const sessionSource = getCodexSessionSource(meta);
826
+ if (options.sessionSource && sessionSource !== options.sessionSource) {
827
+ return false;
828
+ }
829
+ if (options.threadId) {
830
+ return true;
831
+ }
832
+ const sessionStartedAtMs = meta.timestamp ? Date.parse(meta.timestamp) : Number.NaN;
833
+ if (Number.isFinite(sessionStartedAtMs) &&
834
+ sessionStartedAtMs < options.startedAtMs - CODEX_SESSION_MATCH_WINDOW_MS) {
835
+ return false;
836
+ }
837
+ return true;
838
+ }
839
+ export function findCodexSessionFile(cwd, startedAtMs, options = {}) {
840
+ if (options.threadId) {
841
+ const sessionsRoot = buildCodexSessionsRoot();
842
+ if (!sessionsRoot) {
843
+ return null;
844
+ }
845
+ const candidates = listCodexSessionFilesRecursively(sessionsRoot)
846
+ .map((filePath) => {
847
+ const meta = readCodexSessionMeta(filePath);
848
+ if (!matchesCodexSessionMeta(meta, { cwd, startedAtMs, ...options })) {
849
+ return null;
850
+ }
851
+ const stats = fs.statSync(filePath);
852
+ return {
853
+ filePath,
854
+ modifiedAtMs: stats.mtimeMs,
855
+ };
856
+ })
857
+ .filter((candidate) => Boolean(candidate))
858
+ .sort((left, right) => right.modifiedAtMs - left.modifiedAtMs);
859
+ return candidates[0]?.filePath ?? null;
860
+ }
861
+ const dayDirectories = [new Date(), new Date(startedAtMs), new Date(startedAtMs - 86_400_000)]
862
+ .map(buildCodexSessionDayPath)
863
+ .filter((value) => Boolean(value))
864
+ .filter((value, index, values) => values.indexOf(value) === index)
865
+ .filter((directory) => fs.existsSync(directory));
866
+ const candidates = [];
867
+ for (const directory of dayDirectories) {
868
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
869
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
870
+ continue;
871
+ }
872
+ const filePath = path.join(directory, entry.name);
873
+ const stats = fs.statSync(filePath);
874
+ if (stats.mtimeMs < startedAtMs - CODEX_SESSION_MATCH_WINDOW_MS) {
875
+ continue;
876
+ }
877
+ const meta = readCodexSessionMeta(filePath);
878
+ if (!matchesCodexSessionMeta(meta, { cwd, startedAtMs, ...options })) {
879
+ continue;
880
+ }
881
+ const sessionStartedAtMs = meta?.timestamp ? Date.parse(meta.timestamp) : Number.NaN;
882
+ candidates.push({
883
+ filePath,
884
+ modifiedAtMs: stats.mtimeMs,
885
+ sessionStartedAtMs,
886
+ });
887
+ }
888
+ }
889
+ candidates.sort((left, right) => {
890
+ const leftDistance = Number.isFinite(left.sessionStartedAtMs)
891
+ ? Math.abs(left.sessionStartedAtMs - startedAtMs)
892
+ : Number.POSITIVE_INFINITY;
893
+ const rightDistance = Number.isFinite(right.sessionStartedAtMs)
894
+ ? Math.abs(right.sessionStartedAtMs - startedAtMs)
895
+ : Number.POSITIVE_INFINITY;
896
+ if (leftDistance !== rightDistance) {
897
+ return leftDistance - rightDistance;
898
+ }
899
+ return right.modifiedAtMs - left.modifiedAtMs;
900
+ });
901
+ return candidates[0]?.filePath ?? null;
902
+ }
903
+ export function findRecentCodexSessionFileForCwd(cwd, startedAtMs) {
904
+ const sessionsRoot = buildCodexSessionsRoot();
905
+ if (!sessionsRoot) {
906
+ return null;
907
+ }
908
+ const currentCwd = normalizeComparablePath(cwd);
909
+ let bestCandidate = null;
910
+ for (const filePath of listCodexSessionFilesRecursively(sessionsRoot)) {
911
+ const meta = readCodexSessionMeta(filePath);
912
+ if (!meta?.id || !meta.cwd || normalizeComparablePath(meta.cwd) !== currentCwd) {
913
+ continue;
914
+ }
915
+ if (!isTrustedCodexFallbackSession(meta)) {
916
+ continue;
917
+ }
918
+ let stats;
919
+ try {
920
+ stats = fs.statSync(filePath);
921
+ }
922
+ catch {
923
+ continue;
924
+ }
925
+ if (stats.mtimeMs < startedAtMs - CODEX_SESSION_MATCH_WINDOW_MS) {
926
+ continue;
927
+ }
928
+ if (!bestCandidate || stats.mtimeMs > bestCandidate.modifiedAtMs) {
929
+ bestCandidate = {
930
+ threadId: meta.id,
931
+ filePath,
932
+ modifiedAtMs: stats.mtimeMs,
933
+ };
934
+ }
935
+ }
936
+ return bestCandidate;
937
+ }
938
+ export function listCodexResumeSessions(cwd, limit = 10) {
939
+ const sessionsRoot = buildCodexSessionsRoot();
940
+ if (!sessionsRoot) {
941
+ return [];
942
+ }
943
+ const currentCwd = normalizeComparablePath(cwd);
944
+ const newestByThreadId = new Map();
945
+ for (const filePath of listCodexSessionFilesRecursively(sessionsRoot)) {
946
+ const summary = summarizeCodexSessionFile(filePath);
947
+ if (!summary) {
948
+ continue;
949
+ }
950
+ const meta = readCodexSessionMeta(filePath);
951
+ if (!meta?.cwd || normalizeComparablePath(meta.cwd) !== currentCwd) {
952
+ continue;
953
+ }
954
+ const previous = newestByThreadId.get(summary.threadId);
955
+ if (!previous || Date.parse(summary.lastUpdatedAt) > Date.parse(previous.lastUpdatedAt)) {
956
+ newestByThreadId.set(summary.threadId, summary);
957
+ }
958
+ }
959
+ return Array.from(newestByThreadId.values())
960
+ .sort((left, right) => Date.parse(right.lastUpdatedAt) - Date.parse(left.lastUpdatedAt))
961
+ .slice(0, Math.max(1, limit))
962
+ .map((summary) => ({
963
+ sessionId: summary.threadId,
964
+ threadId: summary.threadId,
965
+ title: summary.title,
966
+ lastUpdatedAt: summary.lastUpdatedAt,
967
+ source: summary.source,
968
+ }));
969
+ }
970
+ export function listCodexResumeThreads(cwd, limit = 10) {
971
+ return listCodexResumeSessions(cwd, limit);
972
+ }
973
+ export function resolveSpawnTarget(command, kind, options = {}) {
974
+ const trimmed = command.trim();
975
+ const platform = options.platform ?? process.platform;
976
+ const env = options.env ?? process.env;
977
+ const forwardArgs = options.forwardArgs ?? [];
978
+ if (!trimmed) {
979
+ return { file: trimmed, args: [...forwardArgs] };
980
+ }
981
+ const resolved = resolveCommandPath(trimmed, platform, env) ?? trimmed;
982
+ if (platform !== "win32" || (kind !== "codex" && kind !== "claude" && kind !== "opencode")) {
983
+ return { file: resolved, args: [...forwardArgs] };
984
+ }
985
+ const bundledExe = kind === "codex" || kind === "claude"
986
+ ? resolveBundledWindowsExe(kind, resolved)
987
+ : undefined;
988
+ if (bundledExe) {
989
+ return { file: bundledExe, args: [...forwardArgs] };
990
+ }
991
+ const extension = path.extname(resolved).toLowerCase();
992
+ if (WINDOWS_DIRECT_EXECUTABLE_EXTENSIONS.includes(extension)) {
993
+ if (extension === ".cmd" || extension === ".bat") {
994
+ return wrapWithCmdExe(resolved, forwardArgs, env);
995
+ }
996
+ return { file: resolved, args: [...forwardArgs] };
997
+ }
998
+ if (extension === WINDOWS_POWERSHELL_EXTENSION) {
999
+ const siblingCmd = resolved.slice(0, -extension.length) + ".cmd";
1000
+ if (fileExists(siblingCmd)) {
1001
+ return wrapWithCmdExe(siblingCmd, forwardArgs, env);
1002
+ }
1003
+ }
1004
+ return { file: resolved, args: [...forwardArgs] };
1005
+ }