diff --git a/examples/ai-sdk/package.json b/examples/ai-sdk/package.json index 5a36623..cc961f1 100644 --- a/examples/ai-sdk/package.json +++ b/examples/ai-sdk/package.json @@ -13,7 +13,7 @@ "@agentic/weather": "workspace:*", "@ai-sdk/openai": "catalog:", "ai": "catalog:", - "exit-hook": "^4.0.0", + "exit-hook": "catalog:", "openai": "catalog:", "zod": "catalog:" }, diff --git a/packages/openapi-to-ts/bin/generate-from-openapi.ts b/packages/openapi-to-ts/bin/generate-from-openapi.ts deleted file mode 100644 index daedcea..0000000 --- a/packages/openapi-to-ts/bin/generate-from-openapi.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable no-template-curly-in-string */ -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { assert } from '@agentic/core' - -import { generateTSFromOpenAPI } from '../src' - -const dirname = path.dirname(fileURLToPath(import.meta.url)) - -// TODO: Add proper CLI handling -async function main() { - const pathToOpenApiSpec = - process.argv[2] ?? - path.join(dirname, '..', 'fixtures', 'openapi', '3.0', 'notion.json') - assert(pathToOpenApiSpec, 'Missing path to OpenAPI spec') - - await generateTSFromOpenAPI(pathToOpenApiSpec) -} - -await main() diff --git a/packages/openapi-to-ts/bin/openapi-to-ts.ts b/packages/openapi-to-ts/bin/openapi-to-ts.ts new file mode 100644 index 0000000..c76063b --- /dev/null +++ b/packages/openapi-to-ts/bin/openapi-to-ts.ts @@ -0,0 +1,72 @@ +import { cli } from 'cleye' +import { gracefulExit } from 'exit-hook' + +import { generateTSFromOpenAPI } from '../src' + +async function main() { + const args = cli( + { + name: 'openapi-to-ts', + parameters: [''], + flags: { + debug: { + type: Boolean, + description: 'Enables verbose debug logging', + alias: 'v', + default: false + }, + outputDir: { + type: String, + description: 'Path to the output directory (defaults to cwd)', + alias: 'o' + }, + dryRun: { + type: Boolean, + description: 'Disables all side effects', + default: false + }, + noPrettier: { + type: Boolean, + description: 'Disables prettier formatting', + default: false + }, + noEslint: { + type: Boolean, + description: 'Disables eslint formatting', + default: false + } + } + }, + () => {}, + process.argv + ) + + const openapiFilePath = args._[2]! + + if (!openapiFilePath) { + console.error('Missing required argument: \n') + args.showHelp() + gracefulExit(1) + return + } + + const output = await generateTSFromOpenAPI({ + openapiFilePath, + outputDir: args.flags.outputDir || process.cwd(), + dryRun: args.flags.dryRun, + prettier: !args.flags.noPrettier, + eslint: !args.flags.noEslint + }) + + if (args.flags.dryRun) { + console.log(output) + } +} + +try { + await main() + gracefulExit(0) +} catch (err) { + console.error(err) + gracefulExit(1) +} diff --git a/packages/openapi-to-ts/package.json b/packages/openapi-to-ts/package.json index 73fbf8a..16607cb 100644 --- a/packages/openapi-to-ts/package.json +++ b/packages/openapi-to-ts/package.json @@ -1,7 +1,7 @@ { "name": "@agentic/openapi-to-ts", "version": "0.1.0", - "description": "TODO", + "description": "Generate an Agentic TypeScript client from an OpenAPI spec.", "author": "Travis Fischer ", "license": "MIT", "repository": { @@ -13,7 +13,7 @@ "types": "./dist/index.d.ts", "sideEffects": false, "bin": { - "openapi-to-ts": "./dist/generate-from-openapi.js" + "openapi-to-ts": "./dist/openapi-to-ts.js" }, "files": [ "dist" @@ -30,15 +30,16 @@ "@agentic/core": "workspace:*", "@apidevtools/swagger-parser": "^10.1.1", "camelcase": "^8.0.0", + "cleye": "catalog:", "decamelize": "^6.0.0", "execa": "^9.5.2", + "exit-hook": "catalog:", "json-schema-to-zod": "^2.6.0", "openapi-types": "^12.1.3", "zod": "catalog:" }, "devDependencies": { - "@agentic/tsconfig": "workspace:*", - "ky": "catalog:" + "@agentic/tsconfig": "workspace:*" }, "publishConfig": { "access": "public" diff --git a/packages/openapi-to-ts/readme.md b/packages/openapi-to-ts/readme.md index 776c440..1371248 100644 --- a/packages/openapi-to-ts/readme.md +++ b/packages/openapi-to-ts/readme.md @@ -5,7 +5,7 @@

- TODO. + Generate an Agentic TypeScript client from an OpenAPI spec.

@@ -19,15 +19,36 @@ **See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.** +## Example Usage + +```sh +# local development +tsx bin/openapi-to-ts.ts fixtures/openapi/3.0/notion.json -o fixtures/generated + +# published version +npx @agentic/openapi-to-ts fixtures/openapi/3.0/notion.json -o fixtures/generated + +# npm install -g version +npm install -g @agentic/openapi-to-ts +openapi-to-ts fixtures/openapi/3.0/notion.json -o fixtures/generated +``` + +Most OpenAPI specs should be supported, but I've made no attempt to maximize robustness. This tool is meant to generate Agentic TS clients which are 99% of the way there, but some tweaking may be necessary post-generation. + +Some things you may want to tweak: + +- simplifying the zod parameters accepted by `@aiFunction` input schemas since LLMs tend to do better with a few, key parameters +- removing unused API endpoints & associated types that you don't want to expose to LLMs + ## TODO -- convert to https://github.com/readmeio/oas +- convert openapi parsing & utils to https://github.com/readmeio/oas - support filters - // match only the schema named `foo` and `GET` operation for the `/api/v1/foo` path - include: '^(#/components/schemas/foo|#/paths/api/v1/foo/get)$', -- [ ] Convert HTML in descriptions to markdown. -- [ ] Properly format multiline function comments. -- [ ] Debug stripe schema issue. +- [ ] Convert HTML in descriptions to markdown +- [ ] Properly format multiline function comments +- [ ] Debug stripe schema issue ## License diff --git a/packages/openapi-to-ts/src/generate-ts-from-openapi.ts b/packages/openapi-to-ts/src/generate-ts-from-openapi.ts index fccb485..695738c 100644 --- a/packages/openapi-to-ts/src/generate-ts-from-openapi.ts +++ b/packages/openapi-to-ts/src/generate-ts-from-openapi.ts @@ -1,7 +1,6 @@ /* eslint-disable no-template-curly-in-string */ import * as fs from 'node:fs/promises' import path from 'node:path' -import { fileURLToPath } from 'node:url' import type { IJsonSchema, OpenAPIV3 } from 'openapi-types' import { assert } from '@agentic/core' @@ -16,7 +15,6 @@ import { dereferenceFull, getAndResolve, getComponentDisplayName, - getComponentName, getDescription, getOperationParamsName, getOperationResponseName, @@ -26,7 +24,6 @@ import { prettify } from './utils' -const dirname = path.dirname(fileURLToPath(import.meta.url)) const jsonContentType = 'application/json' const multipartFormData = 'multipart/form-data' @@ -41,7 +38,21 @@ const httpMethods = [ 'trace' ] as const -export async function generateTSFromOpenAPI(openapiFilePath: string) { +export type GenerateTSFromOpenAPIOptions = { + openapiFilePath: string + outputDir: string + dryRun?: boolean + prettier?: boolean + eslint?: boolean +} + +export async function generateTSFromOpenAPI({ + openapiFilePath, + outputDir, + dryRun = false, + prettier = true, + eslint = true +}: GenerateTSFromOpenAPIOptions): Promise { const parser = new SwaggerParser() const spec = (await parser.bundle(openapiFilePath)) as OpenAPIV3.Document // | OpenAPIV3_1.Document @@ -68,12 +79,8 @@ export async function generateTSFromOpenAPI(openapiFilePath: string) { const nameUpperCase = nameSnakeCase.toUpperCase() const clientName = `${name}Client` const namespaceName = nameLowerCase - // const destFolder = path.join('packages', nameKebabCase) - // const destFolderSrc = path.join(destFolder, 'src') - - const destFolder = path.join(dirname, '..', 'fixtures', 'generated') - const destFileClient = path.join(destFolder, `${nameKebabCase}-client.ts`) + const destFileClient = path.join(outputDir, `${nameKebabCase}-client.ts`) const apiBaseUrl = spec.servers?.[0]?.url const securitySchemes = spec.components?.securitySchemes @@ -543,14 +550,14 @@ export async function generateTSFromOpenAPI(openapiFilePath: string) { componentSchemas[type] = schema } - console.log( - '\ncomponents', - JSON.stringify( - sortedComponents.map((ref) => getComponentName(ref)), - null, - 2 - ) - ) + // console.log( + // '\ncomponents', + // JSON.stringify( + // sortedComponents.map((ref) => getComponentName(ref)), + // null, + // 2 + // ) + // ) // console.log( // '\nmodels', @@ -603,9 +610,10 @@ import { z } from 'zod'`.trim() .join('\n\n') const description = getDescription(spec.info?.description) + const prettifyImpl = prettier ? prettify : (code: string) => code const output = ( - await prettify( + await prettifyImpl( [ outputTypes, ` @@ -659,8 +667,16 @@ export class ${clientName} extends AIFunctionsProvider { .replaceAll(/z\s*\.object\({}\)\s*\.merge\(([^)]*)\)/gm, '$1') .replaceAll(/\/\*\*(\S.*\S)\*\//g, '/** $1 */') - console.log(output) - await fs.mkdir(destFolder, { recursive: true }) + if (dryRun) { + return output + } + + await fs.mkdir(outputDir, { recursive: true }) await fs.writeFile(destFileClient, output) - await execa('npx', ['eslint', '--fix', '--no-ignore', destFileClient]) + + if (eslint) { + await execa('npx', ['eslint', '--fix', '--no-ignore', destFileClient]) + } + + return output } diff --git a/packages/openapi-to-ts/src/openapi-parameters-to-json-schema.ts b/packages/openapi-to-ts/src/openapi-parameters-to-json-schema.ts index 3f5e753..4cc59a0 100644 --- a/packages/openapi-to-ts/src/openapi-parameters-to-json-schema.ts +++ b/packages/openapi-to-ts/src/openapi-parameters-to-json-schema.ts @@ -1,6 +1,8 @@ /** * This file is forked from: https://github.com/kogosoftwarellc/open-api/tree/main/packages/openapi-jsonschema-parameters * + * Several fixes have been applied. + * * The original code is licensed under the MIT license. */ diff --git a/packages/openapi-to-ts/tsup.config.ts b/packages/openapi-to-ts/tsup.config.ts index 8c59256..b523b60 100644 --- a/packages/openapi-to-ts/tsup.config.ts +++ b/packages/openapi-to-ts/tsup.config.ts @@ -14,7 +14,7 @@ export default defineConfig([ dts: true }, { - entry: ['bin/generate-from-openapi.ts'], + entry: ['bin/openapi-to-ts.ts'], outDir: 'dist', target: 'node18', platform: 'node', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b72978b..9c7fa36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,12 +39,18 @@ catalogs: ai: specifier: ^4.1.61 version: 4.1.61 + cleye: + specifier: ^1.3.4 + version: 1.3.4 dedent: specifier: ^1.5.3 version: 1.5.3 delay: specifier: ^6.0.0 version: 6.0.0 + exit-hook: + specifier: ^4.0.0 + version: 4.0.0 fast-xml-parser: specifier: ^5.0.9 version: 5.0.9 @@ -189,7 +195,7 @@ importers: specifier: 'catalog:' version: 4.1.61(react@18.3.1)(zod@3.24.2) exit-hook: - specifier: ^4.0.0 + specifier: 'catalog:' version: 4.0.0 openai: specifier: 'catalog:' @@ -891,12 +897,18 @@ importers: camelcase: specifier: ^8.0.0 version: 8.0.0 + cleye: + specifier: 'catalog:' + version: 1.3.4 decamelize: specifier: ^6.0.0 version: 6.0.0 execa: specifier: ^9.5.2 version: 9.5.2 + exit-hook: + specifier: 'catalog:' + version: 4.0.0 json-schema-to-zod: specifier: ^2.6.0 version: 2.6.0 @@ -910,9 +922,6 @@ importers: '@agentic/tsconfig': specifier: workspace:* version: link:../tsconfig - ky: - specifier: 'catalog:' - version: 1.7.5 packages/people-data-labs: dependencies: @@ -3873,6 +3882,9 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} + cleye@1.3.4: + resolution: {integrity: sha512-Rd6M8ecBDtdYdPR22h6gG37lPqqJ3hSOaplaGwuGYey9xKmEElOvTgupqfyLSlISshroRpVhYjDtW3vwNUNBaQ==} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -6494,6 +6506,9 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + terminal-columns@1.4.1: + resolution: {integrity: sha512-IKVL/itiMy947XWVv4IHV7a0KQXvKjj4ptbi7Ew9MPMcOLzkiQeyx3Gyvh62hKrfJ0RZc4M1nbhzjNM39Kyujw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -6671,6 +6686,9 @@ packages: resolution: {integrity: sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==} engines: {node: '>=16'} + type-flag@3.0.0: + resolution: {integrity: sha512-3YaYwMseXCAhBB14RXW5cRQfJQlEknS6i4C8fCfeUdS3ihG9EdccdR9kt3vP73ZdeTGmPb4bZtkDn5XMIn1DLA==} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -10214,6 +10232,11 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + cleye@1.3.4: + dependencies: + terminal-columns: 1.4.1 + type-flag: 3.0.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -13109,6 +13132,8 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + terminal-columns@1.4.1: {} + text-table@0.2.0: {} thenify-all@1.6.0: @@ -13266,6 +13291,8 @@ snapshots: type-fest@4.37.0: {} + type-flag@3.0.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3149e95..8f7c7f4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,8 +7,10 @@ catalog: zod: ^3.24.2 zod-validation-error: ^3.4.0 openai-zod-to-json-schema: ^1.0.3 + cleye: ^1.3.4 dedent: ^1.5.3 delay: ^6.0.0 + exit-hook: ^4.0.0 jsonrepair: ^3.9.0 jsrsasign: ^10.9.0 mathjs: ^13.0.3