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.
Files changed (52) hide show
  1. package/README.md +204 -9
  2. package/SKILL.md +173 -0
  3. package/dist/Cli.d.ts +39 -6
  4. package/dist/Cli.d.ts.map +1 -1
  5. package/dist/Cli.js +536 -43
  6. package/dist/Cli.js.map +1 -1
  7. package/dist/Errors.d.ts +4 -0
  8. package/dist/Errors.d.ts.map +1 -1
  9. package/dist/Errors.js +3 -0
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/Fetch.d.ts +26 -0
  12. package/dist/Fetch.d.ts.map +1 -0
  13. package/dist/Fetch.js +150 -0
  14. package/dist/Fetch.js.map +1 -0
  15. package/dist/Filter.d.ts +14 -0
  16. package/dist/Filter.d.ts.map +1 -0
  17. package/dist/Filter.js +134 -0
  18. package/dist/Filter.js.map +1 -0
  19. package/dist/Help.js +2 -0
  20. package/dist/Help.js.map +1 -1
  21. package/dist/Mcp.d.ts +26 -0
  22. package/dist/Mcp.d.ts.map +1 -1
  23. package/dist/Mcp.js +2 -2
  24. package/dist/Mcp.js.map +1 -1
  25. package/dist/Openapi.d.ts +20 -0
  26. package/dist/Openapi.d.ts.map +1 -0
  27. package/dist/Openapi.js +136 -0
  28. package/dist/Openapi.js.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/middleware.d.ts +8 -2
  34. package/dist/middleware.d.ts.map +1 -1
  35. package/dist/middleware.js.map +1 -1
  36. package/package.json +4 -1
  37. package/src/Cli.test-d.ts +27 -2
  38. package/src/Cli.test.ts +1007 -0
  39. package/src/Cli.ts +676 -47
  40. package/src/Errors.ts +5 -0
  41. package/src/Fetch.test.ts +274 -0
  42. package/src/Fetch.ts +170 -0
  43. package/src/Filter.test.ts +237 -0
  44. package/src/Filter.ts +139 -0
  45. package/src/Help.test.ts +14 -0
  46. package/src/Help.ts +2 -0
  47. package/src/Mcp.ts +3 -3
  48. package/src/Openapi.test.ts +320 -0
  49. package/src/Openapi.ts +196 -0
  50. package/src/e2e.test.ts +778 -0
  51. package/src/index.ts +3 -0
  52. 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
+ }