@meistrari/agent-core 0.2.0-experimental.1 → 0.2.0-experimental.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +50 -0
  2. package/docs/consumer-capability-matrix.md +374 -0
  3. package/package.json +17 -10
  4. package/scripts/generate-codex-protocol.ts +711 -0
  5. package/src/agents/agent-operation.ts +8 -0
  6. package/src/agents/agent-provider.ts +3 -0
  7. package/src/agents/agent-run.ts +14 -2
  8. package/src/agents/agent-runtime-error.ts +28 -0
  9. package/src/agents/agent-session-events.ts +33 -0
  10. package/src/agents/agent-tool-runner.ts +12 -11
  11. package/src/agents/agent-tool.ts +12 -4
  12. package/src/agents/author-context.ts +1 -1
  13. package/src/agents/claude/claude-auth.ts +40 -0
  14. package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
  15. package/src/agents/claude/claude-code-process.ts +98 -0
  16. package/src/agents/claude/claude-command-mapper.ts +63 -3
  17. package/src/agents/claude/claude-desktop.ts +55 -0
  18. package/src/agents/claude/claude-event-mapper.ts +868 -135
  19. package/src/agents/claude/claude-file-change.ts +128 -0
  20. package/src/agents/claude/claude-provider.ts +356 -30
  21. package/src/agents/claude/claude-run.ts +898 -130
  22. package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
  23. package/src/agents/claude/claude-skill-catalog.ts +75 -0
  24. package/src/agents/claude/claude-steered-notifications.ts +180 -0
  25. package/src/agents/claude/claude-tool-mapper.ts +81 -2
  26. package/src/agents/claude/claude-workspace-skills.ts +87 -0
  27. package/src/agents/claude/index.ts +2 -0
  28. package/src/agents/codex/codex-auth-mapper.ts +27 -0
  29. package/src/agents/codex/codex-auth.ts +19 -0
  30. package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
  31. package/src/agents/codex/codex-browser-auth-status.ts +55 -0
  32. package/src/agents/codex/codex-command-mapper.ts +38 -3
  33. package/src/agents/codex/codex-event-mapper.ts +384 -67
  34. package/src/agents/codex/codex-json-rpc-client.ts +66 -21
  35. package/src/agents/codex/codex-provider.ts +118 -236
  36. package/src/agents/codex/codex-run-auth.ts +113 -0
  37. package/src/agents/codex/codex-run.ts +423 -53
  38. package/src/agents/codex/codex-tool-mapper.ts +2 -2
  39. package/src/agents/codex/codex.errors.ts +8 -0
  40. package/src/agents/codex/generated/meta.gen.ts +144 -6
  41. package/src/agents/codex/generated/namespaces.gen.ts +92 -3
  42. package/src/agents/codex/generated/schema.gen.ts +24698 -15638
  43. package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
  44. package/src/agents/index.ts +5 -0
  45. package/src/agents/input-attachment-preparation.ts +13 -0
  46. package/src/agents/mcp-tool-content.ts +82 -0
  47. package/src/agents/model-rebinding-run.ts +14 -3
  48. package/src/agents/selected-repository-instructions.ts +202 -0
  49. package/src/agents/session-context.ts +41 -0
  50. package/src/agents/subscription-token-refresh.ts +73 -0
  51. package/src/agents/user-input-request.ts +19 -20
  52. package/src/errors/index.ts +1 -1
  53. package/src/errors/validation-error.ts +6 -0
  54. package/src/protocol/agent-catalog.ts +159 -0
  55. package/src/protocol/agent-command.ts +52 -7
  56. package/src/protocol/agent-content.ts +36 -16
  57. package/src/protocol/agent-event.ts +210 -73
  58. package/src/protocol/agent-identity.ts +29 -0
  59. package/src/protocol/agent-json.ts +122 -8
  60. package/src/protocol/agent-metadata.ts +2 -2
  61. package/src/protocol/agent-model.ts +13 -1
  62. package/src/protocol/agent-notification.ts +110 -0
  63. package/src/protocol/agent-presentation.ts +71 -20
  64. package/src/protocol/agent-tool.ts +66 -10
  65. package/src/protocol/agent-usage.ts +3 -1
  66. package/src/protocol/agent-user-input.ts +4 -4
  67. package/src/protocol/agent-work.ts +12 -10
  68. package/src/protocol/index.ts +2 -0
  69. package/src/protocol/rejected-keys.ts +11 -0
  70. package/src/provenance.gen.ts +3 -3
  71. package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
@@ -0,0 +1,711 @@
1
+ #!/usr/bin/env bun
2
+ import type { JSONSchema } from 'json-schema-to-typescript'
3
+ import { spawnSync } from 'node:child_process'
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { basename, dirname, join, relative, resolve } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { compile } from 'json-schema-to-typescript'
17
+
18
+ type JsonSchemaObject = JSONSchema & { [key: string]: unknown }
19
+ interface SchemaMap { [key: string]: JSONSchema }
20
+
21
+ interface JsonSchemaFile {
22
+ path: string
23
+ namespace: string | undefined
24
+ fileName: string
25
+ qualifiedName: string
26
+ exportName: string
27
+ }
28
+
29
+ interface MethodEntry {
30
+ method: string
31
+ paramsType: string | undefined
32
+ }
33
+
34
+ interface ResponseEntry {
35
+ method: string
36
+ responseType: string
37
+ }
38
+
39
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
40
+ const codexCliVersion = '0.156.1'
41
+ const upstreamRef = '3d2ee51ca2d5db578f328aa75e20aa22c0197c9a'
42
+ const upstreamSchemaRoot = generateCodexSchemas()
43
+ const upstreamJsonDir = join(upstreamSchemaRoot, 'json')
44
+ const upstreamTypescriptDir = join(upstreamSchemaRoot, 'typescript')
45
+ const generatedDir = join(root, 'src', 'agents', 'codex', 'generated')
46
+
47
+ const MANUAL_SCHEMAS = {
48
+ GetAuthStatusParams: {
49
+ type: 'object',
50
+ title: 'GetAuthStatusParams',
51
+ properties: {
52
+ includeToken: { anyOf: [{ type: 'boolean' }, { type: 'null' }] },
53
+ refreshToken: { anyOf: [{ type: 'boolean' }, { type: 'null' }] },
54
+ },
55
+ },
56
+ GetConversationSummaryParams: {
57
+ title: 'GetConversationSummaryParams',
58
+ oneOf: [
59
+ {
60
+ type: 'object',
61
+ properties: { rolloutPath: { type: 'string' } },
62
+ required: ['rolloutPath'],
63
+ },
64
+ {
65
+ type: 'object',
66
+ properties: { conversationId: { type: 'string' } },
67
+ required: ['conversationId'],
68
+ },
69
+ ],
70
+ },
71
+ GetConversationSummaryResponse: {
72
+ type: 'object',
73
+ title: 'GetConversationSummaryResponse',
74
+ properties: { summary: {} },
75
+ required: ['summary'],
76
+ },
77
+ GitDiffToRemoteParams: {
78
+ type: 'object',
79
+ title: 'GitDiffToRemoteParams',
80
+ properties: { cwd: { type: 'string' } },
81
+ required: ['cwd'],
82
+ },
83
+ GitDiffToRemoteResponse: {
84
+ type: 'object',
85
+ title: 'GitDiffToRemoteResponse',
86
+ properties: {
87
+ sha: { type: 'string' },
88
+ diff: { type: 'string' },
89
+ },
90
+ required: ['sha', 'diff'],
91
+ },
92
+ GetAuthStatusResponse: {
93
+ type: 'object',
94
+ title: 'GetAuthStatusResponse',
95
+ properties: {
96
+ authMethod: { anyOf: [{}, { type: 'null' }] },
97
+ authToken: { anyOf: [{ type: 'string' }, { type: 'null' }] },
98
+ requiresOpenaiAuth: { anyOf: [{ type: 'boolean' }, { type: 'null' }] },
99
+ },
100
+ required: ['authMethod', 'authToken', 'requiresOpenaiAuth'],
101
+ },
102
+ } satisfies SchemaMap
103
+
104
+ try {
105
+ await generateProtocolTypes()
106
+ }
107
+ finally {
108
+ rmSync(upstreamSchemaRoot, { recursive: true, force: true })
109
+ }
110
+ console.log(`Generated Codex ${codexCliVersion} experimental app-server protocol types at ${upstreamRef}`)
111
+
112
+ async function generateProtocolTypes(): Promise<void> {
113
+ if (!existsSync(upstreamJsonDir))
114
+ throw new Error(`Codex JSON schema fixtures not found: ${upstreamJsonDir}`)
115
+ if (!existsSync(upstreamTypescriptDir))
116
+ throw new Error(`Codex TypeScript method fixtures not found: ${upstreamTypescriptDir}`)
117
+
118
+ rmSync(generatedDir, { recursive: true, force: true })
119
+ mkdirSync(generatedDir, { recursive: true })
120
+
121
+ const jsonSchemaFiles = listJsonSchemaFiles(upstreamJsonDir)
122
+ const exportNameByQualifiedName = new Map(
123
+ jsonSchemaFiles.map(file => [file.qualifiedName, file.exportName]),
124
+ )
125
+ const aggregateSchemas: SchemaMap = {}
126
+
127
+ for (const file of jsonSchemaFiles) {
128
+ const parsed = parseSchemaFile(file.path)
129
+ const localDefinitionNames = new Map(
130
+ Object.keys(parsed.definitions ?? {}).map(definitionName => [
131
+ definitionName,
132
+ `${file.exportName}__${definitionName.replace(/[^A-Z0-9]/gi, '')}`,
133
+ ]),
134
+ )
135
+
136
+ for (const [definitionName, definitionSchema] of Object.entries(parsed.definitions ?? {})) {
137
+ const exportName = localDefinitionNames.get(definitionName)
138
+ if (!exportName)
139
+ throw new Error(`Missing local definition export name for ${definitionName}`)
140
+ aggregateSchemas[exportName] = namedSchema(
141
+ exportName,
142
+ normalizeSchema(
143
+ rewriteExternalRefs(
144
+ definitionSchema,
145
+ localDefinitionNames,
146
+ file.namespace,
147
+ exportNameByQualifiedName,
148
+ ),
149
+ ),
150
+ )
151
+ }
152
+
153
+ const topLevelSchema = Object.fromEntries(
154
+ Object.entries(parsed).filter(([key]) => key !== 'definitions'),
155
+ )
156
+ aggregateSchemas[file.exportName] = namedSchema(
157
+ file.exportName,
158
+ normalizeSchema(
159
+ rewriteExternalRefs(
160
+ topLevelSchema,
161
+ localDefinitionNames,
162
+ file.namespace,
163
+ exportNameByQualifiedName,
164
+ ),
165
+ ),
166
+ )
167
+ }
168
+
169
+ for (const [name, schema] of Object.entries(MANUAL_SCHEMAS)) {
170
+ if (!(name in aggregateSchemas))
171
+ aggregateSchemas[name] = namedSchema(name, normalizeSchema(schema))
172
+ }
173
+
174
+ const generatedSchemaNames = new Set(Object.keys(aggregateSchemas))
175
+ const schemaSource = await compile(
176
+ {
177
+ title: 'CodexSchema',
178
+ type: 'object',
179
+ additionalProperties: true,
180
+ definitions: aggregateSchemas,
181
+ },
182
+ 'CodexSchema',
183
+ {
184
+ bannerComment: '',
185
+ unreachableDefinitions: true,
186
+ style: {
187
+ bracketSpacing: true,
188
+ printWidth: 120,
189
+ semi: true,
190
+ singleQuote: false,
191
+ tabWidth: 2,
192
+ trailingComma: 'all',
193
+ useTabs: false,
194
+ },
195
+ },
196
+ )
197
+
198
+ writeFileSync(
199
+ join(generatedDir, 'schema.gen.ts'),
200
+ [
201
+ '/* eslint-disable style/indent, style/member-delimiter-style, style/quotes, style/semi, style/operator-linebreak, style/indent-binary-ops */',
202
+ '// This file is generated by scripts/generate-codex-protocol.ts. Do not edit manually.',
203
+ `// Upstream Codex protocol ref: ${upstreamRef}`,
204
+ '',
205
+ schemaSource.trim(),
206
+ '',
207
+ ].join('\n'),
208
+ 'utf8',
209
+ )
210
+
211
+ const clientRequests = parseRequestEntries(readFileSync(join(upstreamTypescriptDir, 'ClientRequest.ts'), 'utf8'))
212
+ const clientNotifications = parseNotificationEntries(readFileSync(join(upstreamTypescriptDir, 'ClientNotification.ts'), 'utf8'))
213
+ const serverRequests = parseRequestEntries(readFileSync(join(upstreamTypescriptDir, 'ServerRequest.ts'), 'utf8'))
214
+ const serverNotifications = parseNotificationEntries(readFileSync(join(upstreamTypescriptDir, 'ServerNotification.ts'), 'utf8'))
215
+
216
+ writeFileSync(
217
+ join(generatedDir, 'meta.gen.ts'),
218
+ renderMeta({
219
+ upstreamRef,
220
+ generatedSchemaNames,
221
+ clientRequests,
222
+ clientNotifications,
223
+ serverRequests,
224
+ serverNotifications,
225
+ }),
226
+ 'utf8',
227
+ )
228
+
229
+ writeFileSync(
230
+ join(generatedDir, 'namespaces.gen.ts'),
231
+ renderNamespaces(upstreamRef, jsonSchemaFiles),
232
+ 'utf8',
233
+ )
234
+ }
235
+
236
+ function generateCodexSchemas(): string {
237
+ const dir = mkdtempSync(join(tmpdir(), 'codex-protocol-'))
238
+ try {
239
+ const jsonDir = join(dir, 'json')
240
+ const typescriptDir = join(dir, 'typescript')
241
+ mkdirSync(jsonDir, { recursive: true })
242
+ mkdirSync(typescriptDir, { recursive: true })
243
+
244
+ const executable = process.env.CODEX_EXECUTABLE
245
+ if (executable)
246
+ assertCodexVersion(executable)
247
+ const command = executable ?? 'bunx'
248
+ const prefix = executable ? [] : [`@openai/codex@${codexCliVersion}`]
249
+ run(command, [...prefix, 'app-server', 'generate-json-schema', '--out', jsonDir, '--experimental'])
250
+ run(command, [...prefix, 'app-server', 'generate-ts', '--out', typescriptDir, '--experimental'])
251
+ return dir
252
+ }
253
+ catch (error) {
254
+ rmSync(dir, { recursive: true, force: true })
255
+ throw error
256
+ }
257
+ }
258
+
259
+ function assertCodexVersion(executable: string): void {
260
+ const result = spawnSync(executable, ['--version'], { encoding: 'utf8' })
261
+ const expected = `codex-cli ${codexCliVersion}`
262
+ if (result.status !== 0 || result.stdout.trim() !== expected)
263
+ throw new Error(`CODEX_EXECUTABLE must report ${expected}.`)
264
+ }
265
+
266
+ function run(command: string, args: string[]): void {
267
+ const result = spawnSync(command, args, { encoding: 'utf8' })
268
+ if (result.status !== 0) {
269
+ process.stderr.write(result.stdout)
270
+ process.stderr.write(result.stderr)
271
+ throw new Error(`${command} ${args.join(' ')} failed`)
272
+ }
273
+ }
274
+
275
+ function parseSchemaFile(path: string): JsonSchemaObject {
276
+ const parsed = JSON.parse(readFileSync(path, 'utf8'))
277
+ if (!isJsonObject(parsed))
278
+ throw new Error(`Codex schema fixture must be an object: ${path}`)
279
+ return parsed
280
+ }
281
+
282
+ function listJsonSchemaFiles(dir: string): JsonSchemaFile[] {
283
+ return walk(dir)
284
+ .filter(path => path.endsWith('.json') && !basename(path).startsWith('codex_app_server_protocol.'))
285
+ .map((path) => {
286
+ const qualifiedName = relative(dir, path).replace(/\\/g, '/').replace(/\.json$/, '')
287
+ const parts = qualifiedName.split('/')
288
+ const namespace = parts.length > 1 ? parts[0] : undefined
289
+ return {
290
+ path,
291
+ namespace,
292
+ fileName: basename(path),
293
+ qualifiedName,
294
+ exportName: exportNameForQualifiedName(qualifiedName),
295
+ }
296
+ })
297
+ .sort((left, right) => left.exportName.localeCompare(right.exportName))
298
+ }
299
+
300
+ function walk(dir: string): string[] {
301
+ const entries: string[] = []
302
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
303
+ const path = join(dir, entry.name)
304
+ if (entry.isDirectory())
305
+ entries.push(...walk(path))
306
+ else entries.push(path)
307
+ }
308
+ return entries
309
+ }
310
+
311
+ function exportNameForQualifiedName(qualifiedName: string): string {
312
+ const parts = qualifiedName.split('/')
313
+ if (parts.length === 1)
314
+ return parts[0] ?? qualifiedName
315
+ const namespacePrefix = parts
316
+ .slice(0, -1)
317
+ .flatMap(part => part.split(/[^A-Z0-9]+/i))
318
+ .filter(segment => segment.length > 0)
319
+ .map(toPascalSegment)
320
+ .join('')
321
+ return `${namespacePrefix}${parts[parts.length - 1] ?? ''}`
322
+ }
323
+
324
+ function normalizeSchema(value: unknown): JSONSchema {
325
+ return stripNullDefaults(normalizeNullableTypes(value)) as JSONSchema
326
+ }
327
+
328
+ function namedSchema(name: string, schema: JSONSchema): JSONSchema {
329
+ if (!isJsonObject(schema))
330
+ return schema
331
+ return { ...schema, title: name }
332
+ }
333
+
334
+ function normalizeNullableTypes(value: unknown): unknown {
335
+ if (Array.isArray(value))
336
+ return value.map(normalizeNullableTypes)
337
+ if (!isJsonObject(value))
338
+ return value
339
+
340
+ const normalizedObject: JsonSchemaObject = Object.fromEntries(
341
+ Object.entries(value).map(([key, child]) => [key, normalizeNullableTypes(child)]),
342
+ )
343
+ const typeValue = normalizedObject.type
344
+ if (!Array.isArray(typeValue))
345
+ return normalizedObject
346
+
347
+ const normalizedTypes = typeValue.filter(entry => typeof entry === 'string')
348
+ if (normalizedTypes.length !== typeValue.length || !normalizedTypes.includes('null'))
349
+ return normalizedObject
350
+ const nonNullTypes = normalizedTypes.filter(entry => entry !== 'null')
351
+ if (nonNullTypes.length !== 1)
352
+ return normalizedObject
353
+
354
+ const withoutType = { ...normalizedObject }
355
+ delete withoutType.type
356
+ return {
357
+ anyOf: [
358
+ {
359
+ ...withoutType,
360
+ type: nonNullTypes[0],
361
+ },
362
+ { type: 'null' },
363
+ ],
364
+ }
365
+ }
366
+
367
+ function stripNullDefaults(value: unknown): unknown {
368
+ if (Array.isArray(value))
369
+ return value.map(stripNullDefaults)
370
+ if (!isJsonObject(value))
371
+ return value
372
+ return Object.fromEntries(
373
+ Object.entries(value)
374
+ .filter(([key, child]) => !(key === 'default' && child === null))
375
+ .map(([key, child]) => [key, stripNullDefaults(child)]),
376
+ )
377
+ }
378
+
379
+ function rewriteExternalRefs(
380
+ value: unknown,
381
+ localDefinitionNames: Map<string, string>,
382
+ currentNamespace: string | undefined,
383
+ exportNameByQualifiedName: Map<string, string>,
384
+ ): unknown {
385
+ if (Array.isArray(value)) {
386
+ return value.map(entry =>
387
+ rewriteExternalRefs(entry, localDefinitionNames, currentNamespace, exportNameByQualifiedName),
388
+ )
389
+ }
390
+ if (!isJsonObject(value))
391
+ return value
392
+
393
+ return Object.fromEntries(
394
+ Object.entries(value).map(([key, child]) => {
395
+ if (key === '$ref' && typeof child === 'string' && child.startsWith('#/definitions/')) {
396
+ const definitionName = child.slice('#/definitions/'.length)
397
+ const localRewrite = localDefinitionNames.get(definitionName)
398
+ if (localRewrite)
399
+ return [key, `#/definitions/${localRewrite}`]
400
+
401
+ const candidates = [
402
+ ...(currentNamespace ? [`${currentNamespace}/${definitionName}`] : []),
403
+ definitionName,
404
+ definitionName.replace(/^v[12]\//, ''),
405
+ definitionName.replace(/^serde_json\//, ''),
406
+ `v2/${definitionName}`,
407
+ `v1/${definitionName}`,
408
+ `serde_json/${definitionName}`,
409
+ ]
410
+ const rewritten = candidates
411
+ .map(candidate => exportNameByQualifiedName.get(candidate))
412
+ .find((candidate): candidate is string => candidate !== undefined)
413
+ if (!rewritten)
414
+ throw new Error(`Missing rewritten definition for ref: ${child}`)
415
+ return [key, `#/definitions/${rewritten}`]
416
+ }
417
+
418
+ return [
419
+ key,
420
+ rewriteExternalRefs(child, localDefinitionNames, currentNamespace, exportNameByQualifiedName),
421
+ ]
422
+ }),
423
+ )
424
+ }
425
+
426
+ function parseRequestEntries(source: string): MethodEntry[] {
427
+ const declaration = extractRequestTypeDeclaration(source)
428
+ const nativeMethods = [...declaration.matchAll(/(?:["']method["']|\bmethod)\s*:\s*["']([^"']+)["']/g)]
429
+ .map(match => match[1])
430
+ .filter((method): method is string => method !== undefined)
431
+ assertUniqueMethods(nativeMethods, 'native request declaration')
432
+ if (nativeMethods.length === 0)
433
+ throw new Error('Codex request declaration contains no method entries')
434
+
435
+ const entryPattern = /\{\s*"method":\s*"([^"]+)",\s*id:\s*RequestId,\s*params(\?)?:([^,{}]+),\s*\}/g
436
+ const entries: MethodEntry[] = []
437
+ for (const match of declaration.matchAll(entryPattern)) {
438
+ const method = match[1]
439
+ const optionalParams = match[2] === '?'
440
+ const rawParamsType = match[3]?.trim()
441
+ if (!method || !rawParamsType)
442
+ throw new Error(`Unable to parse Codex request entry: ${match[0]}`)
443
+ const parsedParamsType = parseSupportedParamsType(rawParamsType)
444
+ const paramsType = optionalParams && !parsedParamsType.includesUndefined
445
+ ? `${rawParamsType} | undefined`
446
+ : rawParamsType
447
+ entries.push({ method, paramsType })
448
+ }
449
+
450
+ const parsedMethods = entries.map(entry => entry.method)
451
+ assertUniqueMethods(parsedMethods, 'parsed request entries')
452
+ const parsedMethodSet = new Set(parsedMethods)
453
+ const nativeMethodSet = new Set(nativeMethods)
454
+ const missingMethods = nativeMethods.filter(method => !parsedMethodSet.has(method))
455
+ const unexpectedMethods = parsedMethods.filter(method => !nativeMethodSet.has(method))
456
+ if (missingMethods.length > 0 || unexpectedMethods.length > 0) {
457
+ throw new Error([
458
+ 'Codex request parsing did not cover the native method set.',
459
+ `Missing: ${missingMethods.join(', ') || 'none'}.`,
460
+ `Unexpected: ${unexpectedMethods.join(', ') || 'none'}.`,
461
+ ].join(' '))
462
+ }
463
+ return entries
464
+ }
465
+
466
+ function extractRequestTypeDeclaration(source: string): string {
467
+ const declarations = [...source.matchAll(/export type [A-Z]\w*Request\s*=\s*/g)]
468
+ if (declarations.length !== 1)
469
+ throw new Error(`Expected one Codex request type declaration, found ${declarations.length}`)
470
+ const declaration = declarations[0]
471
+ if (declaration?.index === undefined)
472
+ throw new Error('Unable to locate Codex request type declaration')
473
+ const body = source.slice(declaration.index + declaration[0].length).trim()
474
+ if (!body.endsWith(';'))
475
+ throw new Error('Codex request type declaration must end with a semicolon')
476
+ return body.slice(0, -1).trim()
477
+ }
478
+
479
+ function assertUniqueMethods(methods: string[], subject: string): void {
480
+ const seen = new Set<string>()
481
+ const duplicates = new Set<string>()
482
+ for (const method of methods) {
483
+ if (seen.has(method))
484
+ duplicates.add(method)
485
+ seen.add(method)
486
+ }
487
+ if (duplicates.size > 0)
488
+ throw new Error(`Duplicate methods in ${subject}: ${[...duplicates].join(', ')}`)
489
+ }
490
+
491
+ function parseNotificationEntries(source: string): MethodEntry[] {
492
+ const entryPattern = /"method": "([^"]+)"(?:, "params": (\w+(?: \| null)?))?/g
493
+ const entries: MethodEntry[] = []
494
+ for (const match of source.matchAll(entryPattern)) {
495
+ const method = match[1]
496
+ if (!method)
497
+ continue
498
+ entries.push({ method, paramsType: match[2]?.trim() })
499
+ }
500
+ return entries
501
+ }
502
+
503
+ function renderMeta({
504
+ upstreamRef,
505
+ generatedSchemaNames,
506
+ clientRequests,
507
+ clientNotifications,
508
+ serverRequests,
509
+ serverNotifications,
510
+ }: {
511
+ upstreamRef: string
512
+ generatedSchemaNames: Set<string>
513
+ clientRequests: MethodEntry[]
514
+ clientNotifications: MethodEntry[]
515
+ serverRequests: MethodEntry[]
516
+ serverNotifications: MethodEntry[]
517
+ }): string {
518
+ const clientResponses = responseEntries(clientRequests, generatedSchemaNames)
519
+ const serverResponses = responseEntries(serverRequests, generatedSchemaNames)
520
+ return [
521
+ '/* eslint-disable style/indent, style/member-delimiter-style, style/quotes, style/semi, style/quote-props */',
522
+ '// This file is generated by scripts/generate-codex-protocol.ts. Do not edit manually.',
523
+ `// Upstream Codex protocol ref: ${upstreamRef}`,
524
+ '',
525
+ 'import type * as CodexSchema from "./schema.gen.js";',
526
+ '',
527
+ renderMethodConstants('CLIENT_REQUEST_METHODS', clientRequests),
528
+ renderMethodConstants('CLIENT_NOTIFICATION_METHODS', clientNotifications),
529
+ renderMethodConstants('SERVER_REQUEST_METHODS', serverRequests),
530
+ renderMethodConstants('SERVER_NOTIFICATION_METHODS', serverNotifications),
531
+ 'export type ClientRequestMethod = keyof typeof CLIENT_REQUEST_METHODS;',
532
+ 'export type ClientNotificationMethod = keyof typeof CLIENT_NOTIFICATION_METHODS;',
533
+ 'export type ServerRequestMethod = keyof typeof SERVER_REQUEST_METHODS;',
534
+ 'export type ServerNotificationMethod = keyof typeof SERVER_NOTIFICATION_METHODS;',
535
+ '',
536
+ renderTypeInterface('ClientRequestParamsByMethod', clientRequests, entry =>
537
+ resolveParamsTypeReference(entry.paramsType ?? 'undefined', generatedSchemaNames)),
538
+ renderTypeInterface('ClientRequestResponsesByMethod', clientResponses, entry =>
539
+ renderSchemaTypeReference(entry.responseType)),
540
+ renderTypeInterface('ClientNotificationParamsByMethod', clientNotifications, entry =>
541
+ resolveParamsTypeReference(entry.paramsType ?? 'undefined', generatedSchemaNames)),
542
+ renderTypeInterface('ServerRequestParamsByMethod', serverRequests, entry =>
543
+ resolveParamsTypeReference(entry.paramsType ?? 'undefined', generatedSchemaNames)),
544
+ renderTypeInterface('ServerRequestResponsesByMethod', serverResponses, entry =>
545
+ renderSchemaTypeReference(entry.responseType)),
546
+ renderTypeInterface('ServerNotificationParamsByMethod', serverNotifications, entry =>
547
+ resolveParamsTypeReference(entry.paramsType ?? 'undefined', generatedSchemaNames)),
548
+ ].join('\n')
549
+ }
550
+
551
+ function renderNamespaces(upstreamRef: string, jsonSchemaFiles: JsonSchemaFile[]): string {
552
+ const namespaceGroups = new Map<string, JsonSchemaFile[]>()
553
+ for (const file of jsonSchemaFiles) {
554
+ if (!file.namespace)
555
+ continue
556
+ const current = namespaceGroups.get(file.namespace) ?? []
557
+ current.push(file)
558
+ namespaceGroups.set(file.namespace, current)
559
+ }
560
+
561
+ return [
562
+ '/* eslint-disable style/indent, style/quotes, style/semi, style/quote-props */',
563
+ '// This file is generated by scripts/generate-codex-protocol.ts. Do not edit manually.',
564
+ `// Upstream Codex protocol ref: ${upstreamRef}`,
565
+ '',
566
+ ...[...namespaceGroups.entries()]
567
+ .sort(([left], [right]) => left.localeCompare(right))
568
+ .map(([namespace, files]) => [
569
+ `export const ${namespace.replace(/[^A-Z0-9]/gi, '')} = {`,
570
+ ...files
571
+ .sort((left, right) => left.fileName.localeCompare(right.fileName))
572
+ .map(file => ` ${JSON.stringify(file.fileName.replace(/\.json$/, ''))}: ${JSON.stringify(file.exportName)},`),
573
+ '} as const;',
574
+ '',
575
+ ].join('\n')),
576
+ ].join('\n')
577
+ }
578
+
579
+ function responseEntries(requestEntries: MethodEntry[], generatedSchemaNames: Set<string>): ResponseEntry[] {
580
+ return requestEntries.map(entry => ({
581
+ method: entry.method,
582
+ responseType: resolveResponseTypeName(entry.method, entry.paramsType, generatedSchemaNames),
583
+ }))
584
+ }
585
+
586
+ function resolveSchemaTypeName(rawTypeName: string, generatedSchemaNames: Set<string>): string {
587
+ if (rawTypeName === 'undefined')
588
+ return 'undefined'
589
+ const resolved = schemaTypeCandidates(rawTypeName, false)
590
+ .find(candidate => generatedSchemaNames.has(candidate))
591
+ if (resolved)
592
+ return resolved
593
+ throw new Error(`Unable to resolve schema type name: ${rawTypeName}`)
594
+ }
595
+
596
+ function resolveParamsTypeReference(rawTypeName: string, generatedSchemaNames: Set<string>): string {
597
+ const parsed = parseSupportedParamsType(rawTypeName)
598
+ if (!parsed.typeName)
599
+ return 'undefined'
600
+
601
+ const candidates = parsed.includesNull
602
+ ? schemaTypeCandidates(parsed.typeName, true)
603
+ : [
604
+ ...schemaTypeCandidates(parsed.typeName, false),
605
+ ...(parsed.includesUndefined ? schemaTypeCandidates(parsed.typeName, true) : []),
606
+ ]
607
+ const schemaName = candidates.find(candidate => generatedSchemaNames.has(candidate))
608
+ if (!schemaName)
609
+ throw new Error(`Unable to resolve schema type name: ${rawTypeName}`)
610
+ const typeReference = renderSchemaTypeReference(schemaName)
611
+ return parsed.includesUndefined ? `${typeReference} | undefined` : typeReference
612
+ }
613
+
614
+ function schemaTypeCandidates(typeName: string, nullable: boolean): string[] {
615
+ return nullable
616
+ ? [`V2Nullable${typeName}`, `V1Nullable${typeName}`]
617
+ : [typeName, `V2${typeName}`, `V1${typeName}`, `SerdeJson${typeName}`]
618
+ }
619
+
620
+ function parseSupportedParamsType(rawTypeName: string): {
621
+ typeName: string | undefined
622
+ includesNull: boolean
623
+ includesUndefined: boolean
624
+ } {
625
+ const parts = rawTypeName.split(/\s*\|\s*/)
626
+ if (parts.some(part => !/^[A-Z_$][\w$]*$/i.test(part)))
627
+ throw new Error(`Unsupported Codex params type syntax: ${rawTypeName}`)
628
+ if (new Set(parts).size !== parts.length)
629
+ throw new Error(`Duplicate Codex params type member: ${rawTypeName}`)
630
+
631
+ const typeNames = parts.filter(part => part !== 'null' && part !== 'undefined')
632
+ if (typeNames.length > 1)
633
+ throw new Error(`Unsupported Codex params type union: ${rawTypeName}`)
634
+ const includesNull = parts.includes('null')
635
+ const includesUndefined = parts.includes('undefined')
636
+ if (typeNames.length === 0 && (includesNull || !includesUndefined))
637
+ throw new Error(`Unsupported Codex params type union: ${rawTypeName}`)
638
+ return { typeName: typeNames[0], includesNull, includesUndefined }
639
+ }
640
+
641
+ function resolveResponseTypeName(method: string, paramsType: string | undefined, generatedSchemaNames: Set<string>): string {
642
+ const overrides: { [method: string]: string } = {
643
+ 'account/logout': 'LogoutAccountResponse',
644
+ 'account/rateLimits/read': 'GetAccountRateLimitsResponse',
645
+ 'account/usage/read': 'GetAccountTokenUsageResponse',
646
+ 'account/workspaceMessages/read': 'GetWorkspaceMessagesResponse',
647
+ 'config/batchWrite': 'ConfigWriteResponse',
648
+ 'config/mcpServer/reload': 'McpServerRefreshResponse',
649
+ 'config/value/write': 'ConfigWriteResponse',
650
+ 'configRequirements/read': 'ConfigRequirementsReadResponse',
651
+ 'externalAgentConfig/import/readHistories': 'ExternalAgentConfigImportHistoriesReadResponse',
652
+ 'windowsSandbox/readiness': 'WindowsSandboxReadinessResponse',
653
+ }
654
+
655
+ const override = overrides[method]
656
+ if (override)
657
+ return resolveSchemaTypeName(override, generatedSchemaNames)
658
+
659
+ if (paramsType && paramsType !== 'undefined') {
660
+ try {
661
+ const responseType = parseSupportedParamsType(paramsType).typeName?.replace(/Params$/, 'Response')
662
+ if (responseType)
663
+ return resolveSchemaTypeName(responseType, generatedSchemaNames)
664
+ }
665
+ catch {}
666
+ }
667
+ return resolveSchemaTypeName(`${toPascalCaseMethod(method)}Response`, generatedSchemaNames)
668
+ }
669
+
670
+ function toPascalCaseMethod(method: string): string {
671
+ return method
672
+ .split('/')
673
+ .flatMap(segment => segment.split(/(?=[A-Z])/))
674
+ .flatMap(segment => segment.split(/[-_]/))
675
+ .filter(segment => segment.length > 0)
676
+ .map(toPascalSegment)
677
+ .join('')
678
+ }
679
+
680
+ function toPascalSegment(segment: string): string {
681
+ const first = segment[0]
682
+ if (!first)
683
+ return ''
684
+ return `${first.toUpperCase()}${segment.slice(1)}`
685
+ }
686
+
687
+ function renderMethodConstants(name: string, entries: MethodEntry[]): string {
688
+ return [
689
+ `export const ${name} = {`,
690
+ ...entries.map(entry => ` ${JSON.stringify(entry.method)}: ${JSON.stringify(entry.method)},`),
691
+ '} as const;',
692
+ '',
693
+ ].join('\n')
694
+ }
695
+
696
+ function renderTypeInterface<TEntry extends { method: string }>(name: string, entries: TEntry[], typeName: (entry: TEntry) => string): string {
697
+ return [
698
+ `export interface ${name} {`,
699
+ ...entries.map(entry => ` readonly ${JSON.stringify(entry.method)}: ${typeName(entry)};`),
700
+ '}',
701
+ '',
702
+ ].join('\n')
703
+ }
704
+
705
+ function renderSchemaTypeReference(schemaName: string): string {
706
+ return schemaName === 'undefined' ? 'undefined' : `CodexSchema.${schemaName}`
707
+ }
708
+
709
+ function isJsonObject(value: unknown): value is { [key: string]: unknown } {
710
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
711
+ }