#!/usr/bin/env node // Deterministic, dependency-free bindings for the stable protocol subset we use. import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import {execFileSync} from 'node:child_process'; const root = path.resolve(import.meta.dirname, '../pkg/adapters/codexrpc'); const version = '0.153.0'; const inputs = ['v1/InitializeParams', 'v1/InitializeResponse', 'v2/ThreadStartParams', 'v2/ThreadStartResponse', 'v2/ThreadResumeParams', 'v2/ThreadResumeResponse', 'v2/TurnStartParams', 'v2/TurnStartResponse', 'v2/TurnInterruptParams', 'v2/TurnInterruptResponse', 'v2/TurnStartedNotification', 'v2/TurnCompletedNotification', 'v2/ItemStartedNotification', 'v2/ItemCompletedNotification', 'v2/ErrorNotification', 'v2/PluginInstallParams', 'v2/PluginInstallResponse']; const schemaFile = path.join(root, 'schema/protocol.json'); if (process.argv[2] && process.argv[2] !== '--check') { const installed = execFileSync(process.env.CODEX_BIN || 'codex', ['--version'], {encoding:'utf8'}).trim(); if (installed !== 'codex-cli ' + version) throw new Error('Schema refresh requires pinned Codex ' + version); const all = {}, selected = {}; for (const name of inputs) { const schema = JSON.parse(fs.readFileSync(path.join(process.argv[2], name + '.json'))); Object.assign(all, schema.definitions); const {$schema, definitions, ...body} = schema; all[schema.title] = body; } function visit(name) { if (selected[name]) return; if (!all[name]) throw new Error('Unresolved definition ' + name); selected[name] = all[name]; function walk(value) { if (Array.isArray(value)) return value.forEach(walk); if (!value || typeof value !== 'object') return; if (value.$ref) visit(value.$ref.split('/').at(-1)); Object.values(value).forEach(walk); } walk(all[name]); } inputs.forEach(name => visit(name.split('/').at(-1))); const ordered = Object.fromEntries(Object.keys(selected).sort().map(name => [name, selected[name]])); fs.writeFileSync(schemaFile, JSON.stringify({$schema:'http://json-schema.org/draft-07/schema#', codexVersion:version, sourceCommit:'41e22fee981a63b3698df7ed36bad393cda24715', generatedBy:'codex app-server generate-json-schema (stable)', definitions:ordered}, null, 2) + '\n'); } const raw = fs.readFileSync(schemaFile); const schema = JSON.parse(raw); if (schema.codexVersion !== version || schema.sourceCommit !== '41e22fee981a63b3698df7ed36bad393cda24715') throw new Error('Unexpected Codex schema provenance'); const {definitions} = schema; const names = s => s.replace(/(^|[^a-zA-Z0-9])([a-zA-Z0-9])/g, (_, a, b) => b.toUpperCase()).replace(/Id(s)?$/, (_, s) => 'ID' + (s || '')); function nullable(s) {return Array.isArray(s.type) && s.type.includes('null') || (s.anyOf || []).some(v => v.type === 'null');} function type(s) { if (s.$ref) return s.$ref.split('/').at(-1); if (s.anyOf) { const variants = s.anyOf.filter(v => v.type !== 'null'); if (variants.length === 1) return type(variants[0]); return 'json.RawMessage'; } if (s.oneOf) return s.oneOf.every(v => v.type === 'string') ? 'string' : 'json.RawMessage'; const t = Array.isArray(s.type) ? s.type.find(t => t !== 'null') : s.type; switch (t) { case 'string': return 'string'; case 'integer': return 'int64'; case 'number': return 'float64'; case 'boolean': return 'bool'; case 'array': return '[]' + type(s.items); case 'object': { if (!s.properties) return 'map[string]' + (s.additionalProperties && typeof s.additionalProperties === 'object' ? type(s.additionalProperties) : 'json.RawMessage'); return object(s); } default: return 'json.RawMessage'; } } function object(s) { const required = new Set(s.required || []); return 'struct {\n' + Object.keys(s.properties).sort().map(key => { const v = s.properties[key]; let t = type(v); const optional = !required.has(key) || nullable(v); // Pointers preserve explicit false/zero versus omitted override values. if (optional && !t.startsWith('[]') && !t.startsWith('map[') && t !== 'json.RawMessage') t = '*' + t; return names(key) + ' ' + t + ' `json:"' + key + (optional ? ',omitempty' : '') + '"`'; }).join('\n') + '\n}'; } let go = '// Code generated by scripts/generate-codex-rpc.mjs; DO NOT EDIT.\n// Stable Codex ' + version + '; schema SHA-256 ' + crypto.createHash('sha256').update(raw).digest('hex') + '\npackage codexrpc\n\nimport "encoding/json"\n\nconst ProtocolVersion = "' + version + '"\n'; for (const [name, original] of Object.entries(definitions)) { let s = original; // Tagged item/input variants share one envelope, retaining all schema fields. // Consumers MUST dispatch on Type before reading variant-specific fields. if (['ThreadItem', 'UserInput'].includes(name)) { const properties = {}; for (const branch of s.oneOf) for (const [field, shape] of Object.entries(branch.properties)) { if (!(field in properties)) properties[field] = shape; else if (type(properties[field]) !== type(shape)) properties[field] = {}; } properties.type = {type:'string'}; s = {type:'object', properties, required:['type']}; } const target = type(s); go += '\ntype ' + name + (target === 'json.RawMessage' ? ' = ' : ' ') + target + '\n'; } const formatted = execFileSync('gofmt', [], {input:go,encoding:'utf8'}); const output = path.join(root, 'protocol.gen.go'); if (process.argv[2] === '--check') { if (fs.readFileSync(output,'utf8') !== formatted) throw new Error('Codex RPC bindings differ from the pinned schema; regenerate them'); } else fs.writeFileSync(output, formatted);