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

214 lines
7.6 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";
}
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);
});