917 lines · 28.3 KB · TypeScript
RawDownload
#!/usr/bin/env node import * as NodeRuntime from "@effect/platform-node/NodeRuntime";import * as NodeServices from "@effect/platform-node/NodeServices";import { make as makeJsonSchemaGenerator } from "@effect/openapi-generator/JsonSchemaGenerator";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import {  FetchHttpClient,  HttpClient,  HttpClientRequest,  HttpClientResponse,} from "effect/unstable/http";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; const UPSTREAM_REF = "678157acaa819d5510adfe359abb5d0392cfe461";const USER_AGENT = "effect-codex-app-server-generator";const GITHUB_API_BASE =  "https://api.github.com/repos/openai/codex/contents/codex-rs/app-server-protocol"; const GithubContentEntries = Schema.Array(  Schema.Struct({    name: Schema.String,    path: Schema.String,    download_url: Schema.NullOr(Schema.String),    type: Schema.String,  }),);type GithubContentEntry = (typeof GithubContentEntries.Type)[number]; const JsonSchemaDocument = Schema.StructWithRest(  Schema.Struct({    definitions: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)),  }),  [Schema.Record(Schema.String, Schema.Json)],);const decodeGithubContentEntries = Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries));const decodeJsonSchemaDocument = Schema.decodeEffect(Schema.fromJsonString(JsonSchemaDocument)); interface GeneratedPaths {  readonly generatedDir: string;  readonly schemaOutputPath: string;  readonly metaOutputPath: string;  readonly namespacesOutputPath: string;} interface MethodEntry {  readonly method: string;  readonly paramsType?: string;} interface JsonSchemaFile {  readonly namespace?: string;  readonly exportName: string;  readonly fileName: string;  readonly downloadUrl: string;  readonly qualifiedName: string;} class GeneratorError extends Schema.TaggedErrorClass<GeneratorError>()("GeneratorError", {  detail: Schema.String,  cause: Schema.optional(Schema.Defect()),}) {  override get message() {    return this.detail;  }} const ManualSchemas: Record<string, Schema.Json> = {  GetAuthStatusParams: {    type: "object",    title: "GetAuthStatusParams",    properties: {      includeToken: {        anyOf: [{ type: "boolean" }, { type: "null" }],      },      refreshToken: {        anyOf: [{ type: "boolean" }, { type: "null" }],      },    },  },  GetConversationSummaryParams: {    title: "GetConversationSummaryParams",    oneOf: [      {        type: "object",        properties: {          rolloutPath: { type: "string" },        },        required: ["rolloutPath"],      },      {        type: "object",        properties: {          conversationId: { type: "string" },        },        required: ["conversationId"],      },    ],  },  GetConversationSummaryResponse: {    type: "object",    title: "GetConversationSummaryResponse",    properties: {      summary: {},    },    required: ["summary"],  },  GitDiffToRemoteParams: {    type: "object",    title: "GitDiffToRemoteParams",    properties: {      cwd: { type: "string" },    },    required: ["cwd"],  },  GitDiffToRemoteResponse: {    type: "object",    title: "GitDiffToRemoteResponse",    properties: {      sha: { type: "string" },      diff: { type: "string" },    },    required: ["sha", "diff"],  },  GetAuthStatusResponse: {    type: "object",    title: "GetAuthStatusResponse",    properties: {      authMethod: {        anyOf: [{}, { type: "null" }],      },      authToken: {        anyOf: [{ type: "string" }, { type: "null" }],      },      requiresOpenaiAuth: {        anyOf: [{ type: "boolean" }, { type: "null" }],      },    },    required: ["authMethod", "authToken", "requiresOpenaiAuth"],  },}; // Codex 0.150 added these multi-agent values before our next full protocol// refresh. Keep every generated response namespace compatible with them.const Codex0150DefinitionSchemas: Record<string, Schema.Json> = {  CollabAgentTool: {    type: "string",    enum: [      "spawnAgent",      "sendInput",      "resumeAgent",      "wait",      "closeAgent",      "sendMessage",      "followupTask",      "interruptAgent",      "listAgents",    ],  },  CollabAgentToolCallStatus: {    type: "string",    enum: ["inProgress", "completed", "failed", "interrupted"],  },  PlanType: {    type: "string",    enum: [      "free",      "go",      "plus",      "pro",      "prolite",      "team",      "self_serve_business_prolite",      "self_serve_business_usage_based",      "business",      "ent26",      "enterprise_cbp_automation",      "enterprise_cbp_usage_based",      "enterprise",      "edu",      "edu_plus",      "edu_pro",      "unknown",    ],  },  SubAgentActivityKind: {    type: "string",    enum: ["started", "interacted", "interrupted", "completed"],  },}; function applyCodex0151DefinitionCompatibility(  exportName: string,  definitionName: string,  definitionSchema: Schema.Json,): Schema.Json {  const isThreadResponse =    exportName === "V2ThreadReadResponse" ||    exportName === "V2ThreadResumeResponse" ||    exportName === "V2ThreadRollbackResponse";  if (    !isThreadResponse ||    definitionName !== "CodexErrorInfo" ||    typeof definitionSchema !== "object"  ) {    return definitionSchema;  }   const schema = definitionSchema as {    readonly oneOf?: ReadonlyArray<{ readonly enum?: ReadonlyArray<string> }>;  };  const [firstVariant, ...remainingVariants] = schema.oneOf ?? [];  if (!firstVariant?.enum || firstVariant.enum.includes("rateLimitExceeded")) {    return definitionSchema;  }   return {    ...definitionSchema,    oneOf: [      { ...firstVariant, enum: [...firstVariant.enum, "rateLimitExceeded"] },      ...remainingVariants,    ],  };} const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () {  const path = yield* Path.Path;  const generatedDir = path.join(import.meta.dirname, "..", "src", "_generated");  return {    generatedDir,    schemaOutputPath: path.join(generatedDir, "schema.gen.ts"),    metaOutputPath: path.join(generatedDir, "meta.gen.ts"),    namespacesOutputPath: path.join(generatedDir, "namespaces.gen.ts"),  } satisfies GeneratedPaths;}); const ensureGeneratedDir = Effect.fn("ensureGeneratedDir")(function* () {  const fs = yield* FileSystem.FileSystem;  const { generatedDir } = yield* getGeneratedPaths();  yield* fs.makeDirectory(generatedDir, { recursive: true });}); const fetchText = Effect.fn("fetchText")(function* (url: string) {  return yield* HttpClientRequest.get(url).pipe(    HttpClientRequest.setHeader("user-agent", USER_AGENT),    HttpClient.execute,    Effect.flatMap(HttpClientResponse.filterStatusOk),    Effect.flatMap((okResponse) => okResponse.text),    Effect.mapError(      (cause) =>        new GeneratorError({          detail: `Failed to fetch ${url}`,          cause,        }),    ),  );}); const fetchDirectoryEntries = Effect.fn("fetchDirectoryEntries")(function* (path: string) {  const raw = yield* fetchText(`${GITHUB_API_BASE}/${path}?ref=${UPSTREAM_REF}`);  return yield* decodeGithubContentEntries(raw);}); function collectSchemaEntries(  chunk: string,): ReadonlyArray<{ readonly name: string; readonly code: string }> {  const lines = chunk    .split("\n")    .map((line) => line.trim())    .filter((line) => line.length > 0 && !line.startsWith("//"));  const entries: Array<{ name: string; code: string }> = [];   for (let index = 0; index < lines.length; index += 1) {    const typeLine = lines[index];    if (!typeLine?.startsWith("export type ")) {      continue;    }     const constLine = lines[index + 1];    if (!constLine?.startsWith("export const ")) {      throw new Error(`Malformed generator output near: ${typeLine}`);    }     const match = /^export type ([A-Za-z0-9_]+)/.exec(typeLine);    if (!match?.[1]) {      throw new Error(`Could not extract schema name from: ${typeLine}`);    }     entries.push({      name: match[1],      code: `${typeLine}\n${constLine}`,    });    index += 1;  }   return entries;} function normalizeNullableTypes(value: Schema.Json): Schema.Json {  if (Array.isArray(value)) {    return value.map(normalizeNullableTypes);  }  if (value === null || typeof value !== "object") {    return value;  }   const normalizedEntries = Object.entries(value).map(([key, child]) => [    key,    normalizeNullableTypes(child),  ]);  const normalizedObject = Object.fromEntries(normalizedEntries) as Record<string, Schema.Json>;  const typeValue = normalizedObject.type;   if (!Array.isArray(typeValue)) {    return normalizedObject;  }   const normalizedTypes = typeValue.filter((entry): entry is string => typeof entry === "string");  if (normalizedTypes.length !== typeValue.length || !normalizedTypes.includes("null")) {    return normalizedObject;  }   const nonNullTypes = normalizedTypes.filter((entry) => entry !== "null");  if (nonNullTypes.length !== 1) {    return normalizedObject;  }  const nonNullType = nonNullTypes[0]!;   const nextObject: Record<string, Schema.Json> = {};  for (const [key, child] of Object.entries(normalizedObject)) {    if (key !== "type") {      nextObject[key] = child;    }  }   return {    anyOf: [      {        ...nextObject,        type: nonNullType,      },      { type: "null" },    ],  };} function stripNullDefaults(value: Schema.Json): Schema.Json {  if (Array.isArray(value)) {    return value.map(stripNullDefaults);  }  if (value === null || typeof value !== "object") {    return value;  }   return Object.fromEntries(    Object.entries(value)      .filter(([key, child]) => !(key === "default" && child === null))      .map(([key, child]) => [key, stripNullDefaults(child)]),  ) as Schema.Json;} // Codex 0.153 adds async questions to agent messages. Keep older protocol// fields until the next full refresh, including every thread history namespace.function addAsyncQuestionFields(value: Schema.Json): Schema.Json {  if (Array.isArray(value)) {    return value.map(addAsyncQuestionFields);  }  if (value === null || typeof value !== "object") {    return value;  }  const properties = "properties" in value ? value.properties : undefined;  const itemType =    properties && typeof properties === "object" && "type" in properties      ? properties.type      : undefined;  if (    properties &&    typeof properties === "object" &&    itemType &&    typeof itemType === "object" &&    "enum" in itemType &&    Array.isArray(itemType.enum) &&    itemType.enum.includes("agentMessage")  ) {    return {      ...value,      properties: {        ...properties,        delivery: { anyOf: [{ type: "string", enum: ["async"] }, { type: "null" }] },        questions: {          anyOf: [            {              type: "array",              items: {                type: "object",                properties: {                  title: { type: "string" },                  options: {                    anyOf: [{ type: "array", items: { type: "string" } }, { type: "null" }],                  },                },                required: ["title"],              },            },            { type: "null" },          ],        },      },    };  }  return Object.fromEntries(    Object.entries(value).map(([key, child]) => [key, addAsyncQuestionFields(child)]),  );} function toPascalCaseMethod(method: string) {  return method    .split("/")    .flatMap((segment) => segment.split(/(?=[A-Z])/))    .flatMap((segment) => segment.split(/[-_]/))    .filter(Boolean)    .map((segment) => segment[0]!.toUpperCase() + segment.slice(1))    .join("");} function parseRequestEntries(fileContents: string): ReadonlyArray<MethodEntry> {  const entryPattern = /\{\s*"method":\s*"([^"]+)",\s*id:\s*RequestId,\s*params:\s*([^,}]+)/g;  const entries: Array<MethodEntry> = [];  let match: RegExpExecArray | null;  while ((match = entryPattern.exec(fileContents)) !== null) {    entries.push({      method: match[1]!,      paramsType: match[2]!.trim(),    });  }  return entries;} function parseNotificationEntries(fileContents: string): ReadonlyArray<MethodEntry> {  const entryPattern = /\{\s*"method":\s*"([^"]+)"(?:,\s*"params":\s*([^ }]+))?\s*\}/g;  const entries: Array<MethodEntry> = [];  let match: RegExpExecArray | null;  while ((match = entryPattern.exec(fileContents)) !== null) {    entries.push({      method: match[1]!,      ...(match[2] ? { paramsType: match[2].trim() } : {}),    });  }  return entries;} function resolveSchemaTypeName(  rawTypeName: string,  generatedSchemaNames: ReadonlySet<string>,): string {  if (rawTypeName === "undefined") {    return "undefined";  }   const candidates = [    rawTypeName,    `V2${rawTypeName}`,    `V1${rawTypeName}`,    `SerdeJson${rawTypeName}`,  ];  for (const candidate of candidates) {    if (generatedSchemaNames.has(candidate)) {      return candidate;    }  }   throw new Error(`Unable to resolve schema type name: ${rawTypeName}`);} function resolveResponseTypeName(  method: string,  paramsType: string | undefined,  generatedSchemaNames: ReadonlySet<string>,): string {  const overrides: Record<string, string> = {    "account/logout": "LogoutAccountResponse",    "account/rateLimits/read": "GetAccountRateLimitsResponse",    "account/usage/read": "GetAccountTokenUsageResponse",    "account/workspaceMessages/read": "GetWorkspaceMessagesResponse",    "config/batchWrite": "ConfigWriteResponse",    "config/mcpServer/reload": "McpServerRefreshResponse",    "config/value/write": "ConfigWriteResponse",    "configRequirements/read": "ConfigRequirementsReadResponse",    "externalAgentConfig/import/readHistories": "ExternalAgentConfigImportHistoriesReadResponse",  };   const override = overrides[method];  if (override) {    return resolveSchemaTypeName(override, generatedSchemaNames);  }

Lines 1–500 of 917.