#!/usr/bin/env node import { spawnSync } from "node:child_process"; import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import path from "node:path"; function parseArgs(argv) { const options = { codexBin: "codex", outDir: "docs/protocol-snapshots/codex-app-server", }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--codex-bin") { options.codexBin = argv[index + 1]; index += 1; continue; } if (arg === "--out-dir") { options.outDir = argv[index + 1]; index += 1; continue; } if (arg === "--help" || arg === "-h") { options.help = true; } } return options; } function usage() { return `Usage: node scripts/codex-app-server-protocol-snapshot.mjs [--codex-bin codex] [--out-dir docs/protocol-snapshots/codex-app-server]`; } function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd || process.cwd(), encoding: "utf8", maxBuffer: 50 * 1024 * 1024, }); if (result.status !== 0) { throw new Error( [ `Command failed: ${command} ${args.join(" ")}`, result.stdout?.trim(), result.stderr?.trim(), ] .filter(Boolean) .join("\n"), ); } return result.stdout || ""; } function parseCodexVersion(raw) { const match = String(raw || "").match(/codex(?:-cli)?\s+([^\s]+)/i); return match?.[1] || "unknown"; } function stripTrailingWhitespace(text) { return String(text || "").replace(/[ \t]+$/gm, ""); } async function listFiles(root) { const entries = await readdir(root, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path.join(root, entry.name); if (entry.isDirectory()) { files.push(...(await listFiles(fullPath))); } else if (entry.isFile()) { files.push(fullPath); } } return files; } const PROTOCOL_METHOD_PREFIXES = [ "account", "app", "collaborationMode", "command", "config", "configRequirements", "experimentalFeature", "externalAgentConfig", "feedback", "fs", "fuzzyFileSearch", "hooks", "item", "marketplace", "mcpServer", "mcpServerStatus", "model", "modelProvider", "permissionProfile", "plugin", "process", "rawResponseItem", "remoteControl", "review", "serverRequest", "skills", "thread", "turn", "windows", "windowsSandbox", ]; const PROTOCOL_EXACT_METHODS = new Set([ "fuzzyFileSearch", "getAuthStatus", "getConversationSummary", "gitDiffToRemote", "initialize", "initialized", ]); const THREAD_ITEM_TYPES = new Set([ "agentMessage", "collabToolCall", "commandExecution", "contextCompaction", "dynamicToolCall", "enteredReviewMode", "exitedReviewMode", "fileChange", "imageGeneration", "imageView", "mcpToolCall", "plan", "reasoning", "userMessage", "webSearch", ]); function isProtocolMethod(value) { return ( PROTOCOL_EXACT_METHODS.has(value) || PROTOCOL_METHOD_PREFIXES.some((prefix) => value.startsWith(`${prefix}/`)) ); } function extractQuotedValues(text) { const values = []; const pattern = /"([A-Za-z][A-Za-z0-9_]*(?:\/[A-Za-z0-9_-]+)*)"/g; let match; while ((match = pattern.exec(text))) { values.push(match[1]); } return values; } function extractProtocolMethodsFromText(text) { const methods = new Set(); for (const value of extractQuotedValues(text)) { if (isProtocolMethod(value)) { methods.add(value); } } return methods; } function extractThreadItemTypesFromText(text) { const itemTypes = new Set(); for (const value of extractQuotedValues(text)) { if (THREAD_ITEM_TYPES.has(value)) { itemTypes.add(value); } } return itemTypes; } async function extractProtocolArtifacts(root) { const methods = new Set(); const itemTypes = new Set(); for (const filePath of await listFiles(root)) { if (!/\.(json|ts)$/.test(filePath)) { continue; } const text = await readFile(filePath, "utf8"); for (const method of extractProtocolMethodsFromText(text)) { methods.add(method); } for (const itemType of extractThreadItemTypesFromText(text)) { itemTypes.add(itemType); } } return { methods: Array.from(methods).sort(), itemTypes: Array.from(itemTypes).sort(), }; } function buildSupportMatrix({ helpText, methods, itemTypes }) { const methodSet = new Set(methods); const itemTypeSet = new Set(itemTypes); return { stdioTransport: /stdio:\/\//.test(helpText) || /stdio/.test(helpText), unixTransport: /unix:\/\//.test(helpText), wsTransport: /ws:\/\/|websocket/i.test(helpText), wsAuth: /--ws-auth/.test(helpText), threadStart: methodSet.has("thread/start"), threadResume: methodSet.has("thread/resume"), threadRead: methodSet.has("thread/read"), threadList: methodSet.has("thread/list"), threadLoadedList: methodSet.has("thread/loaded/list"), threadTurnHistory: methodSet.has("thread/turns/list"), threadInjectItems: methodSet.has("thread/inject_items"), threadRollback: methodSet.has("thread/rollback"), threadArchive: methodSet.has("thread/archive"), threadUnarchive: methodSet.has("thread/unarchive"), threadFork: methodSet.has("thread/fork"), threadCompactStart: methodSet.has("thread/compact/start"), threadNameSet: methodSet.has("thread/name/set"), threadMetadataUpdate: methodSet.has("thread/metadata/update"), threadShellCommand: methodSet.has("thread/shellCommand"), threadUnsubscribe: methodSet.has("thread/unsubscribe"), threadGoal: methodSet.has("thread/goal/set") || methodSet.has("thread/goal/get"), turnSteer: methodSet.has("turn/steer"), turnInterrupt: methodSet.has("turn/interrupt"), commandExec: methodSet.has("command/exec"), realtimeThread: methods.some((method) => method.startsWith("thread/realtime/")), modelList: methodSet.has("model/list"), appList: methodSet.has("app/list"), appListUpdated: methodSet.has("app/list/updated"), collaborationModeList: methodSet.has("collaborationMode/list"), configRequirementsRead: methodSet.has("configRequirements/read"), mcpServerStatusList: methodSet.has("mcpServerStatus/list"), skillsExtraRoots: methodSet.has("skills/extraRoots/set"), hooksList: methodSet.has("hooks/list"), pluginInstall: methodSet.has("plugin/install"), pluginUninstall: methodSet.has("plugin/uninstall"), pluginRead: methodSet.has("plugin/read"), pluginSkillRead: methodSet.has("plugin/skill/read"), pluginShare: methodSet.has("plugin/share/save") || methodSet.has("plugin/share/checkout") || methodSet.has("plugin/share/delete") || methodSet.has("plugin/share/updateTargets") || methodSet.has("plugin/share/list"), accountLogin: methodSet.has("account/login/start") || methodSet.has("account/login/cancel") || methodSet.has("account/login/completed"), accountLogout: methodSet.has("account/logout"), accountTokenRefresh: methodSet.has("account/chatgptAuthTokens/refresh"), accountAddCreditsNudge: methodSet.has("account/sendAddCreditsNudgeEmail"), configValueWrite: methodSet.has("config/value/write"), configBatchWrite: methodSet.has("config/batchWrite"), configMcpServerReload: methodSet.has("config/mcpServer/reload"), skillsConfigWrite: methodSet.has("skills/config/write"), commandExecWrite: methodSet.has("command/exec/write"), commandExecResize: methodSet.has("command/exec/resize"), commandExecTerminate: methodSet.has("command/exec/terminate"), fsRead: methodSet.has("fs/readFile") || methodSet.has("fs/readDirectory") || methodSet.has("fs/getMetadata"), fsWrite: methodSet.has("fs/writeFile") || methodSet.has("fs/createDirectory") || methodSet.has("fs/remove") || methodSet.has("fs/copy"), fsWatch: methodSet.has("fs/watch") || methodSet.has("fs/unwatch"), externalAgentImport: methodSet.has("externalAgentConfig/import"), marketplaceAdd: methodSet.has("marketplace/add"), marketplaceRemove: methodSet.has("marketplace/remove"), marketplaceUpgrade: methodSet.has("marketplace/upgrade"), experimentalFeatureEnablementSet: methodSet.has("experimentalFeature/enablement/set"), reviewStart: methodSet.has("review/start"), windowsSandboxReadiness: methodSet.has("windowsSandbox/readiness"), windowsSandboxSetupStart: methodSet.has("windowsSandbox/setupStart"), fuzzyFileSearchEvents: methodSet.has("fuzzyFileSearch/sessionUpdated") || methodSet.has("fuzzyFileSearch/sessionCompleted"), mcpOAuthLogin: methodSet.has("mcpServer/oauth/login") || methodSet.has("mcpServer/oauthLogin/completed"), mcpResourceRead: methodSet.has("mcpServer/resource/read"), mcpToolCall: methodSet.has("mcpServer/tool/call"), mcpElicitation: methodSet.has("mcpServer/elicitation/request"), toolRequestUserInput: methodSet.has("item/tool/requestUserInput"), permissionRequestApproval: methodSet.has("item/permissions/requestApproval"), guardianDeniedActionApproval: methodSet.has("thread/approveGuardianDeniedAction"), processEvents: methodSet.has("process/outputDelta") || methodSet.has("process/exited"), rawResponseCompleted: methodSet.has("rawResponseItem/completed"), skillsChanged: methodSet.has("skills/changed"), pluginInstalledNotification: methodSet.has("plugin/installed"), threadLifecycleEvents: methodSet.has("thread/started") || methodSet.has("thread/closed") || methodSet.has("thread/archived") || methodSet.has("thread/unarchived") || methodSet.has("thread/name/updated"), agentMessageDelta: methodSet.has("item/agentMessage/delta"), planDelta: methodSet.has("item/plan/delta"), reasoningDeltas: methodSet.has("item/reasoning/summaryPartAdded") || methodSet.has("item/reasoning/summaryTextDelta") || methodSet.has("item/reasoning/textDelta"), mcpToolProgress: methodSet.has("item/mcpToolCall/progress"), commandOutputDeltas: methodSet.has("command/exec/outputDelta") || methodSet.has("item/commandExecution/outputDelta"), terminalInteraction: methodSet.has("item/commandExecution/terminalInteraction"), fileChangeOutputDelta: methodSet.has("item/fileChange/outputDelta"), threadCollaborationItems: itemTypeSet.has("collabToolCall"), contextCompactionItem: itemTypeSet.has("contextCompaction"), }; } async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { console.log(usage()); return; } const versionRaw = run(options.codexBin, ["--version"]).trim(); const codexVersion = parseCodexVersion(versionRaw); const snapshotDir = path.resolve(options.outDir, codexVersion); const schemaDir = path.join(snapshotDir, "json-schema"); const typescriptDir = path.join(snapshotDir, "typescript"); await mkdir(schemaDir, { recursive: true }); await mkdir(typescriptDir, { recursive: true }); const helpText = stripTrailingWhitespace(run(options.codexBin, ["app-server", "--help"])); await writeFile(path.join(snapshotDir, "app-server-help.txt"), helpText, "utf8"); run(options.codexBin, ["app-server", "generate-json-schema", "--out", schemaDir]); run(options.codexBin, ["app-server", "generate-ts", "--out", typescriptDir]); const schemaArtifacts = await extractProtocolArtifacts(schemaDir); const typescriptArtifacts = await extractProtocolArtifacts(typescriptDir); const methods = Array.from(new Set([...schemaArtifacts.methods, ...typescriptArtifacts.methods])).sort(); const itemTypes = Array.from(new Set([...schemaArtifacts.itemTypes, ...typescriptArtifacts.itemTypes])).sort(); const manifest = { generatedAt: new Date().toISOString(), codexVersion, codexVersionRaw: versionRaw, codexBin: options.codexBin, snapshotDir, methods, itemTypes, supports: buildSupportMatrix({ helpText, methods, itemTypes }), }; await writeFile(path.join(snapshotDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); console.log(JSON.stringify({ ok: true, snapshotDir, codexVersion, methodCount: methods.length })); } main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });