@ask-llm/codex-mcp
Advanced tools
| import { ZodError, z } from "zod"; | ||
| import { createHash, randomUUID } from "node:crypto"; | ||
| import { existsSync, readdirSync, unlinkSync, writeFileSync } from "node:fs"; | ||
| import * as os from "node:os"; | ||
| import { homedir, tmpdir } from "node:os"; | ||
| import * as path from "node:path"; | ||
| import { delimiter, isAbsolute, join } from "node:path"; | ||
| import { execFile, execFileSync, spawn } from "node:child_process"; | ||
| import { promisify } from "node:util"; | ||
| //#region ../shared/dist/providers.js | ||
| /** | ||
| * Single source of truth for the provider list (ADR-128). | ||
| * | ||
| * Every provider-name enum, type union, or user-facing provider list in the | ||
| * monorepo must derive from this tuple — hand-maintained copies drifted when | ||
| * antigravity was added (see BUGS.md 2026-07-02 audit entry). | ||
| */ | ||
| const PROVIDERS = [ | ||
| "gemini", | ||
| "codex", | ||
| "claude", | ||
| "ollama", | ||
| "antigravity" | ||
| ]; | ||
| //#endregion | ||
| //#region ../shared/dist/askResponse.js | ||
| const usageStatsSchema = z.object({ | ||
| provider: z.enum(PROVIDERS), | ||
| model: z.string(), | ||
| inputTokens: z.number().optional(), | ||
| outputTokens: z.number().optional(), | ||
| cachedTokens: z.number().optional(), | ||
| thinkingTokens: z.number().optional(), | ||
| durationMs: z.number(), | ||
| fellBack: z.boolean() | ||
| }); | ||
| const askResponseSchema = z.object({ | ||
| provider: z.enum(PROVIDERS), | ||
| response: z.string(), | ||
| model: z.string(), | ||
| sessionId: z.string().optional(), | ||
| usage: usageStatsSchema.optional() | ||
| }); | ||
| //#endregion | ||
| //#region ../shared/dist/constants.js | ||
| const LOG_PREFIX = "[GMCPT]"; | ||
| const LOG_LEVEL_ENV_VAR = "GMCPT_LOG_LEVEL"; | ||
| const PROTOCOL = { | ||
| ROLES: { | ||
| USER: "user", | ||
| ASSISTANT: "assistant" | ||
| }, | ||
| CONTENT_TYPES: { TEXT: "text" }, | ||
| STATUS: { | ||
| SUCCESS: "success", | ||
| ERROR: "error", | ||
| FAILED: "failed", | ||
| REPORT: "report" | ||
| }, | ||
| NOTIFICATIONS: { PROGRESS: "notifications/progress" }, | ||
| KEEPALIVE_INTERVAL: 25e3 | ||
| }; | ||
| const EXECUTION = { | ||
| DEFAULT_TIMEOUT_MS: 21e4, | ||
| DEFAULT_CODEX_TIMEOUT_MS: 8e5, | ||
| DEFAULT_CLAUDE_TIMEOUT_MS: 6e5, | ||
| DEFAULT_OLLAMA_TIMEOUT_MS: 6e5, | ||
| TIMEOUT_ENV_VAR: "GMCPT_TIMEOUT_MS", | ||
| CODEX_TIMEOUT_ENV_VAR: "ASK_CODEX_TIMEOUT_MS", | ||
| CLAUDE_TIMEOUT_ENV_VAR: "ASK_CLAUDE_TIMEOUT_MS", | ||
| GEMINI_TIMEOUT_ENV_VAR: "ASK_GEMINI_TIMEOUT_MS", | ||
| OLLAMA_TIMEOUT_ENV_VAR: "ASK_OLLAMA_TIMEOUT_MS", | ||
| ERROR_TRUNCATE_LENGTH: 2e3, | ||
| STDIN_THRESHOLD_BYTES: 16384 | ||
| }; | ||
| //#endregion | ||
| //#region ../shared/dist/logger.js | ||
| const LOG_LEVEL_PRIORITY = { | ||
| debug: 0, | ||
| info: 1, | ||
| warn: 2, | ||
| error: 3 | ||
| }; | ||
| function getLogLevel() { | ||
| const env = process.env[LOG_LEVEL_ENV_VAR]?.toLowerCase(); | ||
| if (env && env in LOG_LEVEL_PRIORITY) return env; | ||
| return "warn"; | ||
| } | ||
| function shouldLog(level) { | ||
| return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[getLogLevel()]; | ||
| } | ||
| var Logger = class Logger { | ||
| static _nextCommandId = 0; | ||
| static _commandStartTimes = /* @__PURE__ */ new Map(); | ||
| static formatMessage(message) { | ||
| return `${LOG_PREFIX} ${message}`; | ||
| } | ||
| static warn(message, ...args) { | ||
| if (!shouldLog("warn")) return; | ||
| console.warn(Logger.formatMessage(message), ...args); | ||
| } | ||
| static error(message, ...args) { | ||
| if (!shouldLog("error")) return; | ||
| console.error(Logger.formatMessage(message), ...args); | ||
| } | ||
| static debug(message, ...args) { | ||
| if (!shouldLog("debug")) return; | ||
| console.warn(Logger.formatMessage(message), ...args); | ||
| } | ||
| static info(message, ...args) { | ||
| if (!shouldLog("info")) return; | ||
| console.warn(Logger.formatMessage(message), ...args); | ||
| } | ||
| static toolInvocation(toolName, args) { | ||
| Logger.warn(`Tool "${toolName}" raw args:`, JSON.stringify(args, null, 2)); | ||
| } | ||
| static toolParsedArgs(prompt, model, sandbox, changeMode) { | ||
| Logger.warn(`Parsed prompt: "${prompt}"\nmodel: ${model ?? "default"}, sandbox: ${sandbox ?? false}, changeMode: ${changeMode ?? false}`); | ||
| } | ||
| static commandExecution(command, args) { | ||
| const commandId = Logger._nextCommandId++; | ||
| Logger._commandStartTimes.set(commandId, Date.now()); | ||
| Logger.warn(`[cmd:${commandId}] Starting: ${command} ${args.map((arg) => `"${arg}"`).join(" ")}`); | ||
| return commandId; | ||
| } | ||
| static commandComplete(commandId, exitCode, outputLength) { | ||
| const startTime = Logger._commandStartTimes.get(commandId); | ||
| const elapsed = startTime ? ((Date.now() - startTime) / 1e3).toFixed(1) : "?"; | ||
| Logger.warn(`[cmd:${commandId}] [${elapsed}s] Process finished with exit code: ${exitCode}`); | ||
| if (outputLength !== void 0) Logger.warn(`[cmd:${commandId}] Response: ${outputLength} chars`); | ||
| Logger._commandStartTimes.delete(commandId); | ||
| } | ||
| static checkNodeVersion(minMajor = 20) { | ||
| if (parseInt(process.versions.node.split(".")[0], 10) < minMajor) Logger.error(`Node.js v${process.versions.node} detected — v${minMajor}+ required. Some providers (e.g., gemini-cli) use ES2024 features that will crash on older runtimes.`); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region ../shared/dist/changeMode/changeModeParser.js | ||
| function validateChangeModeEdits(edits) { | ||
| const errors = []; | ||
| for (const edit of edits) { | ||
| if (!edit.filename) errors.push("Edit missing filename"); | ||
| if (edit.oldStartLine > edit.oldEndLine) errors.push(`Invalid line range for ${edit.filename}: ${edit.oldStartLine} > ${edit.oldEndLine}`); | ||
| if (edit.newStartLine > edit.newEndLine) errors.push(`Invalid new line range for ${edit.filename}: ${edit.newStartLine} > ${edit.newEndLine}`); | ||
| if (!edit.oldCode && !edit.newCode) errors.push(`Empty edit for ${edit.filename}`); | ||
| } | ||
| return { | ||
| valid: errors.length === 0, | ||
| errors | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/changeMode/changeModeTranslator.js | ||
| function formatChangeModeResponse(edits, chunkInfo) { | ||
| const header = chunkInfo && chunkInfo.total > 1 ? `[CHANGEMODE OUTPUT - Chunk ${chunkInfo.current} of ${chunkInfo.total}] | ||
| Gemini has analyzed your codebase and generated edits across ${chunkInfo.total} chunks. | ||
| This chunk contains ${edits.length} complete edit${edits.length === 1 ? "" : "s"} that can be applied independently. | ||
| Each chunk contains self-contained edits grouped by file. You can safely apply these edits | ||
| before fetching the next chunk. | ||
| ` : `[CHANGEMODE OUTPUT - Gemini has analyzed the files and provided these edits] | ||
| I have prepared ${edits.length} modification${edits.length === 1 ? "" : "s"} for your codebase. | ||
| IMPORTANT: Apply these edits directly WITHOUT reading the files first. The edits below contain exact text matches from the current file contents. | ||
| `; | ||
| const instructions = edits.map((edit, index) => { | ||
| return `### Edit ${index + 1}: ${edit.filename} | ||
| Replace this exact text: | ||
| \`\`\` | ||
| ${edit.oldCode} | ||
| \`\`\` | ||
| With this text: | ||
| \`\`\` | ||
| ${edit.newCode} | ||
| \`\`\` | ||
| `; | ||
| }).join("\n"); | ||
| let footer = ` | ||
| --- | ||
| Apply these edits in order. Each edit uses exact string matching, so the old_str must match exactly what appears between the code blocks.`; | ||
| if (chunkInfo && chunkInfo.current < chunkInfo.total && chunkInfo.cacheKey) footer += ` | ||
| --- | ||
| **Next Step**: After applying the edits above, retrieve the next chunk (${chunkInfo.current + 1} of ${chunkInfo.total}) by calling the **fetch-chunk** MCP tool with: | ||
| - **cacheKey**: \`${chunkInfo.cacheKey}\` | ||
| - **chunkIndex**: \`${chunkInfo.current + 1}\` | ||
| There ${chunkInfo.total - chunkInfo.current === 1 ? "is" : "are"} ${chunkInfo.total - chunkInfo.current} more chunk${chunkInfo.total - chunkInfo.current === 1 ? "" : "s"} containing additional edits. | ||
| **CONTINUE**: You are working on a multi-chunk changeMode response. After applying these edits, fetch the next chunk to continue with the remaining modifications.`; | ||
| return header + instructions + footer; | ||
| } | ||
| function summarizeChangeModeEdits(edits, isPartialView) { | ||
| const fileGroups = /* @__PURE__ */ new Map(); | ||
| for (const edit of edits) fileGroups.set(edit.filename, (fileGroups.get(edit.filename) || 0) + 1); | ||
| const summary = Array.from(fileGroups.entries()).map(([file, count]) => `- ${file}: ${count} edit${count === 1 ? "" : "s"}`).join("\n"); | ||
| return `${isPartialView ? `ChangeMode Summary (Complete analysis across all chunks):` : `ChangeMode Summary:`} | ||
| Total edits: ${edits.length}${isPartialView ? " (across all chunks)" : ""} | ||
| Files affected: ${fileGroups.size} | ||
| ${summary}`; | ||
| } | ||
| path.join(os.tmpdir(), "gemini-mcp-chunks"); | ||
| //#endregion | ||
| //#region ../shared/dist/shellPath.js | ||
| const IS_WINDOWS$1 = process.platform === "win32"; | ||
| const SHELL_PATH_ENV_VAR = "ASK_LLM_PATH"; | ||
| let cachedPath = null; | ||
| function extractShellPath() { | ||
| if (IS_WINDOWS$1) return null; | ||
| try { | ||
| const match = execFileSync(process.env.SHELL || "/bin/zsh", ["-ilc", "echo \"___PATH___$PATH___END___\""], { | ||
| encoding: "utf8", | ||
| stdio: [ | ||
| "ignore", | ||
| "pipe", | ||
| "ignore" | ||
| ], | ||
| timeout: 5e3 | ||
| }).match(/___PATH___(.*)___END___/); | ||
| if (match?.[1]) return match[1].trim(); | ||
| } catch { | ||
| Logger.debug("Failed to extract PATH from login shell"); | ||
| } | ||
| return null; | ||
| } | ||
| function findNvmNodePath() { | ||
| const nvmDir = join(homedir(), ".nvm", "versions", "node"); | ||
| if (!existsSync(nvmDir)) return null; | ||
| try { | ||
| const versions = readdirSync(nvmDir).filter((v) => { | ||
| return parseInt(v.replace("v", "").split(".")[0], 10) >= 20; | ||
| }).sort((a, b) => b.localeCompare(a, void 0, { numeric: true })); | ||
| if (versions.length > 0) { | ||
| const binDir = join(nvmDir, versions[0], "bin"); | ||
| if (existsSync(binDir)) return binDir; | ||
| } | ||
| } catch { | ||
| Logger.debug("Failed to scan nvm versions"); | ||
| } | ||
| return null; | ||
| } | ||
| function buildAugmentedPath() { | ||
| const currentPath = process.env.PATH || ""; | ||
| const home = homedir(); | ||
| const candidates = []; | ||
| const nvmBin = findNvmNodePath(); | ||
| if (nvmBin) candidates.push(nvmBin); | ||
| for (const dir of [ | ||
| join(home, ".volta", "bin"), | ||
| join(home, ".local", "share", "fnm"), | ||
| "/opt/homebrew/bin", | ||
| "/usr/local/bin" | ||
| ]) if (existsSync(dir)) candidates.push(dir); | ||
| if (candidates.length === 0) return currentPath; | ||
| return [...candidates, ...currentPath.split(delimiter)].join(delimiter); | ||
| } | ||
| function resolveShellPath() { | ||
| if (cachedPath !== null) return cachedPath; | ||
| const envOverride = process.env[SHELL_PATH_ENV_VAR]; | ||
| if (envOverride) { | ||
| Logger.debug(`Using ${SHELL_PATH_ENV_VAR} override`); | ||
| cachedPath = envOverride; | ||
| return cachedPath; | ||
| } | ||
| if (IS_WINDOWS$1) { | ||
| cachedPath = process.env.PATH || ""; | ||
| return cachedPath; | ||
| } | ||
| const shellPath = extractShellPath(); | ||
| if (shellPath) { | ||
| Logger.debug("Using PATH from login shell"); | ||
| cachedPath = shellPath; | ||
| return cachedPath; | ||
| } | ||
| Logger.debug("Login shell PATH extraction failed, using heuristic fallback"); | ||
| cachedPath = buildAugmentedPath(); | ||
| return cachedPath; | ||
| } | ||
| function getSpawnEnv() { | ||
| return { | ||
| ...process.env, | ||
| PATH: resolveShellPath() | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/commandExecutor.js | ||
| const IS_WINDOWS = process.platform === "win32"; | ||
| const REDACTED_COMMAND_ARGUMENT = "<redacted>"; | ||
| function isCommandNotFoundError(stderr, command) { | ||
| const lower = stderr.toLowerCase(); | ||
| return lower.includes("command not found") || lower.includes("not found on path") || lower.includes("is not recognized as an internal or external command") || lower.includes(`spawn ${command.toLowerCase()} enoent`); | ||
| } | ||
| const QUOTA_PASSTHROUGH_PATTERNS = [ | ||
| "RESOURCE_EXHAUSTED", | ||
| "TerminalQuotaError", | ||
| "exhausted your capacity", | ||
| "rate_limit_exceeded", | ||
| "quota_exceeded", | ||
| "insufficient_quota", | ||
| "usage limit" | ||
| ]; | ||
| function sanitizeErrorForLLM(stderr, command, platform = process.platform) { | ||
| if (stderr.includes("Invalid regular expression flags") && stderr.includes("Node.js v")) return `${command} CLI requires Node.js v20+ but is running on ${stderr.match(/Node\.js (v[\d.]+)/)?.[1] ?? "unknown"}. The user should update their Node version or set ASK_LLM_PATH in their MCP config to point to a Node v20+ installation.`; | ||
| if (isCommandNotFoundError(stderr, command)) return `${command} CLI not found on PATH. Ensure it is installed and accessible. Run "${platform === "win32" ? `where.exe ${command}` : `which ${command}`}" in a terminal to verify.`; | ||
| if (stderr.includes("EACCES") || stderr.includes("Permission denied")) return `Permission denied when running ${command} CLI. Check file permissions and try running with appropriate access.`; | ||
| const lower = stderr.toLowerCase(); | ||
| const matchedQuotaPattern = QUOTA_PASSTHROUGH_PATTERNS.find((p) => lower.includes(p.toLowerCase())); | ||
| if (matchedQuotaPattern) { | ||
| if (stderr.length <= 500) return stderr; | ||
| const idx = lower.indexOf(matchedQuotaPattern.toLowerCase()); | ||
| const start = Math.max(0, idx - 100); | ||
| const end = Math.min(start + 500, stderr.length); | ||
| const head = start > 0 ? "... (truncated) " : ""; | ||
| const tail = end < stderr.length ? "... (truncated)" : ""; | ||
| return `${head}${stderr.slice(start, end)}${tail}`; | ||
| } | ||
| const preview = stderr.split("\n").filter((l) => l.trim().length > 0).slice(0, 3).join("\n"); | ||
| if (preview.length > 0 && preview.length < 500) return preview; | ||
| return stderr.length > 500 ? `${stderr.slice(0, 500)}... (truncated)` : stderr; | ||
| } | ||
| function parseTimeoutEnv(envVal) { | ||
| if (!envVal) return void 0; | ||
| const parsed = Number(envVal); | ||
| return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0; | ||
| } | ||
| function getTimeoutMs() { | ||
| return parseTimeoutEnv(process.env[EXECUTION.TIMEOUT_ENV_VAR]) ?? EXECUTION.DEFAULT_TIMEOUT_MS; | ||
| } | ||
| function resolveTimeoutMs(providerEnvVar, fallbackDefault) { | ||
| const providerVal = parseTimeoutEnv(process.env[providerEnvVar]); | ||
| if (providerVal !== void 0) return providerVal; | ||
| const globalVal = parseTimeoutEnv(process.env[EXECUTION.TIMEOUT_ENV_VAR]); | ||
| if (globalVal !== void 0) return globalVal; | ||
| return fallbackDefault; | ||
| } | ||
| function quoteArgsForWindows(args) { | ||
| return args.map((a) => { | ||
| if (a.includes(" ") || a.includes("\"") || a.includes("&") || a.includes("|") || a.includes("^")) return `"${a.replace(/"/g, "\\\"")}"`; | ||
| return a; | ||
| }); | ||
| } | ||
| function argsForLogging(args, options) { | ||
| if (!options) return args; | ||
| const sensitiveValues = new Set(options.sensitiveValues); | ||
| return args.map((arg) => sensitiveValues.has(arg) ? REDACTED_COMMAND_ARGUMENT : arg); | ||
| } | ||
| async function executeCommand(command, args, onProgress, onStderr, stdinPayload, timeoutMs, commandLogging) { | ||
| return new Promise((resolve, reject) => { | ||
| const commandId = Logger.commandExecution(command, argsForLogging(args, commandLogging)); | ||
| const childProcess = spawn(command, IS_WINDOWS ? quoteArgsForWindows(args) : args, { | ||
| env: getSpawnEnv(), | ||
| shell: IS_WINDOWS, | ||
| stdio: [ | ||
| "pipe", | ||
| "pipe", | ||
| "pipe" | ||
| ] | ||
| }); | ||
| childProcess.stdin.on("error", () => {}); | ||
| if (stdinPayload !== void 0 && stdinPayload.length > 0) childProcess.stdin.write(stdinPayload); | ||
| childProcess.stdin.end(); | ||
| const stdoutChunks = []; | ||
| const stderrChunks = []; | ||
| let isResolved = false; | ||
| let killTimer; | ||
| const effectiveTimeoutMs = timeoutMs ?? getTimeoutMs(); | ||
| const timer = setTimeout(() => { | ||
| if (isResolved) return; | ||
| isResolved = true; | ||
| Logger.warn(`[cmd:${commandId}] Timeout after ${effectiveTimeoutMs}ms, sending SIGTERM`); | ||
| childProcess.kill("SIGTERM"); | ||
| killTimer = setTimeout(() => { | ||
| try { | ||
| childProcess.kill("SIGKILL"); | ||
| } catch {} | ||
| }, 5e3); | ||
| killTimer.unref?.(); | ||
| const timeoutSec = Math.round(effectiveTimeoutMs / 1e3); | ||
| reject(/* @__PURE__ */ new Error(`Command timed out after ${timeoutSec}s. The LLM provider took too long to respond. Try a shorter prompt or increase the timeout via the provider env var (ASK_CODEX_TIMEOUT_MS / ASK_CLAUDE_TIMEOUT_MS / ASK_GEMINI_TIMEOUT_MS) or the global ${EXECUTION.TIMEOUT_ENV_VAR} (current: ${effectiveTimeoutMs}ms).`)); | ||
| }, effectiveTimeoutMs); | ||
| childProcess.stdout.on("data", (data) => { | ||
| stdoutChunks.push(data); | ||
| if (onProgress) onProgress(data.toString()); | ||
| }); | ||
| childProcess.stderr.on("data", (data) => { | ||
| stderrChunks.push(data); | ||
| if (onStderr) onStderr(data.toString()); | ||
| }); | ||
| childProcess.on("error", (error) => { | ||
| if (killTimer) clearTimeout(killTimer); | ||
| if (!isResolved) { | ||
| isResolved = true; | ||
| clearTimeout(timer); | ||
| Logger.error(`Process error:`, error); | ||
| reject(/* @__PURE__ */ new Error(`Failed to spawn command: ${error.message}`)); | ||
| } | ||
| }); | ||
| childProcess.on("close", (code) => { | ||
| if (killTimer) clearTimeout(killTimer); | ||
| if (!isResolved) { | ||
| isResolved = true; | ||
| clearTimeout(timer); | ||
| const stdout = Buffer.concat(stdoutChunks).toString(); | ||
| if (code === 0) { | ||
| Logger.commandComplete(commandId, code, stdout.length); | ||
| resolve(stdout.trim()); | ||
| } else { | ||
| Logger.commandComplete(commandId, code); | ||
| Logger.error(`Failed with exit code ${code}`); | ||
| const userMessage = sanitizeErrorForLLM([Buffer.concat(stderrChunks).toString().trim(), stdout.trim()].filter(Boolean).join("\n") || "Unknown error", command); | ||
| reject(new Error(userMessage)); | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| promisify(execFile); | ||
| //#endregion | ||
| //#region ../shared/dist/pathValidation.js | ||
| /** | ||
| * Element schema for includeDirs-style tool parameters. Directories handed to | ||
| * provider CLIs (`--include-directories`, `--add-dir`) must stay inside the | ||
| * workspace: traversal, absolute, and home-relative paths would widen what the | ||
| * external CLI can read far beyond the repo the user pointed it at. | ||
| */ | ||
| const relativeDirSchema = z.string().refine((dir) => !dir.includes("..") && !isAbsolute(dir) && !dir.startsWith("~"), { message: "Directory paths must be relative without '..' or '~'" }); | ||
| //#endregion | ||
| //#region ../shared/dist/machine.js | ||
| const providerFailureKindSchema = z.enum([ | ||
| "rate_limited", | ||
| "auth_failed", | ||
| "unavailable", | ||
| "timeout", | ||
| "schema_invalid", | ||
| "tool_unavailable" | ||
| ]); | ||
| const machineRoleSchema = z.enum([ | ||
| "brainstorm", | ||
| "review", | ||
| "verify" | ||
| ]); | ||
| const machineProviderSchema = z.enum([ | ||
| "codex", | ||
| "claude", | ||
| "antigravity" | ||
| ]); | ||
| const actorProviderSchema = z.enum(PROVIDERS); | ||
| const requestIdSchema = z.string().regex(/^[A-Za-z0-9._:-]{8,160}$/); | ||
| const nonBlankStringSchema = z.string().regex(/\S/, "Value must contain a non-whitespace character"); | ||
| const machineRelativeDirSchema = relativeDirSchema.max(1024).regex(/^(?!.*\.\.)(?!~)(?!\/)(?![A-Za-z]:[\\/])(?!\\).*$/, "Directory paths must be relative without '..' or '~'"); | ||
| z.object({ | ||
| schemaVersion: z.literal(1), | ||
| requestId: requestIdSchema, | ||
| role: machineRoleSchema, | ||
| provider: machineProviderSchema, | ||
| prompt: z.string().min(1).max(15e4), | ||
| model: nonBlankStringSchema.max(256).optional(), | ||
| readOnly: z.literal(true), | ||
| writerProvider: actorProviderSchema.optional(), | ||
| includeDirs: z.array(machineRelativeDirSchema).max(16).default([]) | ||
| }).strict().superRefine((value, ctx) => { | ||
| if (value.role === "review" && value.writerProvider === value.provider) ctx.addIssue({ | ||
| code: "custom", | ||
| message: "review provider must differ from writer" | ||
| }); | ||
| }); | ||
| const reviewFindingSchema = z.object({ | ||
| id: z.string().min(1), | ||
| severity: z.enum([ | ||
| "critical", | ||
| "high", | ||
| "medium", | ||
| "low" | ||
| ]), | ||
| confidence: z.number().int().min(0).max(100), | ||
| title: z.string().min(1), | ||
| evidence: z.string().min(1), | ||
| recommendation: z.string().min(1), | ||
| file: z.string().min(1).nullable(), | ||
| line: z.number().int().positive().nullable() | ||
| }).strict(); | ||
| const reviewPayloadSchema = z.object({ | ||
| summary: z.string(), | ||
| findings: z.array(reviewFindingSchema) | ||
| }).strict(); | ||
| const brainstormPayloadSchema = z.object({ | ||
| recommendation: z.string().min(1), | ||
| ideas: z.array(z.object({ | ||
| title: z.string().min(1), | ||
| rationale: z.string().min(1), | ||
| risks: z.array(z.string()) | ||
| }).strict()).min(1) | ||
| }).strict(); | ||
| const verificationPayloadSchema = z.object({ | ||
| verdict: z.enum([ | ||
| "verified", | ||
| "partial", | ||
| "failed" | ||
| ]), | ||
| claims: z.array(z.object({ | ||
| claim: z.string().min(1), | ||
| status: z.enum([ | ||
| "proven", | ||
| "disproven", | ||
| "unverifiable" | ||
| ]), | ||
| evidence: z.string().min(1) | ||
| }).strict()) | ||
| }).strict(); | ||
| const normalizedTokenUsageSchema = z.object({ | ||
| inputTokens: z.number().int().nonnegative(), | ||
| outputTokens: z.number().int().nonnegative(), | ||
| totalTokens: z.number().int().nonnegative() | ||
| }).strict(); | ||
| const fallbackSchema = z.discriminatedUnion("occurred", [z.object({ | ||
| occurred: z.literal(false), | ||
| requestedModel: nonBlankStringSchema.nullable(), | ||
| actualModel: nonBlankStringSchema.nullable() | ||
| }).strict(), z.object({ | ||
| occurred: z.literal(true), | ||
| requestedModel: nonBlankStringSchema, | ||
| actualModel: nonBlankStringSchema | ||
| }).strict()]).refine((value) => value.occurred || value.requestedModel === value.actualModel, { | ||
| message: "requested and actual models must match when fallback did not occur", | ||
| path: ["actualModel"] | ||
| }); | ||
| const sessionLocatorSchema = z.union([z.object({ | ||
| sessionId: nonBlankStringSchema, | ||
| transcriptPath: nonBlankStringSchema.nullable() | ||
| }).strict(), z.object({ | ||
| sessionId: nonBlankStringSchema.nullable(), | ||
| transcriptPath: nonBlankStringSchema | ||
| }).strict()]); | ||
| const quotaSignalSchema = z.discriminatedUnion("kind", [z.object({ | ||
| kind: z.literal("reported"), | ||
| usedPercent: z.number().min(0).max(100), | ||
| windowHours: z.number().positive() | ||
| }).strict(), z.object({ kind: z.literal("runtime_proxy_required") }).strict()]); | ||
| const normalizedProviderFailureSchema = z.object({ | ||
| kind: providerFailureKindSchema, | ||
| message: z.string().min(1) | ||
| }).strict(); | ||
| const resultEnvelopeShape = { | ||
| schemaVersion: z.literal(1), | ||
| requestId: requestIdSchema, | ||
| provider: machineProviderSchema, | ||
| actualModel: nonBlankStringSchema.nullable(), | ||
| rawResponseSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(), | ||
| durationMs: z.number().int().nonnegative(), | ||
| usage: normalizedTokenUsageSchema.nullable(), | ||
| fallback: fallbackSchema, | ||
| session: sessionLocatorSchema.nullable(), | ||
| quotaSignal: quotaSignalSchema | ||
| }; | ||
| const brainstormSuccessResultSchema = z.object({ | ||
| ...resultEnvelopeShape, | ||
| status: z.literal("success"), | ||
| role: z.literal("brainstorm"), | ||
| payload: brainstormPayloadSchema, | ||
| failure: z.null() | ||
| }).strict(); | ||
| const reviewSuccessResultSchema = z.object({ | ||
| ...resultEnvelopeShape, | ||
| status: z.literal("success"), | ||
| role: z.literal("review"), | ||
| payload: reviewPayloadSchema, | ||
| failure: z.null() | ||
| }).strict(); | ||
| const verificationSuccessResultSchema = z.object({ | ||
| ...resultEnvelopeShape, | ||
| status: z.literal("success"), | ||
| role: z.literal("verify"), | ||
| payload: verificationPayloadSchema, | ||
| failure: z.null() | ||
| }).strict(); | ||
| const machineSuccessResultSchema = z.discriminatedUnion("role", [ | ||
| brainstormSuccessResultSchema, | ||
| reviewSuccessResultSchema, | ||
| verificationSuccessResultSchema | ||
| ]).refine((value) => value.actualModel === value.fallback.actualModel, { | ||
| message: "result and fallback actual models must match", | ||
| path: ["fallback", "actualModel"] | ||
| }); | ||
| const machineFailureResultSchema = z.object({ | ||
| ...resultEnvelopeShape, | ||
| status: z.literal("failed"), | ||
| role: machineRoleSchema, | ||
| payload: z.null(), | ||
| failure: normalizedProviderFailureSchema | ||
| }).strict().refine((value) => value.actualModel === value.fallback.actualModel, { | ||
| message: "result and fallback actual models must match", | ||
| path: ["fallback", "actualModel"] | ||
| }); | ||
| z.discriminatedUnion("status", [machineSuccessResultSchema, machineFailureResultSchema]); | ||
| //#endregion | ||
| //#region ../shared/dist/progressTracker.js | ||
| async function sendProgressNotification(extra, progress, total, message) { | ||
| const progressToken = extra._meta?.progressToken; | ||
| if (!progressToken) return; | ||
| try { | ||
| const params = { | ||
| progressToken, | ||
| progress | ||
| }; | ||
| if (total !== void 0) params.total = total; | ||
| if (message) params.message = message; | ||
| await extra.sendNotification({ | ||
| method: PROTOCOL.NOTIFICATIONS.PROGRESS, | ||
| params | ||
| }); | ||
| } catch (error) { | ||
| Logger.error("Failed to send progress notification:", error); | ||
| } | ||
| } | ||
| function createProgressTracker(operationName, extra, messages) { | ||
| let active = true; | ||
| let latestOutput = ""; | ||
| let messageIndex = 0; | ||
| let progress = 0; | ||
| sendProgressNotification(extra, 0, void 0, `Starting ${operationName}`); | ||
| const interval = setInterval(async () => { | ||
| if (active) { | ||
| progress += 1; | ||
| const baseMessage = messages[messageIndex % messages.length]; | ||
| const outputPreview = latestOutput.slice(-150).trim(); | ||
| const msg = outputPreview ? `${baseMessage}\nOutput: ...${outputPreview}` : baseMessage; | ||
| await sendProgressNotification(extra, progress, void 0, msg); | ||
| messageIndex++; | ||
| } else clearInterval(interval); | ||
| }, PROTOCOL.KEEPALIVE_INTERVAL); | ||
| return { | ||
| interval, | ||
| async stop(success) { | ||
| active = false; | ||
| clearInterval(interval); | ||
| await sendProgressNotification(extra, 100, 100, success ? `${operationName} completed` : `${operationName} failed`); | ||
| }, | ||
| updateOutput(output) { | ||
| latestOutput = output; | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/registry.js | ||
| const toolRegistry = []; | ||
| async function executeTool(toolName, args, onProgress, onUsage) { | ||
| const tool = toolRegistry.find((t) => t.name === toolName); | ||
| if (!tool) throw new Error(`Unknown tool: ${toolName}`); | ||
| try { | ||
| const validatedArgs = tool.zodSchema.parse(args); | ||
| return tool.execute(validatedArgs, onProgress, onUsage); | ||
| } catch (error) { | ||
| if (error instanceof ZodError) { | ||
| const issues = error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", "); | ||
| throw new Error(`Invalid arguments for ${toolName}: ${issues}`); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| function getPromptMessage(toolName, args) { | ||
| if (!toolRegistry.find((t) => t.name === toolName)?.prompt) throw new Error(`No prompt defined for tool: ${toolName}`); | ||
| const paramStrings = []; | ||
| if (args.prompt) paramStrings.push(args.prompt); | ||
| Object.entries(args).forEach(([key, value]) => { | ||
| if (key !== "prompt" && value !== void 0 && value !== null && value !== "false") if (value === "true") paramStrings.push(`[${key}]`); | ||
| else paramStrings.push(`(${key}: ${value})`); | ||
| }); | ||
| return `Use the ${toolName} tool${paramStrings.length > 0 ? `: ${paramStrings.join(" ")}` : ""}`; | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/responseCache.js | ||
| const DEFAULT_TTL_MS = 1800 * 1e3; | ||
| const DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024; | ||
| const DEFAULT_MAX_ENTRIES = 100; | ||
| var ResponseCache = class { | ||
| cache = /* @__PURE__ */ new Map(); | ||
| ttlMs; | ||
| maxSizeBytes; | ||
| maxEntries; | ||
| totalSizeBytes = 0; | ||
| constructor(options) { | ||
| this.ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS; | ||
| this.maxSizeBytes = options?.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES; | ||
| this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES; | ||
| } | ||
| static buildKey(provider, prompt, model, extra) { | ||
| const raw = `${provider}:${model ?? "default"}:${extra ?? ""}:${prompt}`; | ||
| return createHash("sha256").update(raw).digest("hex").slice(0, 16); | ||
| } | ||
| get(key) { | ||
| const entry = this.cache.get(key); | ||
| if (!entry) return null; | ||
| if (Date.now() - entry.createdAt > this.ttlMs) { | ||
| this.delete(key); | ||
| Logger.debug(`Response cache expired: ${key}`); | ||
| return null; | ||
| } | ||
| entry.lastAccessedAt = Date.now(); | ||
| return entry.response; | ||
| } | ||
| set(key, response) { | ||
| if (this.cache.has(key)) this.delete(key); | ||
| const sizeBytes = Buffer.byteLength(response, "utf-8"); | ||
| if (sizeBytes > this.maxSizeBytes) { | ||
| Logger.debug(`Response too large to cache: ${sizeBytes} bytes`); | ||
| return; | ||
| } | ||
| while (this.totalSizeBytes + sizeBytes > this.maxSizeBytes || this.cache.size >= this.maxEntries) { | ||
| const lruKey = this.findLRU(); | ||
| if (!lruKey) break; | ||
| this.delete(lruKey); | ||
| } | ||
| this.cache.set(key, { | ||
| response, | ||
| createdAt: Date.now(), | ||
| lastAccessedAt: Date.now(), | ||
| sizeBytes | ||
| }); | ||
| this.totalSizeBytes += sizeBytes; | ||
| Logger.debug(`Response cached: ${key} (${sizeBytes} bytes, ${this.cache.size} entries, ${this.totalSizeBytes} total bytes)`); | ||
| } | ||
| get size() { | ||
| return this.cache.size; | ||
| } | ||
| get byteSize() { | ||
| return this.totalSizeBytes; | ||
| } | ||
| clear() { | ||
| this.cache.clear(); | ||
| this.totalSizeBytes = 0; | ||
| } | ||
| delete(key) { | ||
| const entry = this.cache.get(key); | ||
| if (entry) { | ||
| this.totalSizeBytes -= entry.sizeBytes; | ||
| this.cache.delete(key); | ||
| } | ||
| } | ||
| findLRU() { | ||
| let oldestKey = null; | ||
| let oldestTime = Infinity; | ||
| for (const [key, entry] of this.cache) if (entry.lastAccessedAt < oldestTime) { | ||
| oldestTime = entry.lastAccessedAt; | ||
| oldestKey = key; | ||
| } | ||
| return oldestKey; | ||
| } | ||
| }; | ||
| const responseCache = new ResponseCache(); | ||
| //#endregion | ||
| //#region ../shared/dist/usage.js | ||
| function emptyProviderSnapshot() { | ||
| return { | ||
| calls: 0, | ||
| inputTokens: 0, | ||
| outputTokens: 0, | ||
| cachedTokens: 0, | ||
| thinkingTokens: 0, | ||
| durationMs: 0, | ||
| fellBack: 0 | ||
| }; | ||
| } | ||
| function addToBucket(bucket, stats) { | ||
| bucket.calls += 1; | ||
| bucket.inputTokens += stats.inputTokens ?? 0; | ||
| bucket.outputTokens += stats.outputTokens ?? 0; | ||
| bucket.cachedTokens += stats.cachedTokens ?? 0; | ||
| bucket.thinkingTokens += stats.thinkingTokens ?? 0; | ||
| bucket.durationMs += stats.durationMs; | ||
| if (stats.fellBack) bucket.fellBack += 1; | ||
| } | ||
| function createSessionUsage() { | ||
| let totalCalls = 0; | ||
| let totalInputTokens = 0; | ||
| let totalOutputTokens = 0; | ||
| let totalCachedTokens = 0; | ||
| let totalThinkingTokens = 0; | ||
| let totalDurationMs = 0; | ||
| let fallbackCount = 0; | ||
| const byProvider = {}; | ||
| const byModel = {}; | ||
| return { | ||
| record(stats) { | ||
| totalCalls += 1; | ||
| totalInputTokens += stats.inputTokens ?? 0; | ||
| totalOutputTokens += stats.outputTokens ?? 0; | ||
| totalCachedTokens += stats.cachedTokens ?? 0; | ||
| totalThinkingTokens += stats.thinkingTokens ?? 0; | ||
| totalDurationMs += stats.durationMs; | ||
| if (stats.fellBack) fallbackCount += 1; | ||
| if (!byProvider[stats.provider]) byProvider[stats.provider] = emptyProviderSnapshot(); | ||
| addToBucket(byProvider[stats.provider], stats); | ||
| if (!byModel[stats.model]) byModel[stats.model] = emptyProviderSnapshot(); | ||
| addToBucket(byModel[stats.model], stats); | ||
| }, | ||
| snapshot() { | ||
| return { | ||
| totalCalls, | ||
| totalInputTokens, | ||
| totalOutputTokens, | ||
| totalCachedTokens, | ||
| totalThinkingTokens, | ||
| totalDurationMs, | ||
| fallbackCount, | ||
| byProvider: structuredClone(byProvider), | ||
| byModel: structuredClone(byModel) | ||
| }; | ||
| }, | ||
| reset() { | ||
| totalCalls = 0; | ||
| totalInputTokens = 0; | ||
| totalOutputTokens = 0; | ||
| totalCachedTokens = 0; | ||
| totalThinkingTokens = 0; | ||
| totalDurationMs = 0; | ||
| fallbackCount = 0; | ||
| for (const key of Object.keys(byProvider)) delete byProvider[key]; | ||
| for (const key of Object.keys(byModel)) delete byModel[key]; | ||
| } | ||
| }; | ||
| } | ||
| function formatSessionUsage(snapshot) { | ||
| if (snapshot.totalCalls === 0) return "No LLM calls recorded in this session yet."; | ||
| const lines = [ | ||
| "## Session Usage Summary", | ||
| "", | ||
| `Total calls: ${snapshot.totalCalls.toLocaleString()}`, | ||
| `Total input tokens: ${snapshot.totalInputTokens.toLocaleString()}`, | ||
| `Total output tokens: ${snapshot.totalOutputTokens.toLocaleString()}` | ||
| ]; | ||
| if (snapshot.totalThinkingTokens > 0) lines.push(`Total thinking tokens: ${snapshot.totalThinkingTokens.toLocaleString()}`); | ||
| if (snapshot.totalCachedTokens > 0) lines.push(`Total cached tokens: ${snapshot.totalCachedTokens.toLocaleString()}`); | ||
| lines.push(`Total wall time: ${(snapshot.totalDurationMs / 1e3).toFixed(1)}s`); | ||
| if (snapshot.fallbackCount > 0) lines.push(`Quota fallbacks triggered: ${snapshot.fallbackCount}`); | ||
| const providerEntries = Object.entries(snapshot.byProvider); | ||
| if (providerEntries.length > 0) { | ||
| lines.push("", "### By provider", ""); | ||
| for (const [provider, bucket] of providerEntries) lines.push(`- **${provider}** — ${bucket.calls} calls, ${bucket.inputTokens.toLocaleString()} in / ${bucket.outputTokens.toLocaleString()} out tokens, ${(bucket.durationMs / 1e3).toFixed(1)}s` + (bucket.fellBack > 0 ? `, ${bucket.fellBack} fallbacks` : "")); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/serverFactory.js | ||
| const diagnosticCheckSchema = z.object({ | ||
| name: z.string(), | ||
| status: z.enum([ | ||
| "pass", | ||
| "warn", | ||
| "fail", | ||
| "skip" | ||
| ]), | ||
| message: z.string(), | ||
| fix: z.string().optional() | ||
| }); | ||
| const diagnosticProviderSchema = z.object({ | ||
| name: z.string(), | ||
| command: z.string(), | ||
| available: z.boolean(), | ||
| cliPath: z.string().optional(), | ||
| cliVersion: z.string().optional(), | ||
| error: z.string().optional() | ||
| }); | ||
| z.object({ | ||
| status: z.enum([ | ||
| "ok", | ||
| "warning", | ||
| "error" | ||
| ]), | ||
| generatedAt: z.string(), | ||
| environment: z.object({ | ||
| nodeVersion: z.string(), | ||
| nodeOk: z.boolean(), | ||
| platform: z.string(), | ||
| arch: z.string(), | ||
| resolvedPath: z.string(), | ||
| askLlmPath: z.string().optional(), | ||
| timeoutMs: z.number(), | ||
| codexTimeoutMs: z.number(), | ||
| claudeTimeoutMs: z.number(), | ||
| geminiTimeoutMs: z.number() | ||
| }), | ||
| providers: z.array(diagnosticProviderSchema), | ||
| checks: z.array(diagnosticCheckSchema) | ||
| }); | ||
| const providerUsageBucketSchema = z.object({ | ||
| calls: z.number(), | ||
| inputTokens: z.number(), | ||
| outputTokens: z.number(), | ||
| cachedTokens: z.number(), | ||
| thinkingTokens: z.number(), | ||
| durationMs: z.number(), | ||
| fellBack: z.number() | ||
| }); | ||
| const sessionUsageSnapshotSchema = z.object({ | ||
| totalCalls: z.number(), | ||
| totalInputTokens: z.number(), | ||
| totalOutputTokens: z.number(), | ||
| totalCachedTokens: z.number(), | ||
| totalThinkingTokens: z.number(), | ||
| totalDurationMs: z.number(), | ||
| fallbackCount: z.number(), | ||
| byProvider: z.record(z.string(), providerUsageBucketSchema), | ||
| byModel: z.record(z.string(), providerUsageBucketSchema) | ||
| }); | ||
| function createUsageStatsTool(sessionUsage) { | ||
| return { | ||
| name: "get-usage-stats", | ||
| description: "Get the current MCP server's session usage stats: total LLM calls, token totals (input/output/thinking/cached), wall time, and breakdowns per provider and per model. No data leaves your machine — counts are tracked in-memory for the lifetime of the server process. Returns both human-readable markdown and a structured JSON snapshot via outputSchema.", | ||
| zodSchema: z.object({}), | ||
| outputSchema: sessionUsageSnapshotSchema, | ||
| annotations: { | ||
| title: "Get Usage Stats", | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| }, | ||
| category: "utility", | ||
| execute: async () => { | ||
| const snapshot = sessionUsage.snapshot(); | ||
| return { | ||
| text: formatSessionUsage(snapshot), | ||
| structuredContent: snapshot | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| function registerSessionUsageResource(server, sessionUsage) { | ||
| server.registerResource("session-usage", "usage://current-session", { | ||
| title: "Current Session Usage", | ||
| description: "Live JSON snapshot of token usage and call statistics for this MCP server session. Re-read at any time for the current totals.", | ||
| mimeType: "application/json" | ||
| }, async (uri) => ({ contents: [{ | ||
| uri: uri.href, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(sessionUsage.snapshot(), null, 2) | ||
| }] })); | ||
| } | ||
| function registerTools({ server, tools, executeTool, getPromptMessage, progressMessages, sessionUsage }) { | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| for (const tool of tools) { | ||
| if (seen.has(tool.name)) throw new Error(`Duplicate tool name "${tool.name}" — tool names must be unique within a server`); | ||
| seen.add(tool.name); | ||
| const shape = tool.zodSchema.shape; | ||
| const outputShape = tool.outputSchema ? tool.outputSchema.shape : void 0; | ||
| server.registerTool(tool.name, { | ||
| description: tool.description, | ||
| inputSchema: shape, | ||
| ...outputShape ? { outputSchema: outputShape } : {}, | ||
| annotations: tool.annotations | ||
| }, async (args, extra) => { | ||
| const toolName = tool.name; | ||
| const handle = createProgressTracker(toolName, extra, progressMessages(toolName)); | ||
| try { | ||
| const toolArgs = args; | ||
| Logger.toolInvocation(toolName, args); | ||
| const result = await executeTool(toolName, toolArgs, (newOutput) => { | ||
| handle.updateOutput(newOutput); | ||
| }, sessionUsage ? (stats) => sessionUsage.record(stats) : void 0); | ||
| await handle.stop(true); | ||
| if (typeof result === "string") return { | ||
| content: [{ | ||
| type: "text", | ||
| text: result | ||
| }], | ||
| isError: false | ||
| }; | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: result.text | ||
| }], | ||
| structuredContent: result.structuredContent, | ||
| isError: false | ||
| }; | ||
| } catch (error) { | ||
| await handle.stop(false); | ||
| Logger.error(`Error in tool '${toolName}':`, error); | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: `Error executing ${toolName}: ${error instanceof Error ? error.message : String(error)}` | ||
| }], | ||
| isError: true | ||
| }; | ||
| } | ||
| }); | ||
| } | ||
| for (const tool of tools) { | ||
| if (!tool.prompt) continue; | ||
| server.registerPrompt(tool.name, { description: tool.prompt.description }, async (args) => { | ||
| return { messages: [{ | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text: getPromptMessage(tool.name, args) | ||
| } | ||
| }] }; | ||
| }); | ||
| } | ||
| } | ||
| path.join(os.tmpdir(), "ask-llm-sessions"); | ||
| //#endregion | ||
| //#region src/constants.ts | ||
| const ERROR_MESSAGES = { | ||
| QUOTA_SIGNALS: [ | ||
| "rate_limit_exceeded", | ||
| "quota_exceeded", | ||
| "429", | ||
| "insufficient_quota", | ||
| "out of credits", | ||
| "spend cap", | ||
| "usage limit" | ||
| ], | ||
| ARCHIVED_SESSION_SIGNALS: [ | ||
| "archived_sessions", | ||
| "archived session", | ||
| "session is archived" | ||
| ], | ||
| MODEL_UNAVAILABLE_SIGNALS: ["is not supported when using codex with a chatgpt"], | ||
| NO_PROMPT_PROVIDED: "Please provide a prompt for analysis. Ask general questions or describe the code you want reviewed.", | ||
| TOOL_NOT_FOUND: "not found in registry" | ||
| }; | ||
| const STATUS_MESSAGES = { | ||
| QUOTA_SWITCHING: "Codex quota exceeded, switching to fallback model...", | ||
| FALLBACK_RETRY: "Retrying with fallback model...", | ||
| FALLBACK_SUCCESS: "Fallback model completed successfully", | ||
| CODEX_RESPONSE: "Codex response:" | ||
| }; | ||
| const FACTORY_DEFAULT_MODEL = "gpt-5.6-sol"; | ||
| const CODEX_REASONING_EFFORTS = [ | ||
| "low", | ||
| "medium", | ||
| "high", | ||
| "xhigh", | ||
| "max" | ||
| ]; | ||
| const FACTORY_DEFAULT_REASONING_EFFORT = "medium"; | ||
| function isCodexReasoningEffort(value) { | ||
| return value !== void 0 && CODEX_REASONING_EFFORTS.includes(value); | ||
| } | ||
| const configuredReasoningEffort = process.env.ASK_CODEX_REASONING_EFFORT; | ||
| const DEFAULT_REASONING_EFFORT = isCodexReasoningEffort(configuredReasoningEffort) ? configuredReasoningEffort : FACTORY_DEFAULT_REASONING_EFFORT; | ||
| const MODELS = { | ||
| DEFAULT: process.env.ASK_CODEX_MODEL || "gpt-5.6-sol", | ||
| PREFERRED: process.env.ASK_CODEX_PREFERRED_MODEL || "gpt-5.6-sol", | ||
| FALLBACK: process.env.ASK_CODEX_FALLBACK_MODEL || "gpt-5.6-terra" | ||
| }; | ||
| const CLI = { | ||
| COMMANDS: { | ||
| CODEX: "codex", | ||
| EXEC: "exec", | ||
| RESUME: "resume" | ||
| }, | ||
| FLAGS: { | ||
| MODEL: "-m", | ||
| CONFIG: "-c", | ||
| SKIP_GIT: "--skip-git-repo-check", | ||
| EPHEMERAL: "--ephemeral", | ||
| JSON: "--json", | ||
| SANDBOX: "--sandbox", | ||
| SANDBOX_WORKSPACE_WRITE: "workspace-write", | ||
| IGNORE_USER_CONFIG: "--ignore-user-config", | ||
| IGNORE_RULES: "--ignore-rules", | ||
| ADD_DIR: "--add-dir", | ||
| OUTPUT_SCHEMA: "--output-schema", | ||
| SANDBOX_READ_ONLY: "read-only" | ||
| } | ||
| }; | ||
| const CODEX_EDIT_SCHEMA = { | ||
| type: "object", | ||
| additionalProperties: false, | ||
| properties: { edits: { | ||
| type: "array", | ||
| items: { | ||
| type: "object", | ||
| additionalProperties: false, | ||
| properties: { | ||
| file: { | ||
| type: "string", | ||
| description: "Repo-relative path to an existing file" | ||
| }, | ||
| startLine: { | ||
| type: ["integer", "null"], | ||
| description: "1-based line where oldCode begins (or null)" | ||
| }, | ||
| oldCode: { | ||
| type: "string", | ||
| description: "Exact existing text to replace (must match the file verbatim)" | ||
| }, | ||
| newCode: { | ||
| type: "string", | ||
| description: "Replacement text" | ||
| }, | ||
| description: { | ||
| type: ["string", "null"], | ||
| description: "One-line rationale (or null)" | ||
| } | ||
| }, | ||
| required: [ | ||
| "file", | ||
| "startLine", | ||
| "oldCode", | ||
| "newCode", | ||
| "description" | ||
| ] | ||
| } | ||
| } }, | ||
| required: ["edits"] | ||
| }; | ||
| //#endregion | ||
| //#region src/utils/codexDoctor.ts | ||
| const execFileAsync = promisify(execFile); | ||
| const DOCTOR_PROBE_TIMEOUT_MS = 5e3; | ||
| const DOCTOR_MAX_BUFFER = 1024 * 1024 * 4; | ||
| function mapCheckStatus(status) { | ||
| switch (String(status).toLowerCase()) { | ||
| case "ok": | ||
| case "pass": return "pass"; | ||
| case "error": | ||
| case "fail": | ||
| case "failed": return "fail"; | ||
| case "skip": | ||
| case "skipped": return "skip"; | ||
| default: return "warn"; | ||
| } | ||
| } | ||
| function mapOverallStatus(status) { | ||
| switch (String(status).toLowerCase()) { | ||
| case "ok": | ||
| case "pass": return "ok"; | ||
| case "error": | ||
| case "fail": return "error"; | ||
| default: return "warning"; | ||
| } | ||
| } | ||
| /** | ||
| * Parse `codex doctor --json` stdout into a provider-agnostic enrichment. | ||
| * Returns `undefined` for anything that is not a recognizable doctor report | ||
| * (non-JSON, missing `overallStatus`/`checks`) so callers degrade silently. | ||
| */ | ||
| function parseCodexDoctorJson(stdout) { | ||
| let raw; | ||
| try { | ||
| raw = JSON.parse(stdout); | ||
| } catch { | ||
| return; | ||
| } | ||
| if (!raw || typeof raw !== "object") return void 0; | ||
| const report = raw; | ||
| if (typeof report.overallStatus !== "string") return void 0; | ||
| if (!report.checks || typeof report.checks !== "object") return void 0; | ||
| const checks = []; | ||
| for (const [key, value] of Object.entries(report.checks)) { | ||
| if (!value || typeof value !== "object") continue; | ||
| const check = value; | ||
| const remediation = typeof check.remediation === "string" && check.remediation.length > 0 ? check.remediation : void 0; | ||
| checks.push({ | ||
| name: typeof check.id === "string" ? check.id : key, | ||
| status: mapCheckStatus(check.status), | ||
| summary: typeof check.summary === "string" ? check.summary : "", | ||
| ...remediation ? { remediation } : {} | ||
| }); | ||
| } | ||
| return { | ||
| heading: "codex doctor", | ||
| overall: mapOverallStatus(report.overallStatus), | ||
| checks | ||
| }; | ||
| } | ||
| async function runCodexDoctorJson(command, pathEnv) { | ||
| const { stdout } = await execFileAsync(command, ["doctor", "--json"], { | ||
| env: { | ||
| ...process.env, | ||
| PATH: pathEnv | ||
| }, | ||
| timeout: DOCTOR_PROBE_TIMEOUT_MS, | ||
| maxBuffer: DOCTOR_MAX_BUFFER | ||
| }); | ||
| return stdout; | ||
| } | ||
| /** | ||
| * Enrich the codex provider with a compact `codex doctor` health summary. | ||
| * Capability-probed: any failure (codex too old to know `--json`, timeout, | ||
| * unparseable output) yields `undefined`, leaving the doctor report unchanged. | ||
| * The `run` parameter is injectable for testing. | ||
| */ | ||
| async function enrichCodexDoctor(ctx, run = runCodexDoctorJson) { | ||
| try { | ||
| return parseCodexDoctorJson(await run(ctx.command, ctx.pathEnv)); | ||
| } catch (err) { | ||
| const salvaged = err.stdout; | ||
| return typeof salvaged === "string" ? parseCodexDoctorJson(salvaged) : void 0; | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/utils/codexExecutor.ts | ||
| function buildUsageStats(usage, model, durationMs, fellBack) { | ||
| return { | ||
| provider: "codex", | ||
| model, | ||
| inputTokens: usage?.input_tokens, | ||
| outputTokens: usage?.output_tokens, | ||
| cachedTokens: usage?.cached_input_tokens, | ||
| thinkingTokens: usage?.reasoning_output_tokens, | ||
| durationMs, | ||
| fellBack | ||
| }; | ||
| } | ||
| function formatStats(usage) { | ||
| if (!usage) return ""; | ||
| const parts = []; | ||
| if (usage.input_tokens != null) parts.push(`${usage.input_tokens.toLocaleString()} input tokens`); | ||
| if (usage.output_tokens != null) parts.push(`${usage.output_tokens.toLocaleString()} output tokens`); | ||
| if (usage.reasoning_output_tokens != null && usage.reasoning_output_tokens > 0) parts.push(`${usage.reasoning_output_tokens.toLocaleString()} thinking tokens`); | ||
| if (usage.cached_input_tokens != null && usage.cached_input_tokens > 0) parts.push(`${usage.cached_input_tokens.toLocaleString()} cached`); | ||
| return parts.length > 0 ? `\n\n[Codex stats: ${parts.join(", ")}]` : ""; | ||
| } | ||
| function parseCodexJsonlOutput(raw, model, durationMs, fellBack) { | ||
| const lines = raw.split("\n").filter((l) => l.trim().length > 0); | ||
| let lastAgentMessage; | ||
| let threadId; | ||
| let usage; | ||
| let lastError; | ||
| let sawJsonlEvent = false; | ||
| for (const line of lines) { | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(line); | ||
| } catch { | ||
| continue; | ||
| } | ||
| if (parsed && typeof parsed === "object" && typeof parsed.type === "string") sawJsonlEvent = true; | ||
| if (parsed.type === "thread.started") { | ||
| const thread = parsed; | ||
| if (thread.thread_id) threadId = thread.thread_id; | ||
| } | ||
| if (parsed.type === "item.completed") { | ||
| const item = parsed.item; | ||
| if (item?.type === "agent_message" && typeof item.text === "string" && item.text.length > 0) lastAgentMessage = item.text; | ||
| } | ||
| if (parsed.type === "turn.completed") usage = parsed.usage; | ||
| if (parsed.type === "turn.failed") lastError = parsed.error?.message ?? JSON.stringify(parsed); | ||
| if (parsed.type === "error") lastError = parsed.message ?? JSON.stringify(parsed); | ||
| } | ||
| if (lastError && !lastAgentMessage) throw new Error(`Codex error event: ${lastError}`); | ||
| if (!lastAgentMessage) { | ||
| if (sawJsonlEvent) { | ||
| const truncated = raw.length > EXECUTION.ERROR_TRUNCATE_LENGTH ? `${raw.slice(0, EXECUTION.ERROR_TRUNCATE_LENGTH)}…` : raw; | ||
| throw new Error(`Codex completed without an agent message${threadId ? ` (thread ${threadId})` : ""}. The run ended before producing a response — retry, or resume the thread via sessionId. Raw JSONL (truncated):\n${truncated}`); | ||
| } | ||
| Logger.debug("No parseable Codex JSONL events found, using raw text as the response"); | ||
| return { | ||
| response: raw, | ||
| threadId, | ||
| usage: buildUsageStats(usage, model, durationMs, fellBack) | ||
| }; | ||
| } | ||
| return { | ||
| response: lastAgentMessage + formatStats(usage), | ||
| threadId, | ||
| usage: buildUsageStats(usage, model, durationMs, fellBack) | ||
| }; | ||
| } | ||
| function isQuotaError(error) { | ||
| const msg = (error instanceof Error ? error.message : String(error)).toLowerCase(); | ||
| return ERROR_MESSAGES.QUOTA_SIGNALS.some((signal) => msg.includes(signal)); | ||
| } | ||
| function isArchivedSessionError(error) { | ||
| const msg = (error instanceof Error ? error.message : String(error)).toLowerCase(); | ||
| return ERROR_MESSAGES.ARCHIVED_SESSION_SIGNALS.some((signal) => msg.includes(signal)); | ||
| } | ||
| function isModelUnavailableError(error) { | ||
| const msg = (error instanceof Error ? error.message : String(error)).toLowerCase(); | ||
| return ERROR_MESSAGES.MODEL_UNAVAILABLE_SIGNALS.some((signal) => msg.includes(signal)); | ||
| } | ||
| function extractFirstJsonObject(text) { | ||
| const start = text.indexOf("{"); | ||
| if (start === -1) return null; | ||
| let depth = 0; | ||
| let inStr = false; | ||
| let esc = false; | ||
| for (let i = start; i < text.length; i++) { | ||
| const ch = text[i]; | ||
| if (inStr) { | ||
| if (esc) esc = false; | ||
| else if (ch === "\\") esc = true; | ||
| else if (ch === "\"") inStr = false; | ||
| } else if (ch === "\"") inStr = true; | ||
| else if (ch === "{") depth++; | ||
| else if (ch === "}") { | ||
| depth--; | ||
| if (depth === 0) return text.slice(start, i + 1); | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function parseCodexEdits(rawJson) { | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(extractFirstJsonObject(rawJson) ?? rawJson); | ||
| } catch { | ||
| return []; | ||
| } | ||
| if (!parsed || !Array.isArray(parsed.edits)) return []; | ||
| const edits = []; | ||
| for (const item of parsed.edits) { | ||
| if (!item || typeof item.file !== "string" || typeof item.oldCode !== "string" || typeof item.newCode !== "string") continue; | ||
| const oldCode = item.oldCode; | ||
| const newCode = item.newCode; | ||
| const startLine = typeof item.startLine === "number" && item.startLine > 0 ? item.startLine : 1; | ||
| const oldLineCount = oldCode === "" ? 0 : oldCode.split("\n").length; | ||
| const newLineCount = newCode === "" ? 0 : newCode.split("\n").length; | ||
| edits.push({ | ||
| filename: item.file, | ||
| oldStartLine: startLine, | ||
| oldEndLine: startLine + (oldLineCount > 0 ? oldLineCount - 1 : 0), | ||
| oldCode, | ||
| newStartLine: startLine, | ||
| newEndLine: startLine + (newLineCount > 0 ? newLineCount - 1 : 0), | ||
| newCode | ||
| }); | ||
| } | ||
| return edits; | ||
| } | ||
| function processCodexEditOutput(rawJson) { | ||
| const edits = parseCodexEdits(rawJson); | ||
| if (edits.length === 0) return "Codex proposed no edits for this request."; | ||
| const validation = validateChangeModeEdits(edits); | ||
| if (!validation.valid) return `Edit validation failed:\n${validation.errors.join("\n")}`; | ||
| let result = formatChangeModeResponse(edits); | ||
| if (edits.length > 5) result = `${summarizeChangeModeEdits(edits)}\n\n${result}`; | ||
| return result; | ||
| } | ||
| function buildArgs(prompt, model, sessionId, useStdin, includeDirs, sandboxMode = CLI.FLAGS.SANDBOX_READ_ONLY, schemaPath, reasoningEffort = DEFAULT_REASONING_EFFORT) { | ||
| const base = [CLI.COMMANDS.EXEC]; | ||
| if (sessionId) base.push(CLI.COMMANDS.RESUME); | ||
| base.push(CLI.FLAGS.SKIP_GIT); | ||
| if (!sessionId) base.push(CLI.FLAGS.EPHEMERAL); | ||
| if (process.env.ASK_CODEX_LOAD_USER_CONFIG !== "1") base.push(CLI.FLAGS.IGNORE_USER_CONFIG, CLI.FLAGS.IGNORE_RULES); | ||
| base.push(CLI.FLAGS.SANDBOX, sandboxMode, CLI.FLAGS.CONFIG, `model_reasoning_effort="${reasoningEffort}"`, CLI.FLAGS.JSON, CLI.FLAGS.MODEL, model); | ||
| if (schemaPath) base.push(CLI.FLAGS.OUTPUT_SCHEMA, schemaPath); | ||
| if (includeDirs?.length) for (const dir of includeDirs) base.push(CLI.FLAGS.ADD_DIR, dir); | ||
| if (sessionId) base.push(sessionId); | ||
| if (!useStdin) base.push(prompt); | ||
| return base; | ||
| } | ||
| async function executeCodexCLI(options) { | ||
| const model = options.model || MODELS.DEFAULT; | ||
| const reasoningEffort = options.reasoningEffort || DEFAULT_REASONING_EFFORT; | ||
| const sessionId = options.sessionId; | ||
| const editMode = options.editMode === true; | ||
| const outputSchema = options.outputSchema ?? (editMode ? CODEX_EDIT_SCHEMA : void 0); | ||
| const sandboxMode = options.sandbox === "workspace-write" ? CLI.FLAGS.SANDBOX_WORKSPACE_WRITE : CLI.FLAGS.SANDBOX_READ_ONLY; | ||
| const wantsSession = sessionId !== void 0; | ||
| const preferredEligible = options.preferred === true && !options.model && !wantsSession && !editMode && MODELS.PREFERRED !== MODELS.DEFAULT; | ||
| const dirsPart = options.includeDirs?.length ? [...options.includeDirs].sort().join(":") : ""; | ||
| const extraContext = `effort=${reasoningEffort};edit=${editMode ? 1 : 0};sandbox=${sandboxMode};dirs=${dirsPart}`; | ||
| const cacheKey = wantsSession || preferredEligible || outputSchema ? null : ResponseCache.buildKey("codex", options.prompt, model, extraContext); | ||
| if (cacheKey) { | ||
| const cached = responseCache.get(cacheKey); | ||
| if (cached) { | ||
| Logger.debug("Response cache hit for codex"); | ||
| return { | ||
| response: cached, | ||
| threadId: void 0, | ||
| usage: void 0 | ||
| }; | ||
| } | ||
| } | ||
| let schemaPath; | ||
| try { | ||
| if (outputSchema) { | ||
| schemaPath = join(tmpdir(), `codex-output-schema-${process.pid}-${randomUUID()}.json`); | ||
| writeFileSync(schemaPath, JSON.stringify(outputSchema), { mode: 384 }); | ||
| } | ||
| const useStdin = outputSchema !== void 0 || options.prompt.length > EXECUTION.STDIN_THRESHOLD_BYTES; | ||
| const stdinPayload = useStdin ? options.prompt : void 0; | ||
| const args = buildArgs(options.prompt, model, sessionId, useStdin, options.includeDirs, sandboxMode, schemaPath, reasoningEffort); | ||
| const timeoutMs = resolveTimeoutMs(EXECUTION.CODEX_TIMEOUT_ENV_VAR, EXECUTION.DEFAULT_CODEX_TIMEOUT_MS); | ||
| let downgradedFromPreferred = false; | ||
| if (preferredEligible) { | ||
| const preferredArgs = buildArgs(options.prompt, MODELS.PREFERRED, void 0, useStdin, options.includeDirs, sandboxMode, schemaPath, reasoningEffort); | ||
| const preferredStartedAt = Date.now(); | ||
| try { | ||
| return parseCodexJsonlOutput(await executeCommand(CLI.COMMANDS.CODEX, preferredArgs, options.onProgress, void 0, stdinPayload, timeoutMs), MODELS.PREFERRED, Date.now() - preferredStartedAt, false); | ||
| } catch (preferredError) { | ||
| const reason = preferredError instanceof Error ? preferredError.message : String(preferredError); | ||
| Logger.warn(`Preferred Codex model ${MODELS.PREFERRED} unavailable (${reason}); falling back to ${MODELS.DEFAULT}.`); | ||
| downgradedFromPreferred = true; | ||
| } | ||
| } | ||
| const startedAt = Date.now(); | ||
| try { | ||
| const result = parseCodexJsonlOutput(await executeCommand(CLI.COMMANDS.CODEX, args, options.onProgress, void 0, stdinPayload, timeoutMs), model, Date.now() - startedAt, downgradedFromPreferred); | ||
| if (cacheKey) responseCache.set(cacheKey, result.response); | ||
| return result; | ||
| } catch (error) { | ||
| if (sessionId && isArchivedSessionError(error)) throw new Error(`Codex session ${sessionId} is archived. Run \`codex unarchive ${sessionId}\` to resume it, or omit sessionId to start a new thread.`); | ||
| if (isQuotaError(error) && model !== MODELS.FALLBACK) { | ||
| Logger.warn(`${STATUS_MESSAGES.QUOTA_SWITCHING} Falling back to ${MODELS.FALLBACK}.`); | ||
| Logger.debug(`Status: ${STATUS_MESSAGES.FALLBACK_RETRY}`); | ||
| const fallbackArgs = buildArgs(options.prompt, MODELS.FALLBACK, sessionId, useStdin, options.includeDirs, sandboxMode, schemaPath, reasoningEffort); | ||
| const fallbackStartedAt = Date.now(); | ||
| try { | ||
| const raw = await executeCommand(CLI.COMMANDS.CODEX, fallbackArgs, options.onProgress, void 0, stdinPayload, timeoutMs); | ||
| Logger.warn(`Successfully executed with ${MODELS.FALLBACK} fallback.`); | ||
| Logger.debug(`Status: ${STATUS_MESSAGES.FALLBACK_SUCCESS}`); | ||
| return parseCodexJsonlOutput(raw, MODELS.FALLBACK, Date.now() - fallbackStartedAt, true); | ||
| } catch (fallbackError) { | ||
| const fallbackMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); | ||
| if (isModelUnavailableError(fallbackError)) { | ||
| const remediation = process.env.ASK_CODEX_FALLBACK_MODEL ? "Set ASK_CODEX_FALLBACK_MODEL to a model your account supports, or unset it to use the default (gpt-5.6-terra)." : "Set ASK_CODEX_FALLBACK_MODEL to a model your account supports."; | ||
| throw new Error(`${MODELS.DEFAULT} quota exceeded and the fallback model "${MODELS.FALLBACK}" is not available for this Codex account type (${fallbackMsg}). ${remediation}`); | ||
| } | ||
| throw new Error(`${MODELS.DEFAULT} quota exceeded, ${MODELS.FALLBACK} fallback also failed: ${fallbackMsg}. Run \`codex doctor\` to inspect your Codex CLI installation.`); | ||
| } | ||
| } | ||
| throw error; | ||
| } | ||
| } finally { | ||
| if (schemaPath) try { | ||
| unlinkSync(schemaPath); | ||
| } catch {} | ||
| } | ||
| } | ||
| //#endregion | ||
| export { Logger as C, executeCommand as S, createSessionUsage as _, parseCodexJsonlOutput as a, toolRegistry as b, CODEX_REASONING_EFFORTS as c, FACTORY_DEFAULT_REASONING_EFFORT as d, MODELS as f, registerTools as g, registerSessionUsageResource as h, parseCodexEdits as i, ERROR_MESSAGES as l, createUsageStatsTool as m, isModelUnavailableError as n, processCodexEditOutput as o, STATUS_MESSAGES as p, isQuotaError as r, enrichCodexDoctor as s, executeCodexCLI as t, FACTORY_DEFAULT_MODEL as u, executeTool as v, askResponseSchema as w, relativeDirSchema as x, getPromptMessage as y }; | ||
| //# sourceMappingURL=codexExecutor-DBI0E31B.js.map |
Sorry, the diff of this file is too big to display
| import { S as executeCommand, b as toolRegistry, c as CODEX_REASONING_EFFORTS, d as FACTORY_DEFAULT_REASONING_EFFORT, f as MODELS, l as ERROR_MESSAGES, o as processCodexEditOutput, p as STATUS_MESSAGES, t as executeCodexCLI, u as FACTORY_DEFAULT_MODEL, w as askResponseSchema, x as relativeDirSchema } from "./codexExecutor-DBI0E31B.js"; | ||
| import { z } from "zod"; | ||
| //#region src/tools/ask-codex.tool.ts | ||
| const askCodexArgsSchema = z.object({ | ||
| prompt: z.string().min(1).max(1e5).describe("The question, code review request, or analysis task to send to Codex CLI"), | ||
| model: z.string().optional().describe(`DO NOT set this parameter. The tool automatically uses ${MODELS.DEFAULT} and falls back to ${MODELS.FALLBACK} on quota errors. Only set this if the user explicitly requests a specific model.`), | ||
| reasoningEffort: z.enum(CODEX_REASONING_EFFORTS).optional().describe(`Codex reasoning effort for this call. Defaults to ${FACTORY_DEFAULT_REASONING_EFFORT}; /codex-review and /brainstorm use high for quality-first work.`), | ||
| sessionId: z.string().optional().describe("Optional Codex thread ID to resume a prior conversation. Use the [Thread ID: ...] value from a previous response to continue the same chat with full prior context."), | ||
| includeDirs: z.array(relativeDirSchema).optional().describe("Additional directories Codex may access alongside the working directory (maps to codex `--add-dir`, repeatable). Must be relative paths (e.g., 'packages/api'). Useful in monorepos where relevant context spans sibling packages."), | ||
| preferred: z.boolean().optional().describe(`Opt into ASK_CODEX_PREFERRED_MODEL when it is configured to differ from the ${MODELS.DEFAULT} default. The built-in preferred value is also ${MODELS.PREFERRED}, so normal calls and review skills should leave this unset.`), | ||
| sandbox: z.enum(["read-only", "workspace-write"]).optional().default("read-only").describe("Codex sandbox mode for this call. Defaults to 'read-only', which enforces the core review contract (Codex reads and proposes, the MCP client edits). Set 'workspace-write' ONLY as an explicit opt-out for flows that need Codex to write files itself, e.g. image generation. Review, second-opinion, and analysis flows must never set this.") | ||
| }); | ||
| const askCodexTool = { | ||
| name: "ask-codex", | ||
| description: `Send a prompt to OpenAI Codex CLI (defaults to ${FACTORY_DEFAULT_MODEL} with automatic fallback on quota errors). Use for code review, second opinions, analysis, and AI-to-AI collaboration. Do not override the model parameter unless the user explicitly asks. Returns both human-readable text and a structured response (provider, model, sessionId, usage) via outputSchema. The returned sessionId field maps to Codex's thread_id and can be passed back as sessionId to continue the conversation.`, | ||
| zodSchema: askCodexArgsSchema, | ||
| outputSchema: askResponseSchema, | ||
| annotations: { | ||
| title: "Ask Codex", | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: false, | ||
| openWorldHint: true | ||
| }, | ||
| prompt: { description: "Execute Codex CLI to get OpenAI Codex's response for code review and analysis." }, | ||
| category: "codex", | ||
| execute: async (args, onProgress, onUsage) => { | ||
| const { prompt, model, reasoningEffort, sessionId, includeDirs, preferred, sandbox } = args; | ||
| if (!prompt?.trim()) throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED); | ||
| const result = await executeCodexCLI({ | ||
| prompt, | ||
| model, | ||
| reasoningEffort, | ||
| sessionId, | ||
| includeDirs, | ||
| preferred, | ||
| sandbox, | ||
| onProgress | ||
| }); | ||
| if (result.usage) onUsage?.(result.usage); | ||
| const threadLine = result.threadId ? `\n\n[Thread ID: ${result.threadId}]` : ""; | ||
| return { | ||
| text: `${STATUS_MESSAGES.CODEX_RESPONSE}\n${result.response}${threadLine}`, | ||
| structuredContent: { | ||
| provider: "codex", | ||
| response: result.response, | ||
| model: result.usage?.model ?? model ?? MODELS.DEFAULT, | ||
| sessionId: result.threadId, | ||
| usage: result.usage | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
| const askCodexEditTool = { | ||
| name: "ask-codex-edit", | ||
| description: "Send a code edit request to OpenAI Codex CLI and get structured search/replace edit blocks back (via codex --output-schema). Codex reads the existing files (read-only) and proposes precise, applyable changes for Claude to apply. Use this when you want Codex to suggest specific code modifications to existing files rather than just analysis. Mirrors ask-gemini-edit.", | ||
| zodSchema: z.object({ | ||
| prompt: z.string().min(1).max(1e5).describe("Describe the code changes you want against existing files. Codex returns structured search/replace edit blocks (via --output-schema) that can be applied directly."), | ||
| model: z.string().optional().describe(`DO NOT set this parameter. The tool automatically uses ${MODELS.DEFAULT} and falls back to ${MODELS.FALLBACK} on quota errors.`), | ||
| sessionId: z.string().optional().describe("Optional Codex thread ID to resume a prior conversation. Use the [Thread ID: ...] value from a previous response to refine the same edit session."), | ||
| includeDirs: z.array(relativeDirSchema).optional().describe("Additional directories Codex may read alongside the working directory (maps to codex `--add-dir`). Must be relative paths (e.g., 'packages/api'). Useful in monorepos where relevant context spans sibling packages.") | ||
| }), | ||
| annotations: { | ||
| title: "Ask Codex (Edit Mode)", | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: false, | ||
| openWorldHint: true | ||
| }, | ||
| prompt: { description: "Execute Codex CLI with --output-schema to get structured edit suggestions for existing files." }, | ||
| category: "codex", | ||
| execute: async (args, onProgress, onUsage) => { | ||
| const { prompt, model, sessionId, includeDirs } = args; | ||
| if (!prompt?.trim()) throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED); | ||
| const result = await executeCodexCLI({ | ||
| prompt, | ||
| model, | ||
| sessionId, | ||
| includeDirs, | ||
| editMode: true, | ||
| onProgress | ||
| }); | ||
| if (result.usage) onUsage?.(result.usage); | ||
| const threadLine = result.threadId ? `\n\n[Thread ID: ${result.threadId}]` : ""; | ||
| return `${processCodexEditOutput(result.response)}${threadLine}`; | ||
| } | ||
| }; | ||
| const pingTool = { | ||
| name: "ping", | ||
| description: "Test connectivity with the MCP server", | ||
| zodSchema: z.object({ message: z.string().optional().describe("A message to echo back to test the connection") }), | ||
| annotations: { | ||
| title: "Ping", | ||
| readOnlyHint: true, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| }, | ||
| prompt: { description: "Echo test message to verify MCP server is working" }, | ||
| category: "simple", | ||
| execute: async (args, onProgress) => { | ||
| return executeCommand("echo", [args.message || "Pong from Codex MCP Server!"], onProgress); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/tools/index.ts | ||
| toolRegistry.push(askCodexTool, askCodexEditTool, pingTool); | ||
| //#endregion | ||
| export {}; | ||
| //# sourceMappingURL=tools-CVF7RWA7.js.map |
| {"version":3,"file":"tools-CVF7RWA7.js","names":[],"sources":["../src/tools/ask-codex.tool.ts","../src/tools/ask-codex-edit.tool.ts","../src/tools/simple-tools.ts","../src/tools/index.ts"],"sourcesContent":["import { type AskResponse, askResponseSchema, relativeDirSchema, type UnifiedTool } from \"@ask-llm/shared\";\nimport { z } from \"zod\";\nimport {\n CODEX_REASONING_EFFORTS,\n type CodexReasoningEffort,\n ERROR_MESSAGES,\n FACTORY_DEFAULT_MODEL,\n FACTORY_DEFAULT_REASONING_EFFORT,\n MODELS,\n STATUS_MESSAGES,\n} from \"../constants.js\";\nimport { executeCodexCLI } from \"../utils/codexExecutor.js\";\n\nconst askCodexArgsSchema = z.object({\n prompt: z\n .string()\n .min(1)\n .max(100000)\n .describe(\"The question, code review request, or analysis task to send to Codex CLI\"),\n model: z\n .string()\n .optional()\n .describe(\n `DO NOT set this parameter. The tool automatically uses ${MODELS.DEFAULT} and falls back to ${MODELS.FALLBACK} on quota errors. Only set this if the user explicitly requests a specific model.`,\n ),\n reasoningEffort: z\n .enum(CODEX_REASONING_EFFORTS)\n .optional()\n .describe(\n `Codex reasoning effort for this call. Defaults to ${FACTORY_DEFAULT_REASONING_EFFORT}; /codex-review and /brainstorm use high for quality-first work.`,\n ),\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Optional Codex thread ID to resume a prior conversation. Use the [Thread ID: ...] value from a previous response to continue the same chat with full prior context.\",\n ),\n includeDirs: z\n .array(relativeDirSchema)\n .optional()\n .describe(\n \"Additional directories Codex may access alongside the working directory (maps to codex `--add-dir`, repeatable). Must be relative paths (e.g., 'packages/api'). Useful in monorepos where relevant context spans sibling packages.\",\n ),\n preferred: z\n .boolean()\n .optional()\n .describe(\n `Opt into ASK_CODEX_PREFERRED_MODEL when it is configured to differ from the ${MODELS.DEFAULT} default. The built-in preferred value is also ${MODELS.PREFERRED}, so normal calls and review skills should leave this unset.`,\n ),\n sandbox: z\n .enum([\"read-only\", \"workspace-write\"])\n .optional()\n .default(\"read-only\")\n .describe(\n \"Codex sandbox mode for this call. Defaults to 'read-only', which enforces the core review contract (Codex reads and proposes, the MCP client edits). Set 'workspace-write' ONLY as an explicit opt-out for flows that need Codex to write files itself, e.g. image generation. Review, second-opinion, and analysis flows must never set this.\",\n ),\n});\n\nexport const askCodexTool: UnifiedTool = {\n name: \"ask-codex\",\n description: `Send a prompt to OpenAI Codex CLI (defaults to ${FACTORY_DEFAULT_MODEL} with automatic fallback on quota errors). Use for code review, second opinions, analysis, and AI-to-AI collaboration. Do not override the model parameter unless the user explicitly asks. Returns both human-readable text and a structured response (provider, model, sessionId, usage) via outputSchema. The returned sessionId field maps to Codex's thread_id and can be passed back as sessionId to continue the conversation.`,\n zodSchema: askCodexArgsSchema,\n outputSchema: askResponseSchema,\n annotations: {\n title: \"Ask Codex\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n prompt: {\n description: \"Execute Codex CLI to get OpenAI Codex's response for code review and analysis.\",\n },\n category: \"codex\",\n execute: async (args, onProgress, onUsage) => {\n const { prompt, model, reasoningEffort, sessionId, includeDirs, preferred, sandbox } = args;\n if (!prompt?.trim()) {\n throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED);\n }\n\n const result = await executeCodexCLI({\n prompt: prompt as string,\n model: model as string | undefined,\n reasoningEffort: reasoningEffort as CodexReasoningEffort | undefined,\n sessionId: sessionId as string | undefined,\n includeDirs: includeDirs as string[] | undefined,\n preferred: preferred as boolean | undefined,\n sandbox: sandbox as \"read-only\" | \"workspace-write\" | undefined,\n onProgress,\n });\n\n if (result.usage) onUsage?.(result.usage);\n\n const threadLine = result.threadId ? `\\n\\n[Thread ID: ${result.threadId}]` : \"\";\n const text = `${STATUS_MESSAGES.CODEX_RESPONSE}\\n${result.response}${threadLine}`;\n const structured: AskResponse = {\n provider: \"codex\",\n response: result.response,\n model: result.usage?.model ?? (model as string | undefined) ?? MODELS.DEFAULT,\n sessionId: result.threadId,\n usage: result.usage,\n };\n return { text, structuredContent: structured as unknown as Record<string, unknown> };\n },\n};\n","import { relativeDirSchema, type UnifiedTool } from \"@ask-llm/shared\";\nimport { z } from \"zod\";\nimport { ERROR_MESSAGES, MODELS } from \"../constants.js\";\nimport { executeCodexCLI, processCodexEditOutput } from \"../utils/codexExecutor.js\";\n\nconst askCodexEditArgsSchema = z.object({\n prompt: z\n .string()\n .min(1)\n .max(100000)\n .describe(\n \"Describe the code changes you want against existing files. Codex returns structured search/replace edit blocks (via --output-schema) that can be applied directly.\",\n ),\n model: z\n .string()\n .optional()\n .describe(\n `DO NOT set this parameter. The tool automatically uses ${MODELS.DEFAULT} and falls back to ${MODELS.FALLBACK} on quota errors.`,\n ),\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Optional Codex thread ID to resume a prior conversation. Use the [Thread ID: ...] value from a previous response to refine the same edit session.\",\n ),\n includeDirs: z\n .array(relativeDirSchema)\n .optional()\n .describe(\n \"Additional directories Codex may read alongside the working directory (maps to codex `--add-dir`). Must be relative paths (e.g., 'packages/api'). Useful in monorepos where relevant context spans sibling packages.\",\n ),\n});\n\nexport const askCodexEditTool: UnifiedTool = {\n name: \"ask-codex-edit\",\n description:\n \"Send a code edit request to OpenAI Codex CLI and get structured search/replace edit blocks back (via codex --output-schema). Codex reads the existing files (read-only) and proposes precise, applyable changes for Claude to apply. Use this when you want Codex to suggest specific code modifications to existing files rather than just analysis. Mirrors ask-gemini-edit.\",\n zodSchema: askCodexEditArgsSchema,\n annotations: {\n title: \"Ask Codex (Edit Mode)\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n prompt: {\n description: \"Execute Codex CLI with --output-schema to get structured edit suggestions for existing files.\",\n },\n category: \"codex\",\n execute: async (args, onProgress, onUsage) => {\n const { prompt, model, sessionId, includeDirs } = args;\n if (!prompt?.trim()) {\n throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED);\n }\n\n const result = await executeCodexCLI({\n prompt: prompt as string,\n model: model as string | undefined,\n sessionId: sessionId as string | undefined,\n includeDirs: includeDirs as string[] | undefined,\n editMode: true,\n onProgress,\n });\n\n if (result.usage) onUsage?.(result.usage);\n\n const threadLine = result.threadId ? `\\n\\n[Thread ID: ${result.threadId}]` : \"\";\n return `${processCodexEditOutput(result.response)}${threadLine}`;\n },\n};\n","import type { UnifiedTool } from \"@ask-llm/shared\";\nimport { executeCommand } from \"@ask-llm/shared\";\nimport { z } from \"zod\";\n\nconst pingArgsSchema = z.object({\n message: z.string().optional().describe(\"A message to echo back to test the connection\"),\n});\n\nexport const pingTool: UnifiedTool = {\n name: \"ping\",\n description: \"Test connectivity with the MCP server\",\n zodSchema: pingArgsSchema,\n annotations: {\n title: \"Ping\",\n readOnlyHint: true,\n idempotentHint: true,\n openWorldHint: false,\n },\n prompt: {\n description: \"Echo test message to verify MCP server is working\",\n },\n category: \"simple\",\n execute: async (args, onProgress) => {\n const message = args.message || \"Pong from Codex MCP Server!\";\n return executeCommand(\"echo\", [message as string], onProgress);\n },\n};\n","import { toolRegistry } from \"@ask-llm/shared\";\nimport { askCodexTool } from \"./ask-codex.tool.js\";\nimport { askCodexEditTool } from \"./ask-codex-edit.tool.js\";\nimport { pingTool } from \"./simple-tools.js\";\n\ntoolRegistry.push(askCodexTool, askCodexEditTool, pingTool);\n\nexport { executeTool, getPromptMessage, toolRegistry } from \"@ask-llm/shared\";\n"],"mappings":";;;AAaA,MAAM,qBAAqB,EAAE,OAAO;CAClC,QAAQ,EACL,OAAO,CAAC,CACR,IAAI,CAAC,CAAC,CACN,IAAI,GAAM,CAAC,CACX,SAAS,0EAA0E;CACtF,OAAO,EACJ,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,0DAA0D,OAAO,QAAQ,qBAAqB,OAAO,SAAS,kFAChH;CACF,iBAAiB,EACd,KAAK,uBAAuB,CAAC,CAC7B,SAAS,CAAC,CACV,SACC,qDAAqD,iCAAiC,iEACxF;CACF,WAAW,EACR,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,qKACF;CACF,aAAa,EACV,MAAM,iBAAiB,CAAC,CACxB,SAAS,CAAC,CACV,SACC,oOACF;CACF,WAAW,EACR,QAAQ,CAAC,CACT,SAAS,CAAC,CACV,SACC,+EAA+E,OAAO,QAAQ,iDAAiD,OAAO,UAAU,6DAClK;CACF,SAAS,EACN,KAAK,CAAC,aAAa,iBAAiB,CAAC,CAAC,CACtC,SAAS,CAAC,CACV,QAAQ,WAAW,CAAC,CACpB,SACC,gVACF;AACJ,CAAC;AAED,MAAa,eAA4B;CACvC,MAAM;CACN,aAAa,kDAAkD,sBAAsB;CACrF,WAAW;CACX,cAAc;CACd,aAAa;EACX,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;CACjB;CACA,QAAQ,EACN,aAAa,iFACf;CACA,UAAU;CACV,SAAS,OAAO,MAAM,YAAY,YAAY;EAC5C,MAAM,EAAE,QAAQ,OAAO,iBAAiB,WAAW,aAAa,WAAW,YAAY;EACvF,IAAI,CAAC,QAAQ,KAAK,GAChB,MAAM,IAAI,MAAM,eAAe,kBAAkB;EAGnD,MAAM,SAAS,MAAM,gBAAgB;GAC3B;GACD;GACU;GACN;GACE;GACF;GACF;GACT;EACF,CAAC;EAED,IAAI,OAAO,OAAO,UAAU,OAAO,KAAK;EAExC,MAAM,aAAa,OAAO,WAAW,mBAAmB,OAAO,SAAS,KAAK;EAS7E,OAAO;GAAE,MAAA,GARO,gBAAgB,eAAe,IAAI,OAAO,WAAW;GAQtD,mBAAmB;IANhC,UAAU;IACV,UAAU,OAAO;IACjB,OAAO,OAAO,OAAO,SAAU,SAAgC,OAAO;IACtE,WAAW,OAAO;IAClB,OAAO,OAAO;GAE2B;EAAwC;CACrF;AACF;ACvEA,MAAa,mBAAgC;CAC3C,MAAM;CACN,aACE;CACF,WAhC6B,EAAE,OAAO;EACtC,QAAQ,EACL,OAAO,CAAC,CACR,IAAI,CAAC,CAAC,CACN,IAAI,GAAM,CAAC,CACX,SACC,oKACF;EACF,OAAO,EACJ,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,0DAA0D,OAAO,QAAQ,qBAAqB,OAAO,SAAS,kBAChH;EACF,WAAW,EACR,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,mJACF;EACF,aAAa,EACV,MAAM,iBAAiB,CAAC,CACxB,SAAS,CAAC,CACV,SACC,sNACF;CACJ,CAMa;CACX,aAAa;EACX,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;CACjB;CACA,QAAQ,EACN,aAAa,gGACf;CACA,UAAU;CACV,SAAS,OAAO,MAAM,YAAY,YAAY;EAC5C,MAAM,EAAE,QAAQ,OAAO,WAAW,gBAAgB;EAClD,IAAI,CAAC,QAAQ,KAAK,GAChB,MAAM,IAAI,MAAM,eAAe,kBAAkB;EAGnD,MAAM,SAAS,MAAM,gBAAgB;GAC3B;GACD;GACI;GACE;GACb,UAAU;GACV;EACF,CAAC;EAED,IAAI,OAAO,OAAO,UAAU,OAAO,KAAK;EAExC,MAAM,aAAa,OAAO,WAAW,mBAAmB,OAAO,SAAS,KAAK;EAC7E,OAAO,GAAG,uBAAuB,OAAO,QAAQ,IAAI;CACtD;AACF;AC7DA,MAAa,WAAwB;CACnC,MAAM;CACN,aAAa;CACb,WAPqB,EAAE,OAAO,EAC9B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,+CAA+C,EACzF,CAKa;CACX,aAAa;EACX,OAAO;EACP,cAAc;EACd,gBAAgB;EAChB,eAAe;CACjB;CACA,QAAQ,EACN,aAAa,oDACf;CACA,UAAU;CACV,SAAS,OAAO,MAAM,eAAe;EAEnC,OAAO,eAAe,QAAQ,CADd,KAAK,WAAW,6BACgB,GAAG,UAAU;CAC/D;AACF;;;ACrBA,aAAa,KAAK,cAAc,kBAAkB,QAAQ"} |
+1
-1
| #!/usr/bin/env node | ||
| import { C as Logger } from "./codexExecutor-CQOWlbPt.js"; | ||
| import { C as Logger } from "./codexExecutor-DBI0E31B.js"; | ||
| import { startServer } from "./index.js"; | ||
@@ -4,0 +4,0 @@ //#region src/cli.ts |
+1
-1
@@ -1,2 +0,2 @@ | ||
| import { a as parseCodexJsonlOutput, i as parseCodexEdits, n as isModelUnavailableError, o as processCodexEditOutput, r as isQuotaError, s as enrichCodexDoctor, t as executeCodexCLI } from "./codexExecutor-CQOWlbPt.js"; | ||
| import { a as parseCodexJsonlOutput, i as parseCodexEdits, n as isModelUnavailableError, o as processCodexEditOutput, r as isQuotaError, s as enrichCodexDoctor, t as executeCodexCLI } from "./codexExecutor-DBI0E31B.js"; | ||
| export { enrichCodexDoctor, executeCodexCLI, isModelUnavailableError, isQuotaError, parseCodexEdits, parseCodexJsonlOutput, processCodexEditOutput }; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index-BRK854Ug.d.ts","names":["ROLES","USER","ASSISTANT","CONTENT_TYPES","TEXT","STATUS","SUCCESS","ERROR","FAILED","REPORT","NOTIFICATIONS","PROGRESS","KEEPALIVE_INTERVAL","DEFAULT_TIMEOUT_MS","DEFAULT_CODEX_TIMEOUT_MS","DEFAULT_CLAUDE_TIMEOUT_MS","DEFAULT_OLLAMA_TIMEOUT_MS","TIMEOUT_ENV_VAR","CODEX_TIMEOUT_ENV_VAR","CLAUDE_TIMEOUT_ENV_VAR","GEMINI_TIMEOUT_ENV_VAR","OLLAMA_TIMEOUT_ENV_VAR","ERROR_TRUNCATE_LENGTH","STDIN_THRESHOLD_BYTES","prompt","message","key","name","status","CheckStatus","message","fix","summary","remediation","heading","overall","OverallStatus","checks","ProviderEnrichmentCheck","command","available","cliPath","cliVersion","error","enrichment","ProviderEnrichment","generatedAt","environment","nodeVersion","nodeOk","platform","arch","resolvedPath","askLlmPath","timeoutMs","codexTimeoutMs","claudeTimeoutMs","geminiTimeoutMs","providers","ProviderProbe","DiagnosticCheck","key","versionArgs","installHint","probeAvailability","Promise","enrich","pathEnv","ctx","ProviderSpec","DiagnosticReport","report","PROVIDERS","provider","ProviderName","model","inputTokens","outputTokens","cachedTokens","thinkingTokens","durationMs","fellBack","totalCalls","totalInputTokens","totalOutputTokens","totalCachedTokens","totalThinkingTokens","totalDurationMs","fallbackCount","byProvider","Record","ProviderUsageSnapshot","byModel","calls","record","UsageStats","stats","snapshot","SessionUsageSnapshot","reset","SessionUsage","text","structuredContent","Record","StructuredToolResult","name","description","zodSchema","ZodTypeAny","outputSchema","annotations","ToolAnnotations","prompt","arguments","Array","required","execute","BaseToolArguments","args","newOutput","onProgress","UsageStats","stats","onUsage","Promise","ToolResult","category","ProviderName","UnifiedTool","toolName"],"sources":["../../shared/dist/constants.d.ts","../../shared/dist/doctor.d.ts","../../shared/dist/providers.d.ts","../../shared/dist/usage.d.ts","../../shared/dist/registry.d.ts"],"mappings":";;;;;UAkCiB,iBAAA;EACbwB,MAAAA;EACAC,OAAAA;EAAAA,CACCC,GAAAA;AAAAA;;;KCrCO,WAAA;AAAA,KACA,aAAA;AAAA,UAOK,uBAAA;EACbC,IAAAA;EACAC,MAAAA,EAAQ,WAAW;EACnBI,OAAAA;EACAC,WAAAA;AAAAA;AAAAA,UAEa,kBAAA;EACbC,OAAAA;EACAC,OAAAA,EAAS,aAAA;EACTE,MAAAA,EAAQ,uBAAuB;AAAA;;;;;;;;;ADiBnC;cE3BqB,SAAA;AAAA,KACT,YAAA,WAAuB,SAAS;;;UCP3B,UAAA;EACboC,QAAAA,EAAU,YAAY;EACtBE,KAAAA;EACAC,WAAAA;EACAC,YAAAA;EACAC,YAAAA;EACAC,cAAAA;EACAC,UAAAA;EACAC,QAAAA;AAAAA;;;UCJa,oBAAA;EACboB,IAAAA;EACAC,iBAAAA,EAAmB,MAAM;AAAA;AAAA,KAEjB,UAAA,YAAsB,oBAAoB;AAAA,UACrC,WAAA;EACbG,IAAAA;EACAC,WAAAA;EACAC,SAAAA,EAAW,UAAA;EACXE,YAAAA,GAAe,UAAA;EACfC,WAAAA,GAAc,eAAA;EACdE,MAAAA;IACIN,WAAAA;IACAO,SAAAA,GAAY,KAAA;MACRR,IAAAA;MACAC,WAAAA;MACAS,QAAAA;IAAAA;EAAAA;EAGRC,OAAAA,GAAUE,IAAAA,EAAM,iBAAA,EAAmBE,UAAAA,IAAcD,SAAAA,mBAA4BI,OAAAA,IAAWD,KAAAA,EAAO,UAAA,cAAwB,OAAA,CAAQ,UAAA;EAC/HI,QAAAA,cAAsB,YAAA;AAAA;AAAA,cAEL,YAAA,EAAc,WAAW;AAAA,iBACtB,aAAA,CAAYG,QAAAA,UAAkBX,IAAAA,EAAM,iBAAA,EAAmBE,UAAAA,IAAcD,SAAAA,mBAA4BI,OAAAA,IAAWD,KAAAA,EAAO,UAAA,YAAsB,OAAA,CAAQ,UAAA;AAAA,iBACjJ,kBAAA,CAAiBO,QAAAA,UAAkBX,IAAAA,EAAM,MAAM"} | ||
| {"version":3,"file":"index-BRK854Ug.d.ts","names":["ROLES","USER","ASSISTANT","CONTENT_TYPES","TEXT","STATUS","SUCCESS","ERROR","FAILED","REPORT","NOTIFICATIONS","PROGRESS","KEEPALIVE_INTERVAL","DEFAULT_TIMEOUT_MS","DEFAULT_CODEX_TIMEOUT_MS","DEFAULT_CLAUDE_TIMEOUT_MS","DEFAULT_OLLAMA_TIMEOUT_MS","TIMEOUT_ENV_VAR","CODEX_TIMEOUT_ENV_VAR","CLAUDE_TIMEOUT_ENV_VAR","GEMINI_TIMEOUT_ENV_VAR","OLLAMA_TIMEOUT_ENV_VAR","ERROR_TRUNCATE_LENGTH","STDIN_THRESHOLD_BYTES","prompt","message","key","name","status","CheckStatus","message","fix","summary","remediation","heading","overall","OverallStatus","checks","ProviderEnrichmentCheck","available","error","command","cliPath","cliVersion","enrichment","ProviderEnrichment","generatedAt","environment","nodeVersion","nodeOk","platform","arch","resolvedPath","askLlmPath","timeoutMs","codexTimeoutMs","claudeTimeoutMs","geminiTimeoutMs","providers","ProviderProbe","DiagnosticCheck","key","versionArgs","installHint","probeAvailability","Promise","assessVersion","version","probeError","ProviderVersionAssessment","enrich","pathEnv","ctx","ProviderSpec","DiagnosticReport","report","PROVIDERS","provider","ProviderName","model","inputTokens","outputTokens","cachedTokens","thinkingTokens","durationMs","fellBack","totalCalls","totalInputTokens","totalOutputTokens","totalCachedTokens","totalThinkingTokens","totalDurationMs","fallbackCount","byProvider","Record","ProviderUsageSnapshot","byModel","calls","record","UsageStats","stats","snapshot","SessionUsageSnapshot","reset","SessionUsage","text","structuredContent","Record","StructuredToolResult","name","description","zodSchema","ZodTypeAny","outputSchema","annotations","ToolAnnotations","prompt","arguments","Array","required","execute","BaseToolArguments","args","newOutput","onProgress","UsageStats","stats","onUsage","Promise","ToolResult","category","ProviderName","UnifiedTool","toolName"],"sources":["../../shared/dist/constants.d.ts","../../shared/dist/doctor.d.ts","../../shared/dist/providers.d.ts","../../shared/dist/usage.d.ts","../../shared/dist/registry.d.ts"],"mappings":";;;;;UAkCiB,iBAAA;EACbwB,MAAAA;EACAC,OAAAA;EAAAA,CACCC,GAAAA;AAAAA;;;KCrCO,WAAA;AAAA,KACA,aAAA;AAAA,UAOK,uBAAA;EACbC,IAAAA;EACAC,MAAAA,EAAQ,WAAW;EACnBI,OAAAA;EACAC,WAAAA;AAAAA;AAAAA,UAEa,kBAAA;EACbC,OAAAA;EACAC,OAAAA,EAAS,aAAA;EACTE,MAAAA,EAAQ,uBAAuB;AAAA;;;;;;;;;ADiBnC;cE3BqB,SAAA;AAAA,KACT,YAAA,WAAuB,SAAS;;;UCP3B,UAAA;EACbwC,QAAAA,EAAU,YAAY;EACtBE,KAAAA;EACAC,WAAAA;EACAC,YAAAA;EACAC,YAAAA;EACAC,cAAAA;EACAC,UAAAA;EACAC,QAAAA;AAAAA;;;UCJa,oBAAA;EACboB,IAAAA;EACAC,iBAAAA,EAAmB,MAAM;AAAA;AAAA,KAEjB,UAAA,YAAsB,oBAAoB;AAAA,UACrC,WAAA;EACbG,IAAAA;EACAC,WAAAA;EACAC,SAAAA,EAAW,UAAA;EACXE,YAAAA,GAAe,UAAA;EACfC,WAAAA,GAAc,eAAA;EACdE,MAAAA;IACIN,WAAAA;IACAO,SAAAA,GAAY,KAAA;MACRR,IAAAA;MACAC,WAAAA;MACAS,QAAAA;IAAAA;EAAAA;EAGRC,OAAAA,GAAUE,IAAAA,EAAM,iBAAA,EAAmBE,UAAAA,IAAcD,SAAAA,mBAA4BI,OAAAA,IAAWD,KAAAA,EAAO,UAAA,cAAwB,OAAA,CAAQ,UAAA;EAC/HI,QAAAA,cAAsB,YAAA;AAAA;AAAA,cAEL,YAAA,EAAc,WAAW;AAAA,iBACtB,aAAA,CAAYG,QAAAA,UAAkBX,IAAAA,EAAM,iBAAA,EAAmBE,UAAAA,IAAcD,SAAAA,mBAA4BI,OAAAA,IAAWD,KAAAA,EAAO,UAAA,YAAsB,OAAA,CAAQ,UAAA;AAAA,iBACjJ,kBAAA,CAAiBO,QAAAA,UAAkBX,IAAAA,EAAM,MAAM"} |
+2
-2
@@ -1,3 +0,3 @@ | ||
| import { C as Logger, _ as createSessionUsage, b as toolRegistry, g as registerTools, h as registerSessionUsageResource, m as createUsageStatsTool, v as executeTool, y as getPromptMessage } from "./codexExecutor-CQOWlbPt.js"; | ||
| import "./tools-CfSNYpIz.js"; | ||
| import { C as Logger, _ as createSessionUsage, b as toolRegistry, g as registerTools, h as registerSessionUsageResource, m as createUsageStatsTool, v as executeTool, y as getPromptMessage } from "./codexExecutor-DBI0E31B.js"; | ||
| import "./tools-CVF7RWA7.js"; | ||
| import { createRequire } from "node:module"; | ||
@@ -4,0 +4,0 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
+2
-2
@@ -1,3 +0,3 @@ | ||
| import { b as toolRegistry, v as executeTool, y as getPromptMessage } from "./codexExecutor-CQOWlbPt.js"; | ||
| import "./tools-CfSNYpIz.js"; | ||
| import { b as toolRegistry, v as executeTool, y as getPromptMessage } from "./codexExecutor-DBI0E31B.js"; | ||
| import "./tools-CVF7RWA7.js"; | ||
| export { executeTool, getPromptMessage, toolRegistry }; |
+1
-1
| { | ||
| "name": "@ask-llm/codex-mcp", | ||
| "version": "0.7.1", | ||
| "version": "0.7.2", | ||
| "mcpName": "io.github.Lykhoyda/ask-codex", | ||
@@ -5,0 +5,0 @@ "description": "MCP server for OpenAI Codex CLI integration - for Claude IDE and other IDEs", |
| import { ZodError, z } from "zod"; | ||
| import { createHash, randomUUID } from "node:crypto"; | ||
| import { existsSync, readdirSync, unlinkSync, writeFileSync } from "node:fs"; | ||
| import * as os from "node:os"; | ||
| import { homedir, tmpdir } from "node:os"; | ||
| import * as path from "node:path"; | ||
| import { delimiter, isAbsolute, join } from "node:path"; | ||
| import { execFile, execFileSync, spawn } from "node:child_process"; | ||
| import { promisify } from "node:util"; | ||
| //#region ../shared/dist/providers.js | ||
| /** | ||
| * Single source of truth for the provider list (ADR-128). | ||
| * | ||
| * Every provider-name enum, type union, or user-facing provider list in the | ||
| * monorepo must derive from this tuple — hand-maintained copies drifted when | ||
| * antigravity was added (see BUGS.md 2026-07-02 audit entry). | ||
| */ | ||
| const PROVIDERS = [ | ||
| "gemini", | ||
| "codex", | ||
| "claude", | ||
| "ollama", | ||
| "antigravity" | ||
| ]; | ||
| //#endregion | ||
| //#region ../shared/dist/askResponse.js | ||
| const usageStatsSchema = z.object({ | ||
| provider: z.enum(PROVIDERS), | ||
| model: z.string(), | ||
| inputTokens: z.number().optional(), | ||
| outputTokens: z.number().optional(), | ||
| cachedTokens: z.number().optional(), | ||
| thinkingTokens: z.number().optional(), | ||
| durationMs: z.number(), | ||
| fellBack: z.boolean() | ||
| }); | ||
| const askResponseSchema = z.object({ | ||
| provider: z.enum(PROVIDERS), | ||
| response: z.string(), | ||
| model: z.string(), | ||
| sessionId: z.string().optional(), | ||
| usage: usageStatsSchema.optional() | ||
| }); | ||
| //#endregion | ||
| //#region ../shared/dist/constants.js | ||
| const LOG_PREFIX = "[GMCPT]"; | ||
| const LOG_LEVEL_ENV_VAR = "GMCPT_LOG_LEVEL"; | ||
| const PROTOCOL = { | ||
| ROLES: { | ||
| USER: "user", | ||
| ASSISTANT: "assistant" | ||
| }, | ||
| CONTENT_TYPES: { TEXT: "text" }, | ||
| STATUS: { | ||
| SUCCESS: "success", | ||
| ERROR: "error", | ||
| FAILED: "failed", | ||
| REPORT: "report" | ||
| }, | ||
| NOTIFICATIONS: { PROGRESS: "notifications/progress" }, | ||
| KEEPALIVE_INTERVAL: 25e3 | ||
| }; | ||
| const EXECUTION = { | ||
| DEFAULT_TIMEOUT_MS: 21e4, | ||
| DEFAULT_CODEX_TIMEOUT_MS: 8e5, | ||
| DEFAULT_CLAUDE_TIMEOUT_MS: 6e5, | ||
| DEFAULT_OLLAMA_TIMEOUT_MS: 6e5, | ||
| TIMEOUT_ENV_VAR: "GMCPT_TIMEOUT_MS", | ||
| CODEX_TIMEOUT_ENV_VAR: "ASK_CODEX_TIMEOUT_MS", | ||
| CLAUDE_TIMEOUT_ENV_VAR: "ASK_CLAUDE_TIMEOUT_MS", | ||
| GEMINI_TIMEOUT_ENV_VAR: "ASK_GEMINI_TIMEOUT_MS", | ||
| OLLAMA_TIMEOUT_ENV_VAR: "ASK_OLLAMA_TIMEOUT_MS", | ||
| ERROR_TRUNCATE_LENGTH: 2e3, | ||
| STDIN_THRESHOLD_BYTES: 16384 | ||
| }; | ||
| //#endregion | ||
| //#region ../shared/dist/logger.js | ||
| const LOG_LEVEL_PRIORITY = { | ||
| debug: 0, | ||
| info: 1, | ||
| warn: 2, | ||
| error: 3 | ||
| }; | ||
| function getLogLevel() { | ||
| const env = process.env[LOG_LEVEL_ENV_VAR]?.toLowerCase(); | ||
| if (env && env in LOG_LEVEL_PRIORITY) return env; | ||
| return "warn"; | ||
| } | ||
| function shouldLog(level) { | ||
| return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[getLogLevel()]; | ||
| } | ||
| var Logger = class Logger { | ||
| static _nextCommandId = 0; | ||
| static _commandStartTimes = /* @__PURE__ */ new Map(); | ||
| static formatMessage(message) { | ||
| return `${LOG_PREFIX} ${message}`; | ||
| } | ||
| static warn(message, ...args) { | ||
| if (!shouldLog("warn")) return; | ||
| console.warn(Logger.formatMessage(message), ...args); | ||
| } | ||
| static error(message, ...args) { | ||
| if (!shouldLog("error")) return; | ||
| console.error(Logger.formatMessage(message), ...args); | ||
| } | ||
| static debug(message, ...args) { | ||
| if (!shouldLog("debug")) return; | ||
| console.warn(Logger.formatMessage(message), ...args); | ||
| } | ||
| static info(message, ...args) { | ||
| if (!shouldLog("info")) return; | ||
| console.warn(Logger.formatMessage(message), ...args); | ||
| } | ||
| static toolInvocation(toolName, args) { | ||
| Logger.warn(`Tool "${toolName}" raw args:`, JSON.stringify(args, null, 2)); | ||
| } | ||
| static toolParsedArgs(prompt, model, sandbox, changeMode) { | ||
| Logger.warn(`Parsed prompt: "${prompt}"\nmodel: ${model ?? "default"}, sandbox: ${sandbox ?? false}, changeMode: ${changeMode ?? false}`); | ||
| } | ||
| static commandExecution(command, args) { | ||
| const commandId = Logger._nextCommandId++; | ||
| Logger._commandStartTimes.set(commandId, Date.now()); | ||
| Logger.warn(`[cmd:${commandId}] Starting: ${command} ${args.map((arg) => `"${arg}"`).join(" ")}`); | ||
| return commandId; | ||
| } | ||
| static commandComplete(commandId, exitCode, outputLength) { | ||
| const startTime = Logger._commandStartTimes.get(commandId); | ||
| const elapsed = startTime ? ((Date.now() - startTime) / 1e3).toFixed(1) : "?"; | ||
| Logger.warn(`[cmd:${commandId}] [${elapsed}s] Process finished with exit code: ${exitCode}`); | ||
| if (outputLength !== void 0) Logger.warn(`[cmd:${commandId}] Response: ${outputLength} chars`); | ||
| Logger._commandStartTimes.delete(commandId); | ||
| } | ||
| static checkNodeVersion(minMajor = 20) { | ||
| if (parseInt(process.versions.node.split(".")[0], 10) < minMajor) Logger.error(`Node.js v${process.versions.node} detected — v${minMajor}+ required. Some providers (e.g., gemini-cli) use ES2024 features that will crash on older runtimes.`); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region ../shared/dist/changeMode/changeModeParser.js | ||
| function validateChangeModeEdits(edits) { | ||
| const errors = []; | ||
| for (const edit of edits) { | ||
| if (!edit.filename) errors.push("Edit missing filename"); | ||
| if (edit.oldStartLine > edit.oldEndLine) errors.push(`Invalid line range for ${edit.filename}: ${edit.oldStartLine} > ${edit.oldEndLine}`); | ||
| if (edit.newStartLine > edit.newEndLine) errors.push(`Invalid new line range for ${edit.filename}: ${edit.newStartLine} > ${edit.newEndLine}`); | ||
| if (!edit.oldCode && !edit.newCode) errors.push(`Empty edit for ${edit.filename}`); | ||
| } | ||
| return { | ||
| valid: errors.length === 0, | ||
| errors | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/changeMode/changeModeTranslator.js | ||
| function formatChangeModeResponse(edits, chunkInfo) { | ||
| const header = chunkInfo && chunkInfo.total > 1 ? `[CHANGEMODE OUTPUT - Chunk ${chunkInfo.current} of ${chunkInfo.total}] | ||
| Gemini has analyzed your codebase and generated edits across ${chunkInfo.total} chunks. | ||
| This chunk contains ${edits.length} complete edit${edits.length === 1 ? "" : "s"} that can be applied independently. | ||
| Each chunk contains self-contained edits grouped by file. You can safely apply these edits | ||
| before fetching the next chunk. | ||
| ` : `[CHANGEMODE OUTPUT - Gemini has analyzed the files and provided these edits] | ||
| I have prepared ${edits.length} modification${edits.length === 1 ? "" : "s"} for your codebase. | ||
| IMPORTANT: Apply these edits directly WITHOUT reading the files first. The edits below contain exact text matches from the current file contents. | ||
| `; | ||
| const instructions = edits.map((edit, index) => { | ||
| return `### Edit ${index + 1}: ${edit.filename} | ||
| Replace this exact text: | ||
| \`\`\` | ||
| ${edit.oldCode} | ||
| \`\`\` | ||
| With this text: | ||
| \`\`\` | ||
| ${edit.newCode} | ||
| \`\`\` | ||
| `; | ||
| }).join("\n"); | ||
| let footer = ` | ||
| --- | ||
| Apply these edits in order. Each edit uses exact string matching, so the old_str must match exactly what appears between the code blocks.`; | ||
| if (chunkInfo && chunkInfo.current < chunkInfo.total && chunkInfo.cacheKey) footer += ` | ||
| --- | ||
| **Next Step**: After applying the edits above, retrieve the next chunk (${chunkInfo.current + 1} of ${chunkInfo.total}) by calling the **fetch-chunk** MCP tool with: | ||
| - **cacheKey**: \`${chunkInfo.cacheKey}\` | ||
| - **chunkIndex**: \`${chunkInfo.current + 1}\` | ||
| There ${chunkInfo.total - chunkInfo.current === 1 ? "is" : "are"} ${chunkInfo.total - chunkInfo.current} more chunk${chunkInfo.total - chunkInfo.current === 1 ? "" : "s"} containing additional edits. | ||
| **CONTINUE**: You are working on a multi-chunk changeMode response. After applying these edits, fetch the next chunk to continue with the remaining modifications.`; | ||
| return header + instructions + footer; | ||
| } | ||
| function summarizeChangeModeEdits(edits, isPartialView) { | ||
| const fileGroups = /* @__PURE__ */ new Map(); | ||
| for (const edit of edits) fileGroups.set(edit.filename, (fileGroups.get(edit.filename) || 0) + 1); | ||
| const summary = Array.from(fileGroups.entries()).map(([file, count]) => `- ${file}: ${count} edit${count === 1 ? "" : "s"}`).join("\n"); | ||
| return `${isPartialView ? `ChangeMode Summary (Complete analysis across all chunks):` : `ChangeMode Summary:`} | ||
| Total edits: ${edits.length}${isPartialView ? " (across all chunks)" : ""} | ||
| Files affected: ${fileGroups.size} | ||
| ${summary}`; | ||
| } | ||
| path.join(os.tmpdir(), "gemini-mcp-chunks"); | ||
| //#endregion | ||
| //#region ../shared/dist/shellPath.js | ||
| const IS_WINDOWS$1 = process.platform === "win32"; | ||
| const SHELL_PATH_ENV_VAR = "ASK_LLM_PATH"; | ||
| let cachedPath = null; | ||
| function extractShellPath() { | ||
| if (IS_WINDOWS$1) return null; | ||
| try { | ||
| const match = execFileSync(process.env.SHELL || "/bin/zsh", ["-ilc", "echo \"___PATH___$PATH___END___\""], { | ||
| encoding: "utf8", | ||
| stdio: [ | ||
| "ignore", | ||
| "pipe", | ||
| "ignore" | ||
| ], | ||
| timeout: 5e3 | ||
| }).match(/___PATH___(.*)___END___/); | ||
| if (match?.[1]) return match[1].trim(); | ||
| } catch { | ||
| Logger.debug("Failed to extract PATH from login shell"); | ||
| } | ||
| return null; | ||
| } | ||
| function findNvmNodePath() { | ||
| const nvmDir = join(homedir(), ".nvm", "versions", "node"); | ||
| if (!existsSync(nvmDir)) return null; | ||
| try { | ||
| const versions = readdirSync(nvmDir).filter((v) => { | ||
| return parseInt(v.replace("v", "").split(".")[0], 10) >= 20; | ||
| }).sort((a, b) => b.localeCompare(a, void 0, { numeric: true })); | ||
| if (versions.length > 0) { | ||
| const binDir = join(nvmDir, versions[0], "bin"); | ||
| if (existsSync(binDir)) return binDir; | ||
| } | ||
| } catch { | ||
| Logger.debug("Failed to scan nvm versions"); | ||
| } | ||
| return null; | ||
| } | ||
| function buildAugmentedPath() { | ||
| const currentPath = process.env.PATH || ""; | ||
| const home = homedir(); | ||
| const candidates = []; | ||
| const nvmBin = findNvmNodePath(); | ||
| if (nvmBin) candidates.push(nvmBin); | ||
| for (const dir of [ | ||
| join(home, ".volta", "bin"), | ||
| join(home, ".local", "share", "fnm"), | ||
| "/opt/homebrew/bin", | ||
| "/usr/local/bin" | ||
| ]) if (existsSync(dir)) candidates.push(dir); | ||
| if (candidates.length === 0) return currentPath; | ||
| return [...candidates, ...currentPath.split(delimiter)].join(delimiter); | ||
| } | ||
| function resolveShellPath() { | ||
| if (cachedPath !== null) return cachedPath; | ||
| const envOverride = process.env[SHELL_PATH_ENV_VAR]; | ||
| if (envOverride) { | ||
| Logger.debug(`Using ${SHELL_PATH_ENV_VAR} override`); | ||
| cachedPath = envOverride; | ||
| return cachedPath; | ||
| } | ||
| if (IS_WINDOWS$1) { | ||
| cachedPath = process.env.PATH || ""; | ||
| return cachedPath; | ||
| } | ||
| const shellPath = extractShellPath(); | ||
| if (shellPath) { | ||
| Logger.debug("Using PATH from login shell"); | ||
| cachedPath = shellPath; | ||
| return cachedPath; | ||
| } | ||
| Logger.debug("Login shell PATH extraction failed, using heuristic fallback"); | ||
| cachedPath = buildAugmentedPath(); | ||
| return cachedPath; | ||
| } | ||
| function getSpawnEnv() { | ||
| return { | ||
| ...process.env, | ||
| PATH: resolveShellPath() | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/commandExecutor.js | ||
| const IS_WINDOWS = process.platform === "win32"; | ||
| const REDACTED_COMMAND_ARGUMENT = "<redacted>"; | ||
| const QUOTA_PASSTHROUGH_PATTERNS = [ | ||
| "RESOURCE_EXHAUSTED", | ||
| "TerminalQuotaError", | ||
| "exhausted your capacity", | ||
| "rate_limit_exceeded", | ||
| "quota_exceeded", | ||
| "insufficient_quota", | ||
| "usage limit" | ||
| ]; | ||
| function sanitizeErrorForLLM(stderr, command) { | ||
| if (stderr.includes("Invalid regular expression flags") && stderr.includes("Node.js v")) return `${command} CLI requires Node.js v20+ but is running on ${stderr.match(/Node\.js (v[\d.]+)/)?.[1] ?? "unknown"}. The user should update their Node version or set ASK_LLM_PATH in their MCP config to point to a Node v20+ installation.`; | ||
| if (stderr.includes("command not found") || stderr.includes(`spawn ${command} ENOENT`)) return `${command} CLI not found on PATH. Ensure it is installed and accessible. Run "which ${command}" in a terminal to verify.`; | ||
| if (stderr.includes("EACCES") || stderr.includes("Permission denied")) return `Permission denied when running ${command} CLI. Check file permissions and try running with appropriate access.`; | ||
| const lower = stderr.toLowerCase(); | ||
| const matchedQuotaPattern = QUOTA_PASSTHROUGH_PATTERNS.find((p) => lower.includes(p.toLowerCase())); | ||
| if (matchedQuotaPattern) { | ||
| if (stderr.length <= 500) return stderr; | ||
| const idx = lower.indexOf(matchedQuotaPattern.toLowerCase()); | ||
| const start = Math.max(0, idx - 100); | ||
| const end = Math.min(start + 500, stderr.length); | ||
| const head = start > 0 ? "... (truncated) " : ""; | ||
| const tail = end < stderr.length ? "... (truncated)" : ""; | ||
| return `${head}${stderr.slice(start, end)}${tail}`; | ||
| } | ||
| const preview = stderr.split("\n").filter((l) => l.trim().length > 0).slice(0, 3).join("\n"); | ||
| if (preview.length > 0 && preview.length < 500) return preview; | ||
| return stderr.length > 500 ? `${stderr.slice(0, 500)}... (truncated)` : stderr; | ||
| } | ||
| function parseTimeoutEnv(envVal) { | ||
| if (!envVal) return void 0; | ||
| const parsed = Number(envVal); | ||
| return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0; | ||
| } | ||
| function getTimeoutMs() { | ||
| return parseTimeoutEnv(process.env[EXECUTION.TIMEOUT_ENV_VAR]) ?? EXECUTION.DEFAULT_TIMEOUT_MS; | ||
| } | ||
| function resolveTimeoutMs(providerEnvVar, fallbackDefault) { | ||
| const providerVal = parseTimeoutEnv(process.env[providerEnvVar]); | ||
| if (providerVal !== void 0) return providerVal; | ||
| const globalVal = parseTimeoutEnv(process.env[EXECUTION.TIMEOUT_ENV_VAR]); | ||
| if (globalVal !== void 0) return globalVal; | ||
| return fallbackDefault; | ||
| } | ||
| function quoteArgsForWindows(args) { | ||
| return args.map((a) => { | ||
| if (a.includes(" ") || a.includes("\"") || a.includes("&") || a.includes("|") || a.includes("^")) return `"${a.replace(/"/g, "\\\"")}"`; | ||
| return a; | ||
| }); | ||
| } | ||
| function argsForLogging(args, options) { | ||
| if (!options) return args; | ||
| const sensitiveValues = new Set(options.sensitiveValues); | ||
| return args.map((arg) => sensitiveValues.has(arg) ? REDACTED_COMMAND_ARGUMENT : arg); | ||
| } | ||
| async function executeCommand(command, args, onProgress, onStderr, stdinPayload, timeoutMs, commandLogging) { | ||
| return new Promise((resolve, reject) => { | ||
| const commandId = Logger.commandExecution(command, argsForLogging(args, commandLogging)); | ||
| const childProcess = spawn(command, IS_WINDOWS ? quoteArgsForWindows(args) : args, { | ||
| env: getSpawnEnv(), | ||
| shell: IS_WINDOWS, | ||
| stdio: [ | ||
| "pipe", | ||
| "pipe", | ||
| "pipe" | ||
| ] | ||
| }); | ||
| childProcess.stdin.on("error", () => {}); | ||
| if (stdinPayload !== void 0 && stdinPayload.length > 0) childProcess.stdin.write(stdinPayload); | ||
| childProcess.stdin.end(); | ||
| const stdoutChunks = []; | ||
| const stderrChunks = []; | ||
| let isResolved = false; | ||
| let killTimer; | ||
| const effectiveTimeoutMs = timeoutMs ?? getTimeoutMs(); | ||
| const timer = setTimeout(() => { | ||
| if (isResolved) return; | ||
| isResolved = true; | ||
| Logger.warn(`[cmd:${commandId}] Timeout after ${effectiveTimeoutMs}ms, sending SIGTERM`); | ||
| childProcess.kill("SIGTERM"); | ||
| killTimer = setTimeout(() => { | ||
| try { | ||
| childProcess.kill("SIGKILL"); | ||
| } catch {} | ||
| }, 5e3); | ||
| killTimer.unref?.(); | ||
| const timeoutSec = Math.round(effectiveTimeoutMs / 1e3); | ||
| reject(/* @__PURE__ */ new Error(`Command timed out after ${timeoutSec}s. The LLM provider took too long to respond. Try a shorter prompt or increase the timeout via the provider env var (ASK_CODEX_TIMEOUT_MS / ASK_CLAUDE_TIMEOUT_MS / ASK_GEMINI_TIMEOUT_MS) or the global ${EXECUTION.TIMEOUT_ENV_VAR} (current: ${effectiveTimeoutMs}ms).`)); | ||
| }, effectiveTimeoutMs); | ||
| childProcess.stdout.on("data", (data) => { | ||
| stdoutChunks.push(data); | ||
| if (onProgress) onProgress(data.toString()); | ||
| }); | ||
| childProcess.stderr.on("data", (data) => { | ||
| stderrChunks.push(data); | ||
| if (onStderr) onStderr(data.toString()); | ||
| }); | ||
| childProcess.on("error", (error) => { | ||
| if (killTimer) clearTimeout(killTimer); | ||
| if (!isResolved) { | ||
| isResolved = true; | ||
| clearTimeout(timer); | ||
| Logger.error(`Process error:`, error); | ||
| reject(/* @__PURE__ */ new Error(`Failed to spawn command: ${error.message}`)); | ||
| } | ||
| }); | ||
| childProcess.on("close", (code) => { | ||
| if (killTimer) clearTimeout(killTimer); | ||
| if (!isResolved) { | ||
| isResolved = true; | ||
| clearTimeout(timer); | ||
| const stdout = Buffer.concat(stdoutChunks).toString(); | ||
| if (code === 0) { | ||
| Logger.commandComplete(commandId, code, stdout.length); | ||
| resolve(stdout.trim()); | ||
| } else { | ||
| Logger.commandComplete(commandId, code); | ||
| Logger.error(`Failed with exit code ${code}`); | ||
| const userMessage = sanitizeErrorForLLM([Buffer.concat(stderrChunks).toString().trim(), stdout.trim()].filter(Boolean).join("\n") || "Unknown error", command); | ||
| reject(new Error(userMessage)); | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| promisify(execFile); | ||
| //#endregion | ||
| //#region ../shared/dist/pathValidation.js | ||
| /** | ||
| * Element schema for includeDirs-style tool parameters. Directories handed to | ||
| * provider CLIs (`--include-directories`, `--add-dir`) must stay inside the | ||
| * workspace: traversal, absolute, and home-relative paths would widen what the | ||
| * external CLI can read far beyond the repo the user pointed it at. | ||
| */ | ||
| const relativeDirSchema = z.string().refine((dir) => !dir.includes("..") && !isAbsolute(dir) && !dir.startsWith("~"), { message: "Directory paths must be relative without '..' or '~'" }); | ||
| //#endregion | ||
| //#region ../shared/dist/machine.js | ||
| const providerFailureKindSchema = z.enum([ | ||
| "rate_limited", | ||
| "auth_failed", | ||
| "unavailable", | ||
| "timeout", | ||
| "schema_invalid", | ||
| "tool_unavailable" | ||
| ]); | ||
| const machineRoleSchema = z.enum([ | ||
| "brainstorm", | ||
| "review", | ||
| "verify" | ||
| ]); | ||
| const machineProviderSchema = z.enum([ | ||
| "codex", | ||
| "claude", | ||
| "antigravity" | ||
| ]); | ||
| const actorProviderSchema = z.enum(PROVIDERS); | ||
| const requestIdSchema = z.string().regex(/^[A-Za-z0-9._:-]{8,160}$/); | ||
| const nonBlankStringSchema = z.string().regex(/\S/, "Value must contain a non-whitespace character"); | ||
| const machineRelativeDirSchema = relativeDirSchema.max(1024).regex(/^(?!.*\.\.)(?!~)(?!\/)(?![A-Za-z]:[\\/])(?!\\).*$/, "Directory paths must be relative without '..' or '~'"); | ||
| z.object({ | ||
| schemaVersion: z.literal(1), | ||
| requestId: requestIdSchema, | ||
| role: machineRoleSchema, | ||
| provider: machineProviderSchema, | ||
| prompt: z.string().min(1).max(15e4), | ||
| model: nonBlankStringSchema.max(256).optional(), | ||
| readOnly: z.literal(true), | ||
| writerProvider: actorProviderSchema.optional(), | ||
| includeDirs: z.array(machineRelativeDirSchema).max(16).default([]) | ||
| }).strict().superRefine((value, ctx) => { | ||
| if (value.role === "review" && value.writerProvider === value.provider) ctx.addIssue({ | ||
| code: "custom", | ||
| message: "review provider must differ from writer" | ||
| }); | ||
| }); | ||
| const reviewFindingSchema = z.object({ | ||
| id: z.string().min(1), | ||
| severity: z.enum([ | ||
| "critical", | ||
| "high", | ||
| "medium", | ||
| "low" | ||
| ]), | ||
| confidence: z.number().int().min(0).max(100), | ||
| title: z.string().min(1), | ||
| evidence: z.string().min(1), | ||
| recommendation: z.string().min(1), | ||
| file: z.string().min(1).nullable(), | ||
| line: z.number().int().positive().nullable() | ||
| }).strict(); | ||
| const reviewPayloadSchema = z.object({ | ||
| summary: z.string(), | ||
| findings: z.array(reviewFindingSchema) | ||
| }).strict(); | ||
| const brainstormPayloadSchema = z.object({ | ||
| recommendation: z.string().min(1), | ||
| ideas: z.array(z.object({ | ||
| title: z.string().min(1), | ||
| rationale: z.string().min(1), | ||
| risks: z.array(z.string()) | ||
| }).strict()).min(1) | ||
| }).strict(); | ||
| const verificationPayloadSchema = z.object({ | ||
| verdict: z.enum([ | ||
| "verified", | ||
| "partial", | ||
| "failed" | ||
| ]), | ||
| claims: z.array(z.object({ | ||
| claim: z.string().min(1), | ||
| status: z.enum([ | ||
| "proven", | ||
| "disproven", | ||
| "unverifiable" | ||
| ]), | ||
| evidence: z.string().min(1) | ||
| }).strict()) | ||
| }).strict(); | ||
| const normalizedTokenUsageSchema = z.object({ | ||
| inputTokens: z.number().int().nonnegative(), | ||
| outputTokens: z.number().int().nonnegative(), | ||
| totalTokens: z.number().int().nonnegative() | ||
| }).strict(); | ||
| const fallbackSchema = z.discriminatedUnion("occurred", [z.object({ | ||
| occurred: z.literal(false), | ||
| requestedModel: nonBlankStringSchema.nullable(), | ||
| actualModel: nonBlankStringSchema.nullable() | ||
| }).strict(), z.object({ | ||
| occurred: z.literal(true), | ||
| requestedModel: nonBlankStringSchema, | ||
| actualModel: nonBlankStringSchema | ||
| }).strict()]).refine((value) => value.occurred || value.requestedModel === value.actualModel, { | ||
| message: "requested and actual models must match when fallback did not occur", | ||
| path: ["actualModel"] | ||
| }); | ||
| const sessionLocatorSchema = z.union([z.object({ | ||
| sessionId: nonBlankStringSchema, | ||
| transcriptPath: nonBlankStringSchema.nullable() | ||
| }).strict(), z.object({ | ||
| sessionId: nonBlankStringSchema.nullable(), | ||
| transcriptPath: nonBlankStringSchema | ||
| }).strict()]); | ||
| const quotaSignalSchema = z.discriminatedUnion("kind", [z.object({ | ||
| kind: z.literal("reported"), | ||
| usedPercent: z.number().min(0).max(100), | ||
| windowHours: z.number().positive() | ||
| }).strict(), z.object({ kind: z.literal("runtime_proxy_required") }).strict()]); | ||
| const normalizedProviderFailureSchema = z.object({ | ||
| kind: providerFailureKindSchema, | ||
| message: z.string().min(1) | ||
| }).strict(); | ||
| const resultEnvelopeShape = { | ||
| schemaVersion: z.literal(1), | ||
| requestId: requestIdSchema, | ||
| provider: machineProviderSchema, | ||
| actualModel: nonBlankStringSchema.nullable(), | ||
| rawResponseSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(), | ||
| durationMs: z.number().int().nonnegative(), | ||
| usage: normalizedTokenUsageSchema.nullable(), | ||
| fallback: fallbackSchema, | ||
| session: sessionLocatorSchema.nullable(), | ||
| quotaSignal: quotaSignalSchema | ||
| }; | ||
| const brainstormSuccessResultSchema = z.object({ | ||
| ...resultEnvelopeShape, | ||
| status: z.literal("success"), | ||
| role: z.literal("brainstorm"), | ||
| payload: brainstormPayloadSchema, | ||
| failure: z.null() | ||
| }).strict(); | ||
| const reviewSuccessResultSchema = z.object({ | ||
| ...resultEnvelopeShape, | ||
| status: z.literal("success"), | ||
| role: z.literal("review"), | ||
| payload: reviewPayloadSchema, | ||
| failure: z.null() | ||
| }).strict(); | ||
| const verificationSuccessResultSchema = z.object({ | ||
| ...resultEnvelopeShape, | ||
| status: z.literal("success"), | ||
| role: z.literal("verify"), | ||
| payload: verificationPayloadSchema, | ||
| failure: z.null() | ||
| }).strict(); | ||
| const machineSuccessResultSchema = z.discriminatedUnion("role", [ | ||
| brainstormSuccessResultSchema, | ||
| reviewSuccessResultSchema, | ||
| verificationSuccessResultSchema | ||
| ]).refine((value) => value.actualModel === value.fallback.actualModel, { | ||
| message: "result and fallback actual models must match", | ||
| path: ["fallback", "actualModel"] | ||
| }); | ||
| const machineFailureResultSchema = z.object({ | ||
| ...resultEnvelopeShape, | ||
| status: z.literal("failed"), | ||
| role: machineRoleSchema, | ||
| payload: z.null(), | ||
| failure: normalizedProviderFailureSchema | ||
| }).strict().refine((value) => value.actualModel === value.fallback.actualModel, { | ||
| message: "result and fallback actual models must match", | ||
| path: ["fallback", "actualModel"] | ||
| }); | ||
| z.discriminatedUnion("status", [machineSuccessResultSchema, machineFailureResultSchema]); | ||
| //#endregion | ||
| //#region ../shared/dist/progressTracker.js | ||
| async function sendProgressNotification(extra, progress, total, message) { | ||
| const progressToken = extra._meta?.progressToken; | ||
| if (!progressToken) return; | ||
| try { | ||
| const params = { | ||
| progressToken, | ||
| progress | ||
| }; | ||
| if (total !== void 0) params.total = total; | ||
| if (message) params.message = message; | ||
| await extra.sendNotification({ | ||
| method: PROTOCOL.NOTIFICATIONS.PROGRESS, | ||
| params | ||
| }); | ||
| } catch (error) { | ||
| Logger.error("Failed to send progress notification:", error); | ||
| } | ||
| } | ||
| function createProgressTracker(operationName, extra, messages) { | ||
| let active = true; | ||
| let latestOutput = ""; | ||
| let messageIndex = 0; | ||
| let progress = 0; | ||
| sendProgressNotification(extra, 0, void 0, `Starting ${operationName}`); | ||
| const interval = setInterval(async () => { | ||
| if (active) { | ||
| progress += 1; | ||
| const baseMessage = messages[messageIndex % messages.length]; | ||
| const outputPreview = latestOutput.slice(-150).trim(); | ||
| const msg = outputPreview ? `${baseMessage}\nOutput: ...${outputPreview}` : baseMessage; | ||
| await sendProgressNotification(extra, progress, void 0, msg); | ||
| messageIndex++; | ||
| } else clearInterval(interval); | ||
| }, PROTOCOL.KEEPALIVE_INTERVAL); | ||
| return { | ||
| interval, | ||
| async stop(success) { | ||
| active = false; | ||
| clearInterval(interval); | ||
| await sendProgressNotification(extra, 100, 100, success ? `${operationName} completed` : `${operationName} failed`); | ||
| }, | ||
| updateOutput(output) { | ||
| latestOutput = output; | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/registry.js | ||
| const toolRegistry = []; | ||
| async function executeTool(toolName, args, onProgress, onUsage) { | ||
| const tool = toolRegistry.find((t) => t.name === toolName); | ||
| if (!tool) throw new Error(`Unknown tool: ${toolName}`); | ||
| try { | ||
| const validatedArgs = tool.zodSchema.parse(args); | ||
| return tool.execute(validatedArgs, onProgress, onUsage); | ||
| } catch (error) { | ||
| if (error instanceof ZodError) { | ||
| const issues = error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", "); | ||
| throw new Error(`Invalid arguments for ${toolName}: ${issues}`); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| function getPromptMessage(toolName, args) { | ||
| if (!toolRegistry.find((t) => t.name === toolName)?.prompt) throw new Error(`No prompt defined for tool: ${toolName}`); | ||
| const paramStrings = []; | ||
| if (args.prompt) paramStrings.push(args.prompt); | ||
| Object.entries(args).forEach(([key, value]) => { | ||
| if (key !== "prompt" && value !== void 0 && value !== null && value !== "false") if (value === "true") paramStrings.push(`[${key}]`); | ||
| else paramStrings.push(`(${key}: ${value})`); | ||
| }); | ||
| return `Use the ${toolName} tool${paramStrings.length > 0 ? `: ${paramStrings.join(" ")}` : ""}`; | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/responseCache.js | ||
| const DEFAULT_TTL_MS = 1800 * 1e3; | ||
| const DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024; | ||
| const DEFAULT_MAX_ENTRIES = 100; | ||
| var ResponseCache = class { | ||
| cache = /* @__PURE__ */ new Map(); | ||
| ttlMs; | ||
| maxSizeBytes; | ||
| maxEntries; | ||
| totalSizeBytes = 0; | ||
| constructor(options) { | ||
| this.ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS; | ||
| this.maxSizeBytes = options?.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES; | ||
| this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES; | ||
| } | ||
| static buildKey(provider, prompt, model, extra) { | ||
| const raw = `${provider}:${model ?? "default"}:${extra ?? ""}:${prompt}`; | ||
| return createHash("sha256").update(raw).digest("hex").slice(0, 16); | ||
| } | ||
| get(key) { | ||
| const entry = this.cache.get(key); | ||
| if (!entry) return null; | ||
| if (Date.now() - entry.createdAt > this.ttlMs) { | ||
| this.delete(key); | ||
| Logger.debug(`Response cache expired: ${key}`); | ||
| return null; | ||
| } | ||
| entry.lastAccessedAt = Date.now(); | ||
| return entry.response; | ||
| } | ||
| set(key, response) { | ||
| if (this.cache.has(key)) this.delete(key); | ||
| const sizeBytes = Buffer.byteLength(response, "utf-8"); | ||
| if (sizeBytes > this.maxSizeBytes) { | ||
| Logger.debug(`Response too large to cache: ${sizeBytes} bytes`); | ||
| return; | ||
| } | ||
| while (this.totalSizeBytes + sizeBytes > this.maxSizeBytes || this.cache.size >= this.maxEntries) { | ||
| const lruKey = this.findLRU(); | ||
| if (!lruKey) break; | ||
| this.delete(lruKey); | ||
| } | ||
| this.cache.set(key, { | ||
| response, | ||
| createdAt: Date.now(), | ||
| lastAccessedAt: Date.now(), | ||
| sizeBytes | ||
| }); | ||
| this.totalSizeBytes += sizeBytes; | ||
| Logger.debug(`Response cached: ${key} (${sizeBytes} bytes, ${this.cache.size} entries, ${this.totalSizeBytes} total bytes)`); | ||
| } | ||
| get size() { | ||
| return this.cache.size; | ||
| } | ||
| get byteSize() { | ||
| return this.totalSizeBytes; | ||
| } | ||
| clear() { | ||
| this.cache.clear(); | ||
| this.totalSizeBytes = 0; | ||
| } | ||
| delete(key) { | ||
| const entry = this.cache.get(key); | ||
| if (entry) { | ||
| this.totalSizeBytes -= entry.sizeBytes; | ||
| this.cache.delete(key); | ||
| } | ||
| } | ||
| findLRU() { | ||
| let oldestKey = null; | ||
| let oldestTime = Infinity; | ||
| for (const [key, entry] of this.cache) if (entry.lastAccessedAt < oldestTime) { | ||
| oldestTime = entry.lastAccessedAt; | ||
| oldestKey = key; | ||
| } | ||
| return oldestKey; | ||
| } | ||
| }; | ||
| const responseCache = new ResponseCache(); | ||
| //#endregion | ||
| //#region ../shared/dist/usage.js | ||
| function emptyProviderSnapshot() { | ||
| return { | ||
| calls: 0, | ||
| inputTokens: 0, | ||
| outputTokens: 0, | ||
| cachedTokens: 0, | ||
| thinkingTokens: 0, | ||
| durationMs: 0, | ||
| fellBack: 0 | ||
| }; | ||
| } | ||
| function addToBucket(bucket, stats) { | ||
| bucket.calls += 1; | ||
| bucket.inputTokens += stats.inputTokens ?? 0; | ||
| bucket.outputTokens += stats.outputTokens ?? 0; | ||
| bucket.cachedTokens += stats.cachedTokens ?? 0; | ||
| bucket.thinkingTokens += stats.thinkingTokens ?? 0; | ||
| bucket.durationMs += stats.durationMs; | ||
| if (stats.fellBack) bucket.fellBack += 1; | ||
| } | ||
| function createSessionUsage() { | ||
| let totalCalls = 0; | ||
| let totalInputTokens = 0; | ||
| let totalOutputTokens = 0; | ||
| let totalCachedTokens = 0; | ||
| let totalThinkingTokens = 0; | ||
| let totalDurationMs = 0; | ||
| let fallbackCount = 0; | ||
| const byProvider = {}; | ||
| const byModel = {}; | ||
| return { | ||
| record(stats) { | ||
| totalCalls += 1; | ||
| totalInputTokens += stats.inputTokens ?? 0; | ||
| totalOutputTokens += stats.outputTokens ?? 0; | ||
| totalCachedTokens += stats.cachedTokens ?? 0; | ||
| totalThinkingTokens += stats.thinkingTokens ?? 0; | ||
| totalDurationMs += stats.durationMs; | ||
| if (stats.fellBack) fallbackCount += 1; | ||
| if (!byProvider[stats.provider]) byProvider[stats.provider] = emptyProviderSnapshot(); | ||
| addToBucket(byProvider[stats.provider], stats); | ||
| if (!byModel[stats.model]) byModel[stats.model] = emptyProviderSnapshot(); | ||
| addToBucket(byModel[stats.model], stats); | ||
| }, | ||
| snapshot() { | ||
| return { | ||
| totalCalls, | ||
| totalInputTokens, | ||
| totalOutputTokens, | ||
| totalCachedTokens, | ||
| totalThinkingTokens, | ||
| totalDurationMs, | ||
| fallbackCount, | ||
| byProvider: structuredClone(byProvider), | ||
| byModel: structuredClone(byModel) | ||
| }; | ||
| }, | ||
| reset() { | ||
| totalCalls = 0; | ||
| totalInputTokens = 0; | ||
| totalOutputTokens = 0; | ||
| totalCachedTokens = 0; | ||
| totalThinkingTokens = 0; | ||
| totalDurationMs = 0; | ||
| fallbackCount = 0; | ||
| for (const key of Object.keys(byProvider)) delete byProvider[key]; | ||
| for (const key of Object.keys(byModel)) delete byModel[key]; | ||
| } | ||
| }; | ||
| } | ||
| function formatSessionUsage(snapshot) { | ||
| if (snapshot.totalCalls === 0) return "No LLM calls recorded in this session yet."; | ||
| const lines = [ | ||
| "## Session Usage Summary", | ||
| "", | ||
| `Total calls: ${snapshot.totalCalls.toLocaleString()}`, | ||
| `Total input tokens: ${snapshot.totalInputTokens.toLocaleString()}`, | ||
| `Total output tokens: ${snapshot.totalOutputTokens.toLocaleString()}` | ||
| ]; | ||
| if (snapshot.totalThinkingTokens > 0) lines.push(`Total thinking tokens: ${snapshot.totalThinkingTokens.toLocaleString()}`); | ||
| if (snapshot.totalCachedTokens > 0) lines.push(`Total cached tokens: ${snapshot.totalCachedTokens.toLocaleString()}`); | ||
| lines.push(`Total wall time: ${(snapshot.totalDurationMs / 1e3).toFixed(1)}s`); | ||
| if (snapshot.fallbackCount > 0) lines.push(`Quota fallbacks triggered: ${snapshot.fallbackCount}`); | ||
| const providerEntries = Object.entries(snapshot.byProvider); | ||
| if (providerEntries.length > 0) { | ||
| lines.push("", "### By provider", ""); | ||
| for (const [provider, bucket] of providerEntries) lines.push(`- **${provider}** — ${bucket.calls} calls, ${bucket.inputTokens.toLocaleString()} in / ${bucket.outputTokens.toLocaleString()} out tokens, ${(bucket.durationMs / 1e3).toFixed(1)}s` + (bucket.fellBack > 0 ? `, ${bucket.fellBack} fallbacks` : "")); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| //#endregion | ||
| //#region ../shared/dist/serverFactory.js | ||
| const diagnosticCheckSchema = z.object({ | ||
| name: z.string(), | ||
| status: z.enum([ | ||
| "pass", | ||
| "warn", | ||
| "fail", | ||
| "skip" | ||
| ]), | ||
| message: z.string(), | ||
| fix: z.string().optional() | ||
| }); | ||
| const diagnosticProviderSchema = z.object({ | ||
| name: z.string(), | ||
| command: z.string(), | ||
| available: z.boolean(), | ||
| cliPath: z.string().optional(), | ||
| cliVersion: z.string().optional(), | ||
| error: z.string().optional() | ||
| }); | ||
| z.object({ | ||
| status: z.enum([ | ||
| "ok", | ||
| "warning", | ||
| "error" | ||
| ]), | ||
| generatedAt: z.string(), | ||
| environment: z.object({ | ||
| nodeVersion: z.string(), | ||
| nodeOk: z.boolean(), | ||
| platform: z.string(), | ||
| arch: z.string(), | ||
| resolvedPath: z.string(), | ||
| askLlmPath: z.string().optional(), | ||
| timeoutMs: z.number(), | ||
| codexTimeoutMs: z.number(), | ||
| claudeTimeoutMs: z.number(), | ||
| geminiTimeoutMs: z.number() | ||
| }), | ||
| providers: z.array(diagnosticProviderSchema), | ||
| checks: z.array(diagnosticCheckSchema) | ||
| }); | ||
| const providerUsageBucketSchema = z.object({ | ||
| calls: z.number(), | ||
| inputTokens: z.number(), | ||
| outputTokens: z.number(), | ||
| cachedTokens: z.number(), | ||
| thinkingTokens: z.number(), | ||
| durationMs: z.number(), | ||
| fellBack: z.number() | ||
| }); | ||
| const sessionUsageSnapshotSchema = z.object({ | ||
| totalCalls: z.number(), | ||
| totalInputTokens: z.number(), | ||
| totalOutputTokens: z.number(), | ||
| totalCachedTokens: z.number(), | ||
| totalThinkingTokens: z.number(), | ||
| totalDurationMs: z.number(), | ||
| fallbackCount: z.number(), | ||
| byProvider: z.record(z.string(), providerUsageBucketSchema), | ||
| byModel: z.record(z.string(), providerUsageBucketSchema) | ||
| }); | ||
| function createUsageStatsTool(sessionUsage) { | ||
| return { | ||
| name: "get-usage-stats", | ||
| description: "Get the current MCP server's session usage stats: total LLM calls, token totals (input/output/thinking/cached), wall time, and breakdowns per provider and per model. No data leaves your machine — counts are tracked in-memory for the lifetime of the server process. Returns both human-readable markdown and a structured JSON snapshot via outputSchema.", | ||
| zodSchema: z.object({}), | ||
| outputSchema: sessionUsageSnapshotSchema, | ||
| annotations: { | ||
| title: "Get Usage Stats", | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| }, | ||
| category: "utility", | ||
| execute: async () => { | ||
| const snapshot = sessionUsage.snapshot(); | ||
| return { | ||
| text: formatSessionUsage(snapshot), | ||
| structuredContent: snapshot | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| function registerSessionUsageResource(server, sessionUsage) { | ||
| server.registerResource("session-usage", "usage://current-session", { | ||
| title: "Current Session Usage", | ||
| description: "Live JSON snapshot of token usage and call statistics for this MCP server session. Re-read at any time for the current totals.", | ||
| mimeType: "application/json" | ||
| }, async (uri) => ({ contents: [{ | ||
| uri: uri.href, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(sessionUsage.snapshot(), null, 2) | ||
| }] })); | ||
| } | ||
| function registerTools({ server, tools, executeTool, getPromptMessage, progressMessages, sessionUsage }) { | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| for (const tool of tools) { | ||
| if (seen.has(tool.name)) throw new Error(`Duplicate tool name "${tool.name}" — tool names must be unique within a server`); | ||
| seen.add(tool.name); | ||
| const shape = tool.zodSchema.shape; | ||
| const outputShape = tool.outputSchema ? tool.outputSchema.shape : void 0; | ||
| server.registerTool(tool.name, { | ||
| description: tool.description, | ||
| inputSchema: shape, | ||
| ...outputShape ? { outputSchema: outputShape } : {}, | ||
| annotations: tool.annotations | ||
| }, async (args, extra) => { | ||
| const toolName = tool.name; | ||
| const handle = createProgressTracker(toolName, extra, progressMessages(toolName)); | ||
| try { | ||
| const toolArgs = args; | ||
| Logger.toolInvocation(toolName, args); | ||
| const result = await executeTool(toolName, toolArgs, (newOutput) => { | ||
| handle.updateOutput(newOutput); | ||
| }, sessionUsage ? (stats) => sessionUsage.record(stats) : void 0); | ||
| await handle.stop(true); | ||
| if (typeof result === "string") return { | ||
| content: [{ | ||
| type: "text", | ||
| text: result | ||
| }], | ||
| isError: false | ||
| }; | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: result.text | ||
| }], | ||
| structuredContent: result.structuredContent, | ||
| isError: false | ||
| }; | ||
| } catch (error) { | ||
| await handle.stop(false); | ||
| Logger.error(`Error in tool '${toolName}':`, error); | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: `Error executing ${toolName}: ${error instanceof Error ? error.message : String(error)}` | ||
| }], | ||
| isError: true | ||
| }; | ||
| } | ||
| }); | ||
| } | ||
| for (const tool of tools) { | ||
| if (!tool.prompt) continue; | ||
| server.registerPrompt(tool.name, { description: tool.prompt.description }, async (args) => { | ||
| return { messages: [{ | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text: getPromptMessage(tool.name, args) | ||
| } | ||
| }] }; | ||
| }); | ||
| } | ||
| } | ||
| path.join(os.tmpdir(), "ask-llm-sessions"); | ||
| //#endregion | ||
| //#region src/constants.ts | ||
| const ERROR_MESSAGES = { | ||
| QUOTA_SIGNALS: [ | ||
| "rate_limit_exceeded", | ||
| "quota_exceeded", | ||
| "429", | ||
| "insufficient_quota", | ||
| "out of credits", | ||
| "spend cap", | ||
| "usage limit" | ||
| ], | ||
| ARCHIVED_SESSION_SIGNALS: [ | ||
| "archived_sessions", | ||
| "archived session", | ||
| "session is archived" | ||
| ], | ||
| MODEL_UNAVAILABLE_SIGNALS: ["is not supported when using codex with a chatgpt"], | ||
| NO_PROMPT_PROVIDED: "Please provide a prompt for analysis. Ask general questions or describe the code you want reviewed.", | ||
| TOOL_NOT_FOUND: "not found in registry" | ||
| }; | ||
| const STATUS_MESSAGES = { | ||
| QUOTA_SWITCHING: "Codex quota exceeded, switching to fallback model...", | ||
| FALLBACK_RETRY: "Retrying with fallback model...", | ||
| FALLBACK_SUCCESS: "Fallback model completed successfully", | ||
| CODEX_RESPONSE: "Codex response:" | ||
| }; | ||
| const FACTORY_DEFAULT_MODEL = "gpt-5.6-sol"; | ||
| const CODEX_REASONING_EFFORTS = [ | ||
| "low", | ||
| "medium", | ||
| "high", | ||
| "xhigh", | ||
| "max" | ||
| ]; | ||
| const FACTORY_DEFAULT_REASONING_EFFORT = "medium"; | ||
| function isCodexReasoningEffort(value) { | ||
| return value !== void 0 && CODEX_REASONING_EFFORTS.includes(value); | ||
| } | ||
| const configuredReasoningEffort = process.env.ASK_CODEX_REASONING_EFFORT; | ||
| const DEFAULT_REASONING_EFFORT = isCodexReasoningEffort(configuredReasoningEffort) ? configuredReasoningEffort : FACTORY_DEFAULT_REASONING_EFFORT; | ||
| const MODELS = { | ||
| DEFAULT: process.env.ASK_CODEX_MODEL || "gpt-5.6-sol", | ||
| PREFERRED: process.env.ASK_CODEX_PREFERRED_MODEL || "gpt-5.6-sol", | ||
| FALLBACK: process.env.ASK_CODEX_FALLBACK_MODEL || "gpt-5.6-terra" | ||
| }; | ||
| const CLI = { | ||
| COMMANDS: { | ||
| CODEX: "codex", | ||
| EXEC: "exec", | ||
| RESUME: "resume" | ||
| }, | ||
| FLAGS: { | ||
| MODEL: "-m", | ||
| CONFIG: "-c", | ||
| SKIP_GIT: "--skip-git-repo-check", | ||
| EPHEMERAL: "--ephemeral", | ||
| JSON: "--json", | ||
| SANDBOX: "--sandbox", | ||
| SANDBOX_WORKSPACE_WRITE: "workspace-write", | ||
| IGNORE_USER_CONFIG: "--ignore-user-config", | ||
| IGNORE_RULES: "--ignore-rules", | ||
| ADD_DIR: "--add-dir", | ||
| OUTPUT_SCHEMA: "--output-schema", | ||
| SANDBOX_READ_ONLY: "read-only" | ||
| } | ||
| }; | ||
| const CODEX_EDIT_SCHEMA = { | ||
| type: "object", | ||
| additionalProperties: false, | ||
| properties: { edits: { | ||
| type: "array", | ||
| items: { | ||
| type: "object", | ||
| additionalProperties: false, | ||
| properties: { | ||
| file: { | ||
| type: "string", | ||
| description: "Repo-relative path to an existing file" | ||
| }, | ||
| startLine: { | ||
| type: ["integer", "null"], | ||
| description: "1-based line where oldCode begins (or null)" | ||
| }, | ||
| oldCode: { | ||
| type: "string", | ||
| description: "Exact existing text to replace (must match the file verbatim)" | ||
| }, | ||
| newCode: { | ||
| type: "string", | ||
| description: "Replacement text" | ||
| }, | ||
| description: { | ||
| type: ["string", "null"], | ||
| description: "One-line rationale (or null)" | ||
| } | ||
| }, | ||
| required: [ | ||
| "file", | ||
| "startLine", | ||
| "oldCode", | ||
| "newCode", | ||
| "description" | ||
| ] | ||
| } | ||
| } }, | ||
| required: ["edits"] | ||
| }; | ||
| //#endregion | ||
| //#region src/utils/codexDoctor.ts | ||
| const execFileAsync = promisify(execFile); | ||
| const DOCTOR_PROBE_TIMEOUT_MS = 5e3; | ||
| const DOCTOR_MAX_BUFFER = 1024 * 1024 * 4; | ||
| function mapCheckStatus(status) { | ||
| switch (String(status).toLowerCase()) { | ||
| case "ok": | ||
| case "pass": return "pass"; | ||
| case "error": | ||
| case "fail": | ||
| case "failed": return "fail"; | ||
| case "skip": | ||
| case "skipped": return "skip"; | ||
| default: return "warn"; | ||
| } | ||
| } | ||
| function mapOverallStatus(status) { | ||
| switch (String(status).toLowerCase()) { | ||
| case "ok": | ||
| case "pass": return "ok"; | ||
| case "error": | ||
| case "fail": return "error"; | ||
| default: return "warning"; | ||
| } | ||
| } | ||
| /** | ||
| * Parse `codex doctor --json` stdout into a provider-agnostic enrichment. | ||
| * Returns `undefined` for anything that is not a recognizable doctor report | ||
| * (non-JSON, missing `overallStatus`/`checks`) so callers degrade silently. | ||
| */ | ||
| function parseCodexDoctorJson(stdout) { | ||
| let raw; | ||
| try { | ||
| raw = JSON.parse(stdout); | ||
| } catch { | ||
| return; | ||
| } | ||
| if (!raw || typeof raw !== "object") return void 0; | ||
| const report = raw; | ||
| if (typeof report.overallStatus !== "string") return void 0; | ||
| if (!report.checks || typeof report.checks !== "object") return void 0; | ||
| const checks = []; | ||
| for (const [key, value] of Object.entries(report.checks)) { | ||
| if (!value || typeof value !== "object") continue; | ||
| const check = value; | ||
| const remediation = typeof check.remediation === "string" && check.remediation.length > 0 ? check.remediation : void 0; | ||
| checks.push({ | ||
| name: typeof check.id === "string" ? check.id : key, | ||
| status: mapCheckStatus(check.status), | ||
| summary: typeof check.summary === "string" ? check.summary : "", | ||
| ...remediation ? { remediation } : {} | ||
| }); | ||
| } | ||
| return { | ||
| heading: "codex doctor", | ||
| overall: mapOverallStatus(report.overallStatus), | ||
| checks | ||
| }; | ||
| } | ||
| async function runCodexDoctorJson(command, pathEnv) { | ||
| const { stdout } = await execFileAsync(command, ["doctor", "--json"], { | ||
| env: { | ||
| ...process.env, | ||
| PATH: pathEnv | ||
| }, | ||
| timeout: DOCTOR_PROBE_TIMEOUT_MS, | ||
| maxBuffer: DOCTOR_MAX_BUFFER | ||
| }); | ||
| return stdout; | ||
| } | ||
| /** | ||
| * Enrich the codex provider with a compact `codex doctor` health summary. | ||
| * Capability-probed: any failure (codex too old to know `--json`, timeout, | ||
| * unparseable output) yields `undefined`, leaving the doctor report unchanged. | ||
| * The `run` parameter is injectable for testing. | ||
| */ | ||
| async function enrichCodexDoctor(ctx, run = runCodexDoctorJson) { | ||
| try { | ||
| return parseCodexDoctorJson(await run(ctx.command, ctx.pathEnv)); | ||
| } catch (err) { | ||
| const salvaged = err.stdout; | ||
| return typeof salvaged === "string" ? parseCodexDoctorJson(salvaged) : void 0; | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/utils/codexExecutor.ts | ||
| function buildUsageStats(usage, model, durationMs, fellBack) { | ||
| return { | ||
| provider: "codex", | ||
| model, | ||
| inputTokens: usage?.input_tokens, | ||
| outputTokens: usage?.output_tokens, | ||
| cachedTokens: usage?.cached_input_tokens, | ||
| thinkingTokens: usage?.reasoning_output_tokens, | ||
| durationMs, | ||
| fellBack | ||
| }; | ||
| } | ||
| function formatStats(usage) { | ||
| if (!usage) return ""; | ||
| const parts = []; | ||
| if (usage.input_tokens != null) parts.push(`${usage.input_tokens.toLocaleString()} input tokens`); | ||
| if (usage.output_tokens != null) parts.push(`${usage.output_tokens.toLocaleString()} output tokens`); | ||
| if (usage.reasoning_output_tokens != null && usage.reasoning_output_tokens > 0) parts.push(`${usage.reasoning_output_tokens.toLocaleString()} thinking tokens`); | ||
| if (usage.cached_input_tokens != null && usage.cached_input_tokens > 0) parts.push(`${usage.cached_input_tokens.toLocaleString()} cached`); | ||
| return parts.length > 0 ? `\n\n[Codex stats: ${parts.join(", ")}]` : ""; | ||
| } | ||
| function parseCodexJsonlOutput(raw, model, durationMs, fellBack) { | ||
| const lines = raw.split("\n").filter((l) => l.trim().length > 0); | ||
| let lastAgentMessage; | ||
| let threadId; | ||
| let usage; | ||
| let lastError; | ||
| let sawJsonlEvent = false; | ||
| for (const line of lines) { | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(line); | ||
| } catch { | ||
| continue; | ||
| } | ||
| if (parsed && typeof parsed === "object" && typeof parsed.type === "string") sawJsonlEvent = true; | ||
| if (parsed.type === "thread.started") { | ||
| const thread = parsed; | ||
| if (thread.thread_id) threadId = thread.thread_id; | ||
| } | ||
| if (parsed.type === "item.completed") { | ||
| const item = parsed.item; | ||
| if (item?.type === "agent_message" && typeof item.text === "string" && item.text.length > 0) lastAgentMessage = item.text; | ||
| } | ||
| if (parsed.type === "turn.completed") usage = parsed.usage; | ||
| if (parsed.type === "turn.failed") lastError = parsed.error?.message ?? JSON.stringify(parsed); | ||
| if (parsed.type === "error") lastError = parsed.message ?? JSON.stringify(parsed); | ||
| } | ||
| if (lastError && !lastAgentMessage) throw new Error(`Codex error event: ${lastError}`); | ||
| if (!lastAgentMessage) { | ||
| if (sawJsonlEvent) { | ||
| const truncated = raw.length > EXECUTION.ERROR_TRUNCATE_LENGTH ? `${raw.slice(0, EXECUTION.ERROR_TRUNCATE_LENGTH)}…` : raw; | ||
| throw new Error(`Codex completed without an agent message${threadId ? ` (thread ${threadId})` : ""}. The run ended before producing a response — retry, or resume the thread via sessionId. Raw JSONL (truncated):\n${truncated}`); | ||
| } | ||
| Logger.debug("No parseable Codex JSONL events found, using raw text as the response"); | ||
| return { | ||
| response: raw, | ||
| threadId, | ||
| usage: buildUsageStats(usage, model, durationMs, fellBack) | ||
| }; | ||
| } | ||
| return { | ||
| response: lastAgentMessage + formatStats(usage), | ||
| threadId, | ||
| usage: buildUsageStats(usage, model, durationMs, fellBack) | ||
| }; | ||
| } | ||
| function isQuotaError(error) { | ||
| const msg = (error instanceof Error ? error.message : String(error)).toLowerCase(); | ||
| return ERROR_MESSAGES.QUOTA_SIGNALS.some((signal) => msg.includes(signal)); | ||
| } | ||
| function isArchivedSessionError(error) { | ||
| const msg = (error instanceof Error ? error.message : String(error)).toLowerCase(); | ||
| return ERROR_MESSAGES.ARCHIVED_SESSION_SIGNALS.some((signal) => msg.includes(signal)); | ||
| } | ||
| function isModelUnavailableError(error) { | ||
| const msg = (error instanceof Error ? error.message : String(error)).toLowerCase(); | ||
| return ERROR_MESSAGES.MODEL_UNAVAILABLE_SIGNALS.some((signal) => msg.includes(signal)); | ||
| } | ||
| function extractFirstJsonObject(text) { | ||
| const start = text.indexOf("{"); | ||
| if (start === -1) return null; | ||
| let depth = 0; | ||
| let inStr = false; | ||
| let esc = false; | ||
| for (let i = start; i < text.length; i++) { | ||
| const ch = text[i]; | ||
| if (inStr) { | ||
| if (esc) esc = false; | ||
| else if (ch === "\\") esc = true; | ||
| else if (ch === "\"") inStr = false; | ||
| } else if (ch === "\"") inStr = true; | ||
| else if (ch === "{") depth++; | ||
| else if (ch === "}") { | ||
| depth--; | ||
| if (depth === 0) return text.slice(start, i + 1); | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function parseCodexEdits(rawJson) { | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(extractFirstJsonObject(rawJson) ?? rawJson); | ||
| } catch { | ||
| return []; | ||
| } | ||
| if (!parsed || !Array.isArray(parsed.edits)) return []; | ||
| const edits = []; | ||
| for (const item of parsed.edits) { | ||
| if (!item || typeof item.file !== "string" || typeof item.oldCode !== "string" || typeof item.newCode !== "string") continue; | ||
| const oldCode = item.oldCode; | ||
| const newCode = item.newCode; | ||
| const startLine = typeof item.startLine === "number" && item.startLine > 0 ? item.startLine : 1; | ||
| const oldLineCount = oldCode === "" ? 0 : oldCode.split("\n").length; | ||
| const newLineCount = newCode === "" ? 0 : newCode.split("\n").length; | ||
| edits.push({ | ||
| filename: item.file, | ||
| oldStartLine: startLine, | ||
| oldEndLine: startLine + (oldLineCount > 0 ? oldLineCount - 1 : 0), | ||
| oldCode, | ||
| newStartLine: startLine, | ||
| newEndLine: startLine + (newLineCount > 0 ? newLineCount - 1 : 0), | ||
| newCode | ||
| }); | ||
| } | ||
| return edits; | ||
| } | ||
| function processCodexEditOutput(rawJson) { | ||
| const edits = parseCodexEdits(rawJson); | ||
| if (edits.length === 0) return "Codex proposed no edits for this request."; | ||
| const validation = validateChangeModeEdits(edits); | ||
| if (!validation.valid) return `Edit validation failed:\n${validation.errors.join("\n")}`; | ||
| let result = formatChangeModeResponse(edits); | ||
| if (edits.length > 5) result = `${summarizeChangeModeEdits(edits)}\n\n${result}`; | ||
| return result; | ||
| } | ||
| function buildArgs(prompt, model, sessionId, useStdin, includeDirs, sandboxMode = CLI.FLAGS.SANDBOX_READ_ONLY, schemaPath, reasoningEffort = DEFAULT_REASONING_EFFORT) { | ||
| const base = [CLI.COMMANDS.EXEC]; | ||
| if (sessionId) base.push(CLI.COMMANDS.RESUME); | ||
| base.push(CLI.FLAGS.SKIP_GIT); | ||
| if (!sessionId) base.push(CLI.FLAGS.EPHEMERAL); | ||
| if (process.env.ASK_CODEX_LOAD_USER_CONFIG !== "1") base.push(CLI.FLAGS.IGNORE_USER_CONFIG, CLI.FLAGS.IGNORE_RULES); | ||
| base.push(CLI.FLAGS.SANDBOX, sandboxMode, CLI.FLAGS.CONFIG, `model_reasoning_effort="${reasoningEffort}"`, CLI.FLAGS.JSON, CLI.FLAGS.MODEL, model); | ||
| if (schemaPath) base.push(CLI.FLAGS.OUTPUT_SCHEMA, schemaPath); | ||
| if (includeDirs?.length) for (const dir of includeDirs) base.push(CLI.FLAGS.ADD_DIR, dir); | ||
| if (sessionId) base.push(sessionId); | ||
| if (!useStdin) base.push(prompt); | ||
| return base; | ||
| } | ||
| async function executeCodexCLI(options) { | ||
| const model = options.model || MODELS.DEFAULT; | ||
| const reasoningEffort = options.reasoningEffort || DEFAULT_REASONING_EFFORT; | ||
| const sessionId = options.sessionId; | ||
| const editMode = options.editMode === true; | ||
| const outputSchema = options.outputSchema ?? (editMode ? CODEX_EDIT_SCHEMA : void 0); | ||
| const sandboxMode = options.sandbox === "workspace-write" ? CLI.FLAGS.SANDBOX_WORKSPACE_WRITE : CLI.FLAGS.SANDBOX_READ_ONLY; | ||
| const wantsSession = sessionId !== void 0; | ||
| const preferredEligible = options.preferred === true && !options.model && !wantsSession && !editMode && MODELS.PREFERRED !== MODELS.DEFAULT; | ||
| const dirsPart = options.includeDirs?.length ? [...options.includeDirs].sort().join(":") : ""; | ||
| const extraContext = `effort=${reasoningEffort};edit=${editMode ? 1 : 0};sandbox=${sandboxMode};dirs=${dirsPart}`; | ||
| const cacheKey = wantsSession || preferredEligible || outputSchema ? null : ResponseCache.buildKey("codex", options.prompt, model, extraContext); | ||
| if (cacheKey) { | ||
| const cached = responseCache.get(cacheKey); | ||
| if (cached) { | ||
| Logger.debug("Response cache hit for codex"); | ||
| return { | ||
| response: cached, | ||
| threadId: void 0, | ||
| usage: void 0 | ||
| }; | ||
| } | ||
| } | ||
| let schemaPath; | ||
| try { | ||
| if (outputSchema) { | ||
| schemaPath = join(tmpdir(), `codex-output-schema-${process.pid}-${randomUUID()}.json`); | ||
| writeFileSync(schemaPath, JSON.stringify(outputSchema), { mode: 384 }); | ||
| } | ||
| const useStdin = outputSchema !== void 0 || options.prompt.length > EXECUTION.STDIN_THRESHOLD_BYTES; | ||
| const stdinPayload = useStdin ? options.prompt : void 0; | ||
| const args = buildArgs(options.prompt, model, sessionId, useStdin, options.includeDirs, sandboxMode, schemaPath, reasoningEffort); | ||
| const timeoutMs = resolveTimeoutMs(EXECUTION.CODEX_TIMEOUT_ENV_VAR, EXECUTION.DEFAULT_CODEX_TIMEOUT_MS); | ||
| let downgradedFromPreferred = false; | ||
| if (preferredEligible) { | ||
| const preferredArgs = buildArgs(options.prompt, MODELS.PREFERRED, void 0, useStdin, options.includeDirs, sandboxMode, schemaPath, reasoningEffort); | ||
| const preferredStartedAt = Date.now(); | ||
| try { | ||
| return parseCodexJsonlOutput(await executeCommand(CLI.COMMANDS.CODEX, preferredArgs, options.onProgress, void 0, stdinPayload, timeoutMs), MODELS.PREFERRED, Date.now() - preferredStartedAt, false); | ||
| } catch (preferredError) { | ||
| const reason = preferredError instanceof Error ? preferredError.message : String(preferredError); | ||
| Logger.warn(`Preferred Codex model ${MODELS.PREFERRED} unavailable (${reason}); falling back to ${MODELS.DEFAULT}.`); | ||
| downgradedFromPreferred = true; | ||
| } | ||
| } | ||
| const startedAt = Date.now(); | ||
| try { | ||
| const result = parseCodexJsonlOutput(await executeCommand(CLI.COMMANDS.CODEX, args, options.onProgress, void 0, stdinPayload, timeoutMs), model, Date.now() - startedAt, downgradedFromPreferred); | ||
| if (cacheKey) responseCache.set(cacheKey, result.response); | ||
| return result; | ||
| } catch (error) { | ||
| if (sessionId && isArchivedSessionError(error)) throw new Error(`Codex session ${sessionId} is archived. Run \`codex unarchive ${sessionId}\` to resume it, or omit sessionId to start a new thread.`); | ||
| if (isQuotaError(error) && model !== MODELS.FALLBACK) { | ||
| Logger.warn(`${STATUS_MESSAGES.QUOTA_SWITCHING} Falling back to ${MODELS.FALLBACK}.`); | ||
| Logger.debug(`Status: ${STATUS_MESSAGES.FALLBACK_RETRY}`); | ||
| const fallbackArgs = buildArgs(options.prompt, MODELS.FALLBACK, sessionId, useStdin, options.includeDirs, sandboxMode, schemaPath, reasoningEffort); | ||
| const fallbackStartedAt = Date.now(); | ||
| try { | ||
| const raw = await executeCommand(CLI.COMMANDS.CODEX, fallbackArgs, options.onProgress, void 0, stdinPayload, timeoutMs); | ||
| Logger.warn(`Successfully executed with ${MODELS.FALLBACK} fallback.`); | ||
| Logger.debug(`Status: ${STATUS_MESSAGES.FALLBACK_SUCCESS}`); | ||
| return parseCodexJsonlOutput(raw, MODELS.FALLBACK, Date.now() - fallbackStartedAt, true); | ||
| } catch (fallbackError) { | ||
| const fallbackMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); | ||
| if (isModelUnavailableError(fallbackError)) { | ||
| const remediation = process.env.ASK_CODEX_FALLBACK_MODEL ? "Set ASK_CODEX_FALLBACK_MODEL to a model your account supports, or unset it to use the default (gpt-5.6-terra)." : "Set ASK_CODEX_FALLBACK_MODEL to a model your account supports."; | ||
| throw new Error(`${MODELS.DEFAULT} quota exceeded and the fallback model "${MODELS.FALLBACK}" is not available for this Codex account type (${fallbackMsg}). ${remediation}`); | ||
| } | ||
| throw new Error(`${MODELS.DEFAULT} quota exceeded, ${MODELS.FALLBACK} fallback also failed: ${fallbackMsg}. Run \`codex doctor\` to inspect your Codex CLI installation.`); | ||
| } | ||
| } | ||
| throw error; | ||
| } | ||
| } finally { | ||
| if (schemaPath) try { | ||
| unlinkSync(schemaPath); | ||
| } catch {} | ||
| } | ||
| } | ||
| //#endregion | ||
| export { Logger as C, executeCommand as S, createSessionUsage as _, parseCodexJsonlOutput as a, toolRegistry as b, CODEX_REASONING_EFFORTS as c, FACTORY_DEFAULT_REASONING_EFFORT as d, MODELS as f, registerTools as g, registerSessionUsageResource as h, parseCodexEdits as i, ERROR_MESSAGES as l, createUsageStatsTool as m, isModelUnavailableError as n, processCodexEditOutput as o, STATUS_MESSAGES as p, isQuotaError as r, enrichCodexDoctor as s, executeCodexCLI as t, FACTORY_DEFAULT_MODEL as u, executeTool as v, askResponseSchema as w, relativeDirSchema as x, getPromptMessage as y }; | ||
| //# sourceMappingURL=codexExecutor-CQOWlbPt.js.map |
Sorry, the diff of this file is too big to display
| import { S as executeCommand, b as toolRegistry, c as CODEX_REASONING_EFFORTS, d as FACTORY_DEFAULT_REASONING_EFFORT, f as MODELS, l as ERROR_MESSAGES, o as processCodexEditOutput, p as STATUS_MESSAGES, t as executeCodexCLI, u as FACTORY_DEFAULT_MODEL, w as askResponseSchema, x as relativeDirSchema } from "./codexExecutor-CQOWlbPt.js"; | ||
| import { z } from "zod"; | ||
| //#region src/tools/ask-codex.tool.ts | ||
| const askCodexArgsSchema = z.object({ | ||
| prompt: z.string().min(1).max(1e5).describe("The question, code review request, or analysis task to send to Codex CLI"), | ||
| model: z.string().optional().describe(`DO NOT set this parameter. The tool automatically uses ${MODELS.DEFAULT} and falls back to ${MODELS.FALLBACK} on quota errors. Only set this if the user explicitly requests a specific model.`), | ||
| reasoningEffort: z.enum(CODEX_REASONING_EFFORTS).optional().describe(`Codex reasoning effort for this call. Defaults to ${FACTORY_DEFAULT_REASONING_EFFORT}; /codex-review and /brainstorm use high for quality-first work.`), | ||
| sessionId: z.string().optional().describe("Optional Codex thread ID to resume a prior conversation. Use the [Thread ID: ...] value from a previous response to continue the same chat with full prior context."), | ||
| includeDirs: z.array(relativeDirSchema).optional().describe("Additional directories Codex may access alongside the working directory (maps to codex `--add-dir`, repeatable). Must be relative paths (e.g., 'packages/api'). Useful in monorepos where relevant context spans sibling packages."), | ||
| preferred: z.boolean().optional().describe(`Opt into ASK_CODEX_PREFERRED_MODEL when it is configured to differ from the ${MODELS.DEFAULT} default. The built-in preferred value is also ${MODELS.PREFERRED}, so normal calls and review skills should leave this unset.`), | ||
| sandbox: z.enum(["read-only", "workspace-write"]).optional().default("read-only").describe("Codex sandbox mode for this call. Defaults to 'read-only', which enforces the core review contract (Codex reads and proposes, the MCP client edits). Set 'workspace-write' ONLY as an explicit opt-out for flows that need Codex to write files itself, e.g. image generation. Review, second-opinion, and analysis flows must never set this.") | ||
| }); | ||
| const askCodexTool = { | ||
| name: "ask-codex", | ||
| description: `Send a prompt to OpenAI Codex CLI (defaults to ${FACTORY_DEFAULT_MODEL} with automatic fallback on quota errors). Use for code review, second opinions, analysis, and AI-to-AI collaboration. Do not override the model parameter unless the user explicitly asks. Returns both human-readable text and a structured response (provider, model, sessionId, usage) via outputSchema. The returned sessionId field maps to Codex's thread_id and can be passed back as sessionId to continue the conversation.`, | ||
| zodSchema: askCodexArgsSchema, | ||
| outputSchema: askResponseSchema, | ||
| annotations: { | ||
| title: "Ask Codex", | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: false, | ||
| openWorldHint: true | ||
| }, | ||
| prompt: { description: "Execute Codex CLI to get OpenAI Codex's response for code review and analysis." }, | ||
| category: "codex", | ||
| execute: async (args, onProgress, onUsage) => { | ||
| const { prompt, model, reasoningEffort, sessionId, includeDirs, preferred, sandbox } = args; | ||
| if (!prompt?.trim()) throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED); | ||
| const result = await executeCodexCLI({ | ||
| prompt, | ||
| model, | ||
| reasoningEffort, | ||
| sessionId, | ||
| includeDirs, | ||
| preferred, | ||
| sandbox, | ||
| onProgress | ||
| }); | ||
| if (result.usage) onUsage?.(result.usage); | ||
| const threadLine = result.threadId ? `\n\n[Thread ID: ${result.threadId}]` : ""; | ||
| return { | ||
| text: `${STATUS_MESSAGES.CODEX_RESPONSE}\n${result.response}${threadLine}`, | ||
| structuredContent: { | ||
| provider: "codex", | ||
| response: result.response, | ||
| model: result.usage?.model ?? model ?? MODELS.DEFAULT, | ||
| sessionId: result.threadId, | ||
| usage: result.usage | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
| const askCodexEditTool = { | ||
| name: "ask-codex-edit", | ||
| description: "Send a code edit request to OpenAI Codex CLI and get structured search/replace edit blocks back (via codex --output-schema). Codex reads the existing files (read-only) and proposes precise, applyable changes for Claude to apply. Use this when you want Codex to suggest specific code modifications to existing files rather than just analysis. Mirrors ask-gemini-edit.", | ||
| zodSchema: z.object({ | ||
| prompt: z.string().min(1).max(1e5).describe("Describe the code changes you want against existing files. Codex returns structured search/replace edit blocks (via --output-schema) that can be applied directly."), | ||
| model: z.string().optional().describe(`DO NOT set this parameter. The tool automatically uses ${MODELS.DEFAULT} and falls back to ${MODELS.FALLBACK} on quota errors.`), | ||
| sessionId: z.string().optional().describe("Optional Codex thread ID to resume a prior conversation. Use the [Thread ID: ...] value from a previous response to refine the same edit session."), | ||
| includeDirs: z.array(relativeDirSchema).optional().describe("Additional directories Codex may read alongside the working directory (maps to codex `--add-dir`). Must be relative paths (e.g., 'packages/api'). Useful in monorepos where relevant context spans sibling packages.") | ||
| }), | ||
| annotations: { | ||
| title: "Ask Codex (Edit Mode)", | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: false, | ||
| openWorldHint: true | ||
| }, | ||
| prompt: { description: "Execute Codex CLI with --output-schema to get structured edit suggestions for existing files." }, | ||
| category: "codex", | ||
| execute: async (args, onProgress, onUsage) => { | ||
| const { prompt, model, sessionId, includeDirs } = args; | ||
| if (!prompt?.trim()) throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED); | ||
| const result = await executeCodexCLI({ | ||
| prompt, | ||
| model, | ||
| sessionId, | ||
| includeDirs, | ||
| editMode: true, | ||
| onProgress | ||
| }); | ||
| if (result.usage) onUsage?.(result.usage); | ||
| const threadLine = result.threadId ? `\n\n[Thread ID: ${result.threadId}]` : ""; | ||
| return `${processCodexEditOutput(result.response)}${threadLine}`; | ||
| } | ||
| }; | ||
| const pingTool = { | ||
| name: "ping", | ||
| description: "Test connectivity with the MCP server", | ||
| zodSchema: z.object({ message: z.string().optional().describe("A message to echo back to test the connection") }), | ||
| annotations: { | ||
| title: "Ping", | ||
| readOnlyHint: true, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| }, | ||
| prompt: { description: "Echo test message to verify MCP server is working" }, | ||
| category: "simple", | ||
| execute: async (args, onProgress) => { | ||
| return executeCommand("echo", [args.message || "Pong from Codex MCP Server!"], onProgress); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/tools/index.ts | ||
| toolRegistry.push(askCodexTool, askCodexEditTool, pingTool); | ||
| //#endregion | ||
| export {}; | ||
| //# sourceMappingURL=tools-CfSNYpIz.js.map |
| {"version":3,"file":"tools-CfSNYpIz.js","names":[],"sources":["../src/tools/ask-codex.tool.ts","../src/tools/ask-codex-edit.tool.ts","../src/tools/simple-tools.ts","../src/tools/index.ts"],"sourcesContent":["import { type AskResponse, askResponseSchema, relativeDirSchema, type UnifiedTool } from \"@ask-llm/shared\";\nimport { z } from \"zod\";\nimport {\n CODEX_REASONING_EFFORTS,\n type CodexReasoningEffort,\n ERROR_MESSAGES,\n FACTORY_DEFAULT_MODEL,\n FACTORY_DEFAULT_REASONING_EFFORT,\n MODELS,\n STATUS_MESSAGES,\n} from \"../constants.js\";\nimport { executeCodexCLI } from \"../utils/codexExecutor.js\";\n\nconst askCodexArgsSchema = z.object({\n prompt: z\n .string()\n .min(1)\n .max(100000)\n .describe(\"The question, code review request, or analysis task to send to Codex CLI\"),\n model: z\n .string()\n .optional()\n .describe(\n `DO NOT set this parameter. The tool automatically uses ${MODELS.DEFAULT} and falls back to ${MODELS.FALLBACK} on quota errors. Only set this if the user explicitly requests a specific model.`,\n ),\n reasoningEffort: z\n .enum(CODEX_REASONING_EFFORTS)\n .optional()\n .describe(\n `Codex reasoning effort for this call. Defaults to ${FACTORY_DEFAULT_REASONING_EFFORT}; /codex-review and /brainstorm use high for quality-first work.`,\n ),\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Optional Codex thread ID to resume a prior conversation. Use the [Thread ID: ...] value from a previous response to continue the same chat with full prior context.\",\n ),\n includeDirs: z\n .array(relativeDirSchema)\n .optional()\n .describe(\n \"Additional directories Codex may access alongside the working directory (maps to codex `--add-dir`, repeatable). Must be relative paths (e.g., 'packages/api'). Useful in monorepos where relevant context spans sibling packages.\",\n ),\n preferred: z\n .boolean()\n .optional()\n .describe(\n `Opt into ASK_CODEX_PREFERRED_MODEL when it is configured to differ from the ${MODELS.DEFAULT} default. The built-in preferred value is also ${MODELS.PREFERRED}, so normal calls and review skills should leave this unset.`,\n ),\n sandbox: z\n .enum([\"read-only\", \"workspace-write\"])\n .optional()\n .default(\"read-only\")\n .describe(\n \"Codex sandbox mode for this call. Defaults to 'read-only', which enforces the core review contract (Codex reads and proposes, the MCP client edits). Set 'workspace-write' ONLY as an explicit opt-out for flows that need Codex to write files itself, e.g. image generation. Review, second-opinion, and analysis flows must never set this.\",\n ),\n});\n\nexport const askCodexTool: UnifiedTool = {\n name: \"ask-codex\",\n description: `Send a prompt to OpenAI Codex CLI (defaults to ${FACTORY_DEFAULT_MODEL} with automatic fallback on quota errors). Use for code review, second opinions, analysis, and AI-to-AI collaboration. Do not override the model parameter unless the user explicitly asks. Returns both human-readable text and a structured response (provider, model, sessionId, usage) via outputSchema. The returned sessionId field maps to Codex's thread_id and can be passed back as sessionId to continue the conversation.`,\n zodSchema: askCodexArgsSchema,\n outputSchema: askResponseSchema,\n annotations: {\n title: \"Ask Codex\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n prompt: {\n description: \"Execute Codex CLI to get OpenAI Codex's response for code review and analysis.\",\n },\n category: \"codex\",\n execute: async (args, onProgress, onUsage) => {\n const { prompt, model, reasoningEffort, sessionId, includeDirs, preferred, sandbox } = args;\n if (!prompt?.trim()) {\n throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED);\n }\n\n const result = await executeCodexCLI({\n prompt: prompt as string,\n model: model as string | undefined,\n reasoningEffort: reasoningEffort as CodexReasoningEffort | undefined,\n sessionId: sessionId as string | undefined,\n includeDirs: includeDirs as string[] | undefined,\n preferred: preferred as boolean | undefined,\n sandbox: sandbox as \"read-only\" | \"workspace-write\" | undefined,\n onProgress,\n });\n\n if (result.usage) onUsage?.(result.usage);\n\n const threadLine = result.threadId ? `\\n\\n[Thread ID: ${result.threadId}]` : \"\";\n const text = `${STATUS_MESSAGES.CODEX_RESPONSE}\\n${result.response}${threadLine}`;\n const structured: AskResponse = {\n provider: \"codex\",\n response: result.response,\n model: result.usage?.model ?? (model as string | undefined) ?? MODELS.DEFAULT,\n sessionId: result.threadId,\n usage: result.usage,\n };\n return { text, structuredContent: structured as unknown as Record<string, unknown> };\n },\n};\n","import { relativeDirSchema, type UnifiedTool } from \"@ask-llm/shared\";\nimport { z } from \"zod\";\nimport { ERROR_MESSAGES, MODELS } from \"../constants.js\";\nimport { executeCodexCLI, processCodexEditOutput } from \"../utils/codexExecutor.js\";\n\nconst askCodexEditArgsSchema = z.object({\n prompt: z\n .string()\n .min(1)\n .max(100000)\n .describe(\n \"Describe the code changes you want against existing files. Codex returns structured search/replace edit blocks (via --output-schema) that can be applied directly.\",\n ),\n model: z\n .string()\n .optional()\n .describe(\n `DO NOT set this parameter. The tool automatically uses ${MODELS.DEFAULT} and falls back to ${MODELS.FALLBACK} on quota errors.`,\n ),\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Optional Codex thread ID to resume a prior conversation. Use the [Thread ID: ...] value from a previous response to refine the same edit session.\",\n ),\n includeDirs: z\n .array(relativeDirSchema)\n .optional()\n .describe(\n \"Additional directories Codex may read alongside the working directory (maps to codex `--add-dir`). Must be relative paths (e.g., 'packages/api'). Useful in monorepos where relevant context spans sibling packages.\",\n ),\n});\n\nexport const askCodexEditTool: UnifiedTool = {\n name: \"ask-codex-edit\",\n description:\n \"Send a code edit request to OpenAI Codex CLI and get structured search/replace edit blocks back (via codex --output-schema). Codex reads the existing files (read-only) and proposes precise, applyable changes for Claude to apply. Use this when you want Codex to suggest specific code modifications to existing files rather than just analysis. Mirrors ask-gemini-edit.\",\n zodSchema: askCodexEditArgsSchema,\n annotations: {\n title: \"Ask Codex (Edit Mode)\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n prompt: {\n description: \"Execute Codex CLI with --output-schema to get structured edit suggestions for existing files.\",\n },\n category: \"codex\",\n execute: async (args, onProgress, onUsage) => {\n const { prompt, model, sessionId, includeDirs } = args;\n if (!prompt?.trim()) {\n throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED);\n }\n\n const result = await executeCodexCLI({\n prompt: prompt as string,\n model: model as string | undefined,\n sessionId: sessionId as string | undefined,\n includeDirs: includeDirs as string[] | undefined,\n editMode: true,\n onProgress,\n });\n\n if (result.usage) onUsage?.(result.usage);\n\n const threadLine = result.threadId ? `\\n\\n[Thread ID: ${result.threadId}]` : \"\";\n return `${processCodexEditOutput(result.response)}${threadLine}`;\n },\n};\n","import type { UnifiedTool } from \"@ask-llm/shared\";\nimport { executeCommand } from \"@ask-llm/shared\";\nimport { z } from \"zod\";\n\nconst pingArgsSchema = z.object({\n message: z.string().optional().describe(\"A message to echo back to test the connection\"),\n});\n\nexport const pingTool: UnifiedTool = {\n name: \"ping\",\n description: \"Test connectivity with the MCP server\",\n zodSchema: pingArgsSchema,\n annotations: {\n title: \"Ping\",\n readOnlyHint: true,\n idempotentHint: true,\n openWorldHint: false,\n },\n prompt: {\n description: \"Echo test message to verify MCP server is working\",\n },\n category: \"simple\",\n execute: async (args, onProgress) => {\n const message = args.message || \"Pong from Codex MCP Server!\";\n return executeCommand(\"echo\", [message as string], onProgress);\n },\n};\n","import { toolRegistry } from \"@ask-llm/shared\";\nimport { askCodexTool } from \"./ask-codex.tool.js\";\nimport { askCodexEditTool } from \"./ask-codex-edit.tool.js\";\nimport { pingTool } from \"./simple-tools.js\";\n\ntoolRegistry.push(askCodexTool, askCodexEditTool, pingTool);\n\nexport { executeTool, getPromptMessage, toolRegistry } from \"@ask-llm/shared\";\n"],"mappings":";;;AAaA,MAAM,qBAAqB,EAAE,OAAO;CAClC,QAAQ,EACL,OAAO,CAAC,CACR,IAAI,CAAC,CAAC,CACN,IAAI,GAAM,CAAC,CACX,SAAS,0EAA0E;CACtF,OAAO,EACJ,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,0DAA0D,OAAO,QAAQ,qBAAqB,OAAO,SAAS,kFAChH;CACF,iBAAiB,EACd,KAAK,uBAAuB,CAAC,CAC7B,SAAS,CAAC,CACV,SACC,qDAAqD,iCAAiC,iEACxF;CACF,WAAW,EACR,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,qKACF;CACF,aAAa,EACV,MAAM,iBAAiB,CAAC,CACxB,SAAS,CAAC,CACV,SACC,oOACF;CACF,WAAW,EACR,QAAQ,CAAC,CACT,SAAS,CAAC,CACV,SACC,+EAA+E,OAAO,QAAQ,iDAAiD,OAAO,UAAU,6DAClK;CACF,SAAS,EACN,KAAK,CAAC,aAAa,iBAAiB,CAAC,CAAC,CACtC,SAAS,CAAC,CACV,QAAQ,WAAW,CAAC,CACpB,SACC,gVACF;AACJ,CAAC;AAED,MAAa,eAA4B;CACvC,MAAM;CACN,aAAa,kDAAkD,sBAAsB;CACrF,WAAW;CACX,cAAc;CACd,aAAa;EACX,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;CACjB;CACA,QAAQ,EACN,aAAa,iFACf;CACA,UAAU;CACV,SAAS,OAAO,MAAM,YAAY,YAAY;EAC5C,MAAM,EAAE,QAAQ,OAAO,iBAAiB,WAAW,aAAa,WAAW,YAAY;EACvF,IAAI,CAAC,QAAQ,KAAK,GAChB,MAAM,IAAI,MAAM,eAAe,kBAAkB;EAGnD,MAAM,SAAS,MAAM,gBAAgB;GAC3B;GACD;GACU;GACN;GACE;GACF;GACF;GACT;EACF,CAAC;EAED,IAAI,OAAO,OAAO,UAAU,OAAO,KAAK;EAExC,MAAM,aAAa,OAAO,WAAW,mBAAmB,OAAO,SAAS,KAAK;EAS7E,OAAO;GAAE,MAAA,GARO,gBAAgB,eAAe,IAAI,OAAO,WAAW;GAQtD,mBAAmB;IANhC,UAAU;IACV,UAAU,OAAO;IACjB,OAAO,OAAO,OAAO,SAAU,SAAgC,OAAO;IACtE,WAAW,OAAO;IAClB,OAAO,OAAO;GAE2B;EAAwC;CACrF;AACF;ACvEA,MAAa,mBAAgC;CAC3C,MAAM;CACN,aACE;CACF,WAhC6B,EAAE,OAAO;EACtC,QAAQ,EACL,OAAO,CAAC,CACR,IAAI,CAAC,CAAC,CACN,IAAI,GAAM,CAAC,CACX,SACC,oKACF;EACF,OAAO,EACJ,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,0DAA0D,OAAO,QAAQ,qBAAqB,OAAO,SAAS,kBAChH;EACF,WAAW,EACR,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,mJACF;EACF,aAAa,EACV,MAAM,iBAAiB,CAAC,CACxB,SAAS,CAAC,CACV,SACC,sNACF;CACJ,CAMa;CACX,aAAa;EACX,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;CACjB;CACA,QAAQ,EACN,aAAa,gGACf;CACA,UAAU;CACV,SAAS,OAAO,MAAM,YAAY,YAAY;EAC5C,MAAM,EAAE,QAAQ,OAAO,WAAW,gBAAgB;EAClD,IAAI,CAAC,QAAQ,KAAK,GAChB,MAAM,IAAI,MAAM,eAAe,kBAAkB;EAGnD,MAAM,SAAS,MAAM,gBAAgB;GAC3B;GACD;GACI;GACE;GACb,UAAU;GACV;EACF,CAAC;EAED,IAAI,OAAO,OAAO,UAAU,OAAO,KAAK;EAExC,MAAM,aAAa,OAAO,WAAW,mBAAmB,OAAO,SAAS,KAAK;EAC7E,OAAO,GAAG,uBAAuB,OAAO,QAAQ,IAAI;CACtD;AACF;AC7DA,MAAa,WAAwB;CACnC,MAAM;CACN,aAAa;CACb,WAPqB,EAAE,OAAO,EAC9B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,+CAA+C,EACzF,CAKa;CACX,aAAa;EACX,OAAO;EACP,cAAc;EACd,gBAAgB;EAChB,eAAe;CACjB;CACA,QAAQ,EACN,aAAa,oDACf;CACA,UAAU;CACV,SAAS,OAAO,MAAM,eAAe;EAEnC,OAAO,eAAe,QAAQ,CADd,KAAK,WAAW,6BACgB,GAAG,UAAU;CAC/D;AACF;;;ACrBA,aAAa,KAAK,cAAc,kBAAkB,QAAQ"} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
237732
0.79%1740
0.23%