/* eslint-disable no-undef */ const { StringDecoder } = require("node:string_decoder"); const DEFAULT_CODEX_CLI_TIMEOUT_MS = 10_000; const CODEX_AUTH_VALIDATION_TIMEOUT_MS = 10_000; const MAX_AGENT_CLI_BUFFER_CHARS = 10 * 1024 * 1024; function createAgentCliHelpers(ctx) { with (ctx) { const codexAuthValidationInFlight = new Map(); async function runCommand(command, args, options) { return await new Promise((resolve, reject) => { let settled = false; let closed = false; let timeoutId = null; let killId = null; function clearTimers() { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } if (killId) { clearTimeout(killId); killId = null; } } const spawnSpec = prepareCommandForSpawn(command, args || []); const child = spawn(spawnSpec.command, spawnSpec.args, { stdio: ["ignore", "pipe", "pipe"], cwd: options?.cwd || undefined, env: options?.env || process.env, shell: spawnSpec.shell, windowsHide: true, }); let stdout = ""; let stderr = ""; let stdoutBytes = 0; let stderrBytes = 0; let stdoutTruncated = false; let stderrTruncated = false; const stdoutDecoder = new StringDecoder("utf8"); const stderrDecoder = new StringDecoder("utf8"); const timeoutMs = Number.isFinite(options?.timeoutMs) ? Number(options.timeoutMs) : 0; child.stdout.on("data", (chunk) => { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); const remaining = Math.max(0, MAX_AGENT_CLI_BUFFER_CHARS - stdoutBytes); const accepted = buffer.length <= remaining ? buffer : buffer.subarray(0, remaining); if (accepted.length > 0) stdout += stdoutDecoder.write(accepted); stdoutBytes += accepted.length; if (accepted.length < buffer.length) stdoutTruncated = true; }); child.stderr.on("data", (chunk) => { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); const remaining = Math.max(0, MAX_AGENT_CLI_BUFFER_CHARS - stderrBytes); const accepted = buffer.length <= remaining ? buffer : buffer.subarray(0, remaining); if (accepted.length > 0) stderr += stderrDecoder.write(accepted); stderrBytes += accepted.length; if (accepted.length < buffer.length) stderrTruncated = true; }); child.once("error", (error) => { closed = true; if (settled) return; settled = true; clearTimers(); reject(error); }); child.once("close", (exitCode) => { closed = true; clearTimers(); if (settled) return; settled = true; if (!stdoutTruncated || stdoutDecoder.lastNeed === 0) stdout += stdoutDecoder.end(); if (!stderrTruncated || stderrDecoder.lastNeed === 0) stderr += stderrDecoder.end(); resolve({ stdout: stripAnsi(stdout), stderr: stripAnsi(stderr), exitCode, }); }); if (timeoutMs > 0) { timeoutId = setTimeout(() => { if (settled) return; settled = true; const error = new Error(`Command timed out after ${timeoutMs}ms`); error.code = "ETIMEDOUT"; try { if (!closed) child.kill("SIGTERM"); } catch {} killId = setTimeout(() => { try { if (!closed) child.kill("SIGKILL"); } catch {} }, 750); if (typeof killId.unref === "function") killId.unref(); reject(error); }, timeoutMs); if (typeof timeoutId.unref === "function") timeoutId.unref(); } }); } function getCommandOutput(result) { return [result?.stdout, result?.stderr] .filter((chunk) => typeof chunk === "string" && chunk.length > 0) .join("\n") .trim(); } function getFirstCommandOutputLine(result) { return getCommandOutput(result).split(/\r?\n/)[0] || ""; } async function probeCliVersion(probeCmd, probeArgs, env) { try { const result = await runCommand(probeCmd, probeArgs, { env, timeoutMs: 5000 }); return { launched: true, exitCode: result.exitCode, output: getCommandOutput(result), version: getFirstCommandOutputLine(result), }; } catch { return { launched: false, exitCode: null, output: "", version: "", }; } } async function runCodexCli(args, options) { const shellEnv = await getShellEnv(); const requestedPath = String(options?.codexPath || "").trim(); const configuredPath = requestedPath ? normalizeCliPathForPlatform?.(requestedPath) : null; if (requestedPath && !configuredPath) { throw new Error(`Codex CLI path not found: ${requestedPath}`); } const codexCliPath = configuredPath || await resolveCliFromPathAsync("codex", shellEnv) || "codex"; return await runCommand(codexCliPath, args, { cwd: options?.cwd?.trim() || undefined, env: shellEnv, timeoutMs: Number.isFinite(options?.timeoutMs) ? Number(options.timeoutMs) : DEFAULT_CODEX_CLI_TIMEOUT_MS, }); } async function runCodexCliChecked(args, options) { const result = await runCodexCli(args, options); if (result.exitCode === 0) { return result; } const errorText = result.stderr.trim() || result.stdout.trim() || `Codex command failed with exit code ${result.exitCode ?? "unknown"}`; throw new Error(errorText); } async function validateCodexChatGptAuth(options) { const maxAgeMs = options?.maxAgeMs ?? 30000; const now = Date.now(); const rawRequestedCodexPath = String(options?.codexPath || "").trim(); const requestedCodexPath = rawRequestedCodexPath ? normalizeCliPathForPlatform?.(rawRequestedCodexPath) : null; if (rawRequestedCodexPath && !requestedCodexPath) { const result = { ok: false, checkedAt: now, codexPath: null, error: `Codex CLI path not found: ${rawRequestedCodexPath}`, code: "ENOENT", }; setCodexValidationCache(result); return result; } const cached = getCodexValidationCache(); if (cached && now - cached.checkedAt < maxAgeMs && (cached.codexPath || null) === requestedCodexPath) return cached; const inFlightKey = requestedCodexPath || "__auto__"; const existingValidation = codexAuthValidationInFlight.get(inFlightKey); if (existingValidation) return existingValidation; const validationPromise = (async () => { const shellEnv = await getShellEnv(); const rawCodexPath = requestedCodexPath || await resolveSdkBinPathAsync("codex", shellEnv); const codexPath = rawCodexPath && typeof resolveCodexExecutableForSdk === "function" ? resolveCodexExecutableForSdk(rawCodexPath) || null : rawCodexPath; if (!codexPath) { const result = { ok: false, checkedAt: now, codexPath: requestedCodexPath, error: "codex binary not found", code: "ENOENT" }; setCodexValidationCache(result); return result; } const abortController = new AbortController(); let timeoutId = null; let iterator = null; try { const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { const error = new Error( `Codex ChatGPT auth validation timed out after ${CODEX_AUTH_VALIDATION_TIMEOUT_MS}ms`, ); error.code = "ETIMEDOUT"; try { abortController.abort(error); } catch {} reject(error); }, CODEX_AUTH_VALIDATION_TIMEOUT_MS); if (typeof timeoutId?.unref === "function") timeoutId.unref(); }); const probePromise = (async () => { // Minimal read-only probe turn through the SDK to confirm auth works. const { Codex } = await (typeof loadCodexSdk === "function" ? loadCodexSdk() : import("@openai/codex-sdk")); const codexOptions = { env: addCodexExecutableEnvForSdk(shellEnv, codexPath) }; if (codexPath) codexOptions.codexPathOverride = codexPath; const codex = new Codex(codexOptions); const thread = codex.startThread({ skipGitRepoCheck: true }); const { events } = await thread.runStreamed("ping", { sandbox: "read-only", signal: abortController.signal, }); iterator = events?.[Symbol.asyncIterator]?.(); if (!iterator) throw new Error("Codex auth validation returned no event stream"); let failed = null; while (true) { const next = await iterator.next(); if (next.done) break; const event = next.value; if (event?.type === "turn.failed") { failed = event.error; break; } if (event?.type === "turn.completed") break; if (event?.type === "item.completed") break; } if (failed) throw failed; })(); await Promise.race([probePromise, timeoutPromise]); const result = { ok: true, checkedAt: now, codexPath, error: null }; setCodexValidationCache(result); return result; } catch (error) { const normalized = extractCodexError(error); const result = { ok: false, checkedAt: now, codexPath, error: normalized.message, code: normalized.code }; setCodexValidationCache(result); return result; } finally { if (timeoutId) clearTimeout(timeoutId); try { abortController.abort(); } catch {} try { void Promise.resolve(iterator?.return?.()).catch(() => {}); } catch {} } })(); codexAuthValidationInFlight.set(inFlightKey, validationPromise); try { return await validationPromise; } finally { if (codexAuthValidationInFlight.get(inFlightKey) === validationPromise) { codexAuthValidationInFlight.delete(inFlightKey); } } } function objectToPairs(value) { if (!value || typeof value !== "object") return []; return Object.entries(value) .filter(([name, val]) => typeof name === "string" && typeof val === "string") .map(([name, val]) => ({ name, value: val })); } function resolveCodexStdioEnv(transport, shellEnv) { const merged = {}; if (transport?.env && typeof transport.env === "object") { for (const [name, value] of Object.entries(transport.env)) { if (typeof name === "string" && typeof value === "string") { merged[name] = value; } } } if (Array.isArray(transport?.env_vars)) { for (const envName of transport.env_vars) { const value = shellEnv[envName] || process.env[envName]; if (typeof value === "string" && value.length > 0 && !merged[envName]) { merged[envName] = value; } } } return merged; } function resolveCodexHttpHeaders(transport, shellEnv) { const merged = {}; if (transport?.http_headers && typeof transport.http_headers === "object") { for (const [name, value] of Object.entries(transport.http_headers)) { if (typeof name === "string" && typeof value === "string") { merged[name] = value; } } } if (transport?.env_http_headers && typeof transport.env_http_headers === "object") { for (const [headerName, envName] of Object.entries(transport.env_http_headers)) { if (typeof headerName !== "string" || typeof envName !== "string") continue; const value = shellEnv[envName] || process.env[envName]; if (typeof value === "string" && value.length > 0) { merged[headerName] = value; } } } const bearerEnvVar = typeof transport?.bearer_token_env_var === "string" ? transport.bearer_token_env_var.trim() : ""; if (bearerEnvVar && !merged.Authorization) { const token = shellEnv[bearerEnvVar] || process.env[bearerEnvVar]; if (typeof token === "string" && token.trim()) { merged.Authorization = `Bearer ${token.trim()}`; } } return merged; } async function resolveCodexMcpSnapshot(cwd) { const empty = { mcpServers: [], fingerprint: getCodexMcpFingerprint([]) }; try { const result = await runCodexCliChecked(["mcp", "list", "--json"], { cwd: cwd || undefined, }); const parsed = JSON.parse(result.stdout); if (!Array.isArray(parsed)) { return empty; } const shellEnv = await getShellEnv(); const mcpServers = []; for (const entry of parsed) { if (!entry?.enabled || !entry?.transport || typeof entry?.name !== "string") { continue; } const transportType = String(entry.transport.type || "").trim().toLowerCase(); if (transportType === "stdio") { const command = String(entry.transport.command || "").trim(); if (!command) continue; mcpServers.push({ name: entry.name, type: "stdio", command, args: Array.isArray(entry.transport.args) ? entry.transport.args.filter((arg) => typeof arg === "string") : [], env: objectToPairs(resolveCodexStdioEnv(entry.transport, shellEnv)), }); continue; } if (transportType === "streamable_http" || transportType === "http" || transportType === "sse") { const url = String(entry.transport.url || "").trim(); if (!url) continue; mcpServers.push({ name: entry.name, type: "http", url, headers: objectToPairs(resolveCodexHttpHeaders(entry.transport, shellEnv)), }); } } return { mcpServers, fingerprint: getCodexMcpFingerprint(mcpServers), }; } catch (err) { console.error("[Codex] Failed to resolve MCP servers:", err?.message || err); return empty; } } return { runCommand, getCommandOutput, getFirstCommandOutputLine, probeCliVersion, runCodexCli, runCodexCliChecked, validateCodexChatGptAuth, objectToPairs, resolveCodexStdioEnv, resolveCodexHttpHeaders, resolveCodexMcpSnapshot, }; } } module.exports = { createAgentCliHelpers, CODEX_AUTH_VALIDATION_TIMEOUT_MS, DEFAULT_CODEX_CLI_TIMEOUT_MS, MAX_AGENT_CLI_BUFFER_CHARS, };