357 lines
12 KiB
JavaScript
357 lines
12 KiB
JavaScript
#!/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);
|
|
});
|