#!/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"; } 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; } function extractProtocolMethodsFromText(text) { const methods = new Set(); const pattern = /"([A-Za-z][A-Za-z0-9]*(?:\/[A-Za-z0-9_-]+)+)"/g; let match; while ((match = pattern.exec(text))) { const value = match[1]; if (/^(thread|turn|item|rawResponseItem|model|modelProvider|experimentalFeature|permissionProfile|process|command|review|account|config|mcpServer|plugin|marketplace|skills|hooks|fs|remoteControl|externalAgentConfig|fuzzyFileSearch|windowsSandbox)\//.test(value)) { methods.add(value); } } return methods; } async function extractProtocolMethods(root) { const methods = 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); } } return Array.from(methods).sort(); } function buildSupportMatrix({ helpText, methods }) { const methodSet = new Set(methods); return { stdioTransport: /stdio:\/\//.test(helpText) || /stdio/.test(helpText), unixTransport: /unix:\/\//.test(helpText), wsTransport: /ws:\/\/|websocket/i.test(helpText), wsAuth: /--ws-auth/.test(helpText), 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"), 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"), }; } 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 = 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 methods = Array.from( new Set([ ...(await extractProtocolMethods(schemaDir)), ...(await extractProtocolMethods(typescriptDir)), ]), ).sort(); const manifest = { generatedAt: new Date().toISOString(), codexVersion, codexVersionRaw: versionRaw, codexBin: options.codexBin, snapshotDir, methods, supports: buildSupportMatrix({ helpText, methods }), }; 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); });