Files
boss/scripts/codex-app-server-protocol-snapshot.mjs

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);
});