incur 0.1.17 → 0.2.1
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.
- package/README.md +204 -9
- package/SKILL.md +173 -0
- package/dist/Cli.d.ts +39 -6
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +536 -43
- package/dist/Cli.js.map +1 -1
- package/dist/Errors.d.ts +4 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +3 -0
- package/dist/Errors.js.map +1 -1
- package/dist/Fetch.d.ts +26 -0
- package/dist/Fetch.d.ts.map +1 -0
- package/dist/Fetch.js +150 -0
- package/dist/Fetch.js.map +1 -0
- package/dist/Filter.d.ts +14 -0
- package/dist/Filter.d.ts.map +1 -0
- package/dist/Filter.js +134 -0
- package/dist/Filter.js.map +1 -0
- package/dist/Help.js +2 -0
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +26 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +2 -2
- package/dist/Mcp.js.map +1 -1
- package/dist/Openapi.d.ts +20 -0
- package/dist/Openapi.d.ts.map +1 -0
- package/dist/Openapi.js +136 -0
- package/dist/Openapi.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +8 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +4 -1
- package/src/Cli.test-d.ts +27 -2
- package/src/Cli.test.ts +1007 -0
- package/src/Cli.ts +676 -47
- package/src/Errors.ts +5 -0
- package/src/Fetch.test.ts +274 -0
- package/src/Fetch.ts +170 -0
- package/src/Filter.test.ts +237 -0
- package/src/Filter.ts +139 -0
- package/src/Help.test.ts +14 -0
- package/src/Help.ts +2 -0
- package/src/Mcp.ts +3 -3
- package/src/Openapi.test.ts +320 -0
- package/src/Openapi.ts +196 -0
- package/src/e2e.test.ts +778 -0
- package/src/index.ts +3 -0
- package/src/middleware.ts +9 -2
package/src/Openapi.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { dereference } from '@readme/openapi-parser'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import * as Fetch from './Fetch.js'
|
|
5
|
+
|
|
6
|
+
/** A minimal OpenAPI 3.x spec shape. Accepts both hand-written specs and generated ones (e.g. from `@hono/zod-openapi`). */
|
|
7
|
+
export type OpenAPISpec = { paths?: {} | undefined }
|
|
8
|
+
|
|
9
|
+
/** Internal operation shape after casting. */
|
|
10
|
+
type Operation = {
|
|
11
|
+
description?: string | undefined
|
|
12
|
+
operationId?: string | undefined
|
|
13
|
+
parameters?: readonly Parameter[] | undefined
|
|
14
|
+
requestBody?: RequestBody | undefined
|
|
15
|
+
responses?: Record<string, unknown> | undefined
|
|
16
|
+
summary?: string | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Parameter = {
|
|
20
|
+
description?: string | undefined
|
|
21
|
+
in: 'cookie' | 'header' | 'path' | 'query'
|
|
22
|
+
name: string
|
|
23
|
+
required?: boolean | undefined
|
|
24
|
+
schema?: Record<string, unknown> | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type RequestBody = {
|
|
28
|
+
content?: Record<string, { schema?: Record<string, unknown> | undefined }> | undefined
|
|
29
|
+
required?: boolean | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A fetch handler. */
|
|
33
|
+
type FetchHandler = (req: Request) => Response | Promise<Response>
|
|
34
|
+
|
|
35
|
+
/** A generated command entry compatible with incur's internal CommandEntry. */
|
|
36
|
+
type GeneratedCommand = {
|
|
37
|
+
args?: z.ZodObject<any> | undefined
|
|
38
|
+
description?: string | undefined
|
|
39
|
+
options?: z.ZodObject<any> | undefined
|
|
40
|
+
run: (context: any) => any
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Generates incur command entries from an OpenAPI spec. Resolves all `$ref` pointers. */
|
|
44
|
+
export async function generateCommands(
|
|
45
|
+
spec: OpenAPISpec,
|
|
46
|
+
fetch: FetchHandler,
|
|
47
|
+
options: { basePath?: string | undefined } = {},
|
|
48
|
+
): Promise<Map<string, GeneratedCommand>> {
|
|
49
|
+
const resolved = await dereference(structuredClone(spec) as any) as unknown as OpenAPISpec
|
|
50
|
+
const commands = new Map<string, GeneratedCommand>()
|
|
51
|
+
const paths = (resolved.paths ?? {}) as Record<string, Record<string, unknown>>
|
|
52
|
+
|
|
53
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
54
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
55
|
+
if (method.startsWith('x-')) continue
|
|
56
|
+
const op = operation as Operation
|
|
57
|
+
const name = op.operationId ?? `${method}_${path.replace(/[/{}]/g, '_')}`
|
|
58
|
+
const httpMethod = method.toUpperCase()
|
|
59
|
+
|
|
60
|
+
const pathParams = (op.parameters ?? []).filter((p) => p.in === 'path')
|
|
61
|
+
const queryParams = (op.parameters ?? []).filter((p) => p.in === 'query')
|
|
62
|
+
|
|
63
|
+
const bodySchema = op.requestBody?.content?.['application/json']?.schema
|
|
64
|
+
const bodyProps = (bodySchema?.properties ?? {}) as Record<string, Record<string, unknown>>
|
|
65
|
+
const bodyRequired = new Set((bodySchema?.required as string[]) ?? [])
|
|
66
|
+
|
|
67
|
+
// Build args Zod schema from path params
|
|
68
|
+
let argsSchema: z.ZodObject<any> | undefined
|
|
69
|
+
if (pathParams.length > 0) {
|
|
70
|
+
const shape: Record<string, z.ZodType> = {}
|
|
71
|
+
for (const p of pathParams) {
|
|
72
|
+
let zodType = p.schema ? toZod(p.schema) : z.string()
|
|
73
|
+
if (p.description) zodType = zodType.describe(p.description)
|
|
74
|
+
// Path params need coercion from string argv
|
|
75
|
+
shape[p.name] = coerceIfNeeded(zodType)
|
|
76
|
+
}
|
|
77
|
+
argsSchema = z.object(shape)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build options Zod schema from query params + body properties
|
|
81
|
+
const optShape: Record<string, z.ZodType> = {}
|
|
82
|
+
for (const p of queryParams) {
|
|
83
|
+
let zodType = p.schema ? toZod(p.schema) : z.string()
|
|
84
|
+
if (!p.required) zodType = zodType.optional()
|
|
85
|
+
if (p.description) zodType = zodType.describe(p.description)
|
|
86
|
+
optShape[p.name] = coerceIfNeeded(zodType)
|
|
87
|
+
}
|
|
88
|
+
for (const [key, schema] of Object.entries(bodyProps)) {
|
|
89
|
+
let zodType = toZod(schema)
|
|
90
|
+
if (!bodyRequired.has(key)) zodType = zodType.optional()
|
|
91
|
+
optShape[key] = zodType
|
|
92
|
+
}
|
|
93
|
+
const optionsSchema = Object.keys(optShape).length > 0 ? z.object(optShape) : undefined
|
|
94
|
+
|
|
95
|
+
commands.set(name, {
|
|
96
|
+
description: op.summary ?? op.description,
|
|
97
|
+
args: argsSchema,
|
|
98
|
+
options: optionsSchema,
|
|
99
|
+
run: createHandler({ basePath: options.basePath, fetch, httpMethod, path, pathParams, queryParams, bodyProps }),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return commands
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createHandler(config: {
|
|
108
|
+
basePath?: string | undefined
|
|
109
|
+
bodyProps: Record<string, Record<string, unknown>>
|
|
110
|
+
fetch: FetchHandler
|
|
111
|
+
httpMethod: string
|
|
112
|
+
path: string
|
|
113
|
+
pathParams: Parameter[]
|
|
114
|
+
queryParams: Parameter[]
|
|
115
|
+
}) {
|
|
116
|
+
return async (context: any) => {
|
|
117
|
+
const { args = {}, options = {} } = context
|
|
118
|
+
|
|
119
|
+
// Build URL path with interpolated path params
|
|
120
|
+
let urlPath = (config.basePath ?? '') + config.path
|
|
121
|
+
for (const p of config.pathParams) {
|
|
122
|
+
const value = args[p.name]
|
|
123
|
+
if (value !== undefined) urlPath = urlPath.replace(`{${p.name}}`, String(value))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build query string from query params
|
|
127
|
+
const query = new URLSearchParams()
|
|
128
|
+
for (const p of config.queryParams) {
|
|
129
|
+
const value = options[p.name]
|
|
130
|
+
if (value !== undefined) query.set(p.name, String(value))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Build body from body properties
|
|
134
|
+
let body: string | undefined
|
|
135
|
+
const bodyKeys = Object.keys(config.bodyProps)
|
|
136
|
+
if (bodyKeys.length > 0) {
|
|
137
|
+
const bodyObj: Record<string, unknown> = {}
|
|
138
|
+
for (const key of bodyKeys)
|
|
139
|
+
if (options[key] !== undefined) bodyObj[key] = options[key]
|
|
140
|
+
if (Object.keys(bodyObj).length > 0) body = JSON.stringify(bodyObj)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const input: Fetch.FetchInput = {
|
|
144
|
+
path: urlPath,
|
|
145
|
+
method: config.httpMethod,
|
|
146
|
+
headers: new Headers(),
|
|
147
|
+
body,
|
|
148
|
+
query,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (body) input.headers.set('content-type', 'application/json')
|
|
152
|
+
|
|
153
|
+
const request = Fetch.buildRequest(input)
|
|
154
|
+
const response = await config.fetch(request)
|
|
155
|
+
const output = await Fetch.parseResponse(response)
|
|
156
|
+
|
|
157
|
+
if (!output.ok)
|
|
158
|
+
return context.error({
|
|
159
|
+
code: `HTTP_${output.status}`,
|
|
160
|
+
message:
|
|
161
|
+
typeof output.data === 'object' && output.data !== null && 'message' in output.data
|
|
162
|
+
? String((output.data as any).message)
|
|
163
|
+
: typeof output.data === 'string'
|
|
164
|
+
? output.data
|
|
165
|
+
: `HTTP ${output.status}`,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return output.data
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Converts a JSON Schema object to a Zod schema. */
|
|
173
|
+
function toZod(schema: Record<string, unknown>): z.ZodType {
|
|
174
|
+
return z.fromJSONSchema(schema)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Wraps a Zod schema with coercion if the base type is number or boolean (argv is always strings). */
|
|
178
|
+
function coerceIfNeeded(schema: z.ZodType): z.ZodType {
|
|
179
|
+
const isOptional = schema instanceof z.ZodOptional
|
|
180
|
+
const inner = isOptional ? schema.unwrap() : schema
|
|
181
|
+
|
|
182
|
+
// Direct number/boolean
|
|
183
|
+
if (inner instanceof z.ZodNumber) return isOptional ? z.coerce.number().optional() : z.coerce.number()
|
|
184
|
+
if (inner instanceof z.ZodBoolean) return isOptional ? z.coerce.boolean().optional() : z.coerce.boolean()
|
|
185
|
+
|
|
186
|
+
// Union containing number (e.g. type: ["number", "null"] from OpenAPI 3.1)
|
|
187
|
+
if (inner instanceof z.ZodUnion) {
|
|
188
|
+
const options = (inner as any)._zod?.def?.options as z.ZodType[] | undefined
|
|
189
|
+
if (options?.some((o: z.ZodType) => o instanceof z.ZodNumber))
|
|
190
|
+
return isOptional ? z.coerce.number().optional() : z.coerce.number()
|
|
191
|
+
if (options?.some((o: z.ZodType) => o instanceof z.ZodBoolean))
|
|
192
|
+
return isOptional ? z.coerce.boolean().optional() : z.coerce.boolean()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return schema
|
|
196
|
+
}
|