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