@apitap/core 1.5.3 → 1.6.0
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 +28 -8
- package/dist/auth/handoff.js +1 -1
- package/dist/auth/handoff.js.map +1 -1
- package/dist/capture/cdp-attach.d.ts +60 -0
- package/dist/capture/cdp-attach.js +422 -0
- package/dist/capture/cdp-attach.js.map +1 -0
- package/dist/capture/filter.js +6 -0
- package/dist/capture/filter.js.map +1 -1
- package/dist/capture/parameterize.d.ts +7 -6
- package/dist/capture/parameterize.js +204 -12
- package/dist/capture/parameterize.js.map +1 -1
- package/dist/capture/session.js +20 -10
- package/dist/capture/session.js.map +1 -1
- package/dist/cli.js +387 -20
- package/dist/cli.js.map +1 -1
- package/dist/discovery/openapi.js +23 -50
- package/dist/discovery/openapi.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +12 -0
- package/dist/mcp.js.map +1 -1
- package/dist/native-host.js +5 -0
- package/dist/native-host.js.map +1 -1
- package/dist/plugin.js +10 -3
- package/dist/plugin.js.map +1 -1
- package/dist/replay/engine.d.ts +13 -0
- package/dist/replay/engine.js +20 -0
- package/dist/replay/engine.js.map +1 -1
- package/dist/skill/apis-guru.d.ts +35 -0
- package/dist/skill/apis-guru.js +128 -0
- package/dist/skill/apis-guru.js.map +1 -0
- package/dist/skill/generator.d.ts +7 -1
- package/dist/skill/generator.js +35 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/merge.d.ts +29 -0
- package/dist/skill/merge.js +252 -0
- package/dist/skill/merge.js.map +1 -0
- package/dist/skill/openapi-converter.d.ts +31 -0
- package/dist/skill/openapi-converter.js +383 -0
- package/dist/skill/openapi-converter.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/package.json +1 -1
- package/src/auth/handoff.ts +1 -1
- package/src/capture/cdp-attach.ts +501 -0
- package/src/capture/filter.ts +5 -0
- package/src/capture/parameterize.ts +207 -11
- package/src/capture/session.ts +20 -10
- package/src/cli.ts +420 -18
- package/src/discovery/openapi.ts +25 -56
- package/src/index.ts +1 -0
- package/src/mcp.ts +12 -0
- package/src/native-host.ts +7 -0
- package/src/plugin.ts +10 -3
- package/src/replay/engine.ts +19 -0
- package/src/skill/apis-guru.ts +163 -0
- package/src/skill/generator.ts +38 -3
- package/src/skill/merge.ts +281 -0
- package/src/skill/openapi-converter.ts +426 -0
- package/src/types.ts +42 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// src/skill/openapi-converter.ts
|
|
2
|
+
import type { SkillEndpoint, ImportResult, ImportMeta, RequestBody } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a JSON $ref pointer in an OpenAPI spec.
|
|
6
|
+
* Uses a visited set to detect cycles and a depth limit as safety net.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveRef(
|
|
9
|
+
obj: any,
|
|
10
|
+
spec: Record<string, any>,
|
|
11
|
+
visited: Set<string> = new Set(),
|
|
12
|
+
depth: number = 0,
|
|
13
|
+
): any {
|
|
14
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
15
|
+
|
|
16
|
+
// Handle allOf composition: merge properties from all entries
|
|
17
|
+
if (obj.allOf && Array.isArray(obj.allOf)) {
|
|
18
|
+
const merged: Record<string, any> = { type: 'object', properties: {} };
|
|
19
|
+
for (const entry of obj.allOf) {
|
|
20
|
+
const resolved = resolveRef(entry, spec, new Set(visited), depth + 1);
|
|
21
|
+
if (resolved?.properties) {
|
|
22
|
+
Object.assign(merged.properties, resolved.properties);
|
|
23
|
+
}
|
|
24
|
+
if (resolved?.required) {
|
|
25
|
+
merged.required = [...(merged.required || []), ...resolved.required];
|
|
26
|
+
}
|
|
27
|
+
if (resolved?.description && !merged.description) {
|
|
28
|
+
merged.description = resolved.description;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return merged;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!obj.$ref) return obj;
|
|
35
|
+
|
|
36
|
+
const ref = obj.$ref as string;
|
|
37
|
+
if (!ref.startsWith('#/')) return null; // Only local document refs supported
|
|
38
|
+
if (visited.has(ref)) return null; // cycle detected
|
|
39
|
+
if (depth > 10) return null; // depth safety net
|
|
40
|
+
|
|
41
|
+
visited.add(ref);
|
|
42
|
+
|
|
43
|
+
const refPath = ref.replace('#/', '').split('/');
|
|
44
|
+
let resolved: any = spec;
|
|
45
|
+
for (const segment of refPath) {
|
|
46
|
+
resolved = resolved?.[segment];
|
|
47
|
+
if (resolved === undefined) return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Recursively resolve if the resolved object also has $ref or allOf
|
|
51
|
+
return resolveRef(resolved, spec, visited, depth + 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function extractDomainAndBasePath(
|
|
55
|
+
spec: Record<string, any>,
|
|
56
|
+
specUrl: string,
|
|
57
|
+
): { domain: string; basePath: string } {
|
|
58
|
+
const serverUrl = spec.servers?.[0]?.url;
|
|
59
|
+
if (serverUrl) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = new URL(serverUrl);
|
|
62
|
+
return { domain: parsed.hostname, basePath: parsed.pathname.replace(/\/$/, '') };
|
|
63
|
+
} catch {
|
|
64
|
+
if (serverUrl.startsWith('/')) {
|
|
65
|
+
const providerName = spec.info?.['x-providerName'];
|
|
66
|
+
const domain = (providerName && /^[a-zA-Z0-9][a-zA-Z0-9._-]+$/.test(providerName))
|
|
67
|
+
? providerName
|
|
68
|
+
: new URL(specUrl).hostname;
|
|
69
|
+
return { domain, basePath: serverUrl.replace(/\/$/, '') };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (spec.host) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(`https://${spec.host}`);
|
|
76
|
+
return { domain: parsed.hostname, basePath: (spec.basePath || '').replace(/\/$/, '') };
|
|
77
|
+
} catch {
|
|
78
|
+
return { domain: spec.host.split(':')[0], basePath: (spec.basePath || '').replace(/\/$/, '') };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return { domain: new URL(specUrl).hostname, basePath: '' };
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error(`Cannot determine API domain from spec (no servers, host, or valid specUrl)`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ConfidenceInput {
|
|
89
|
+
method: string;
|
|
90
|
+
hasExamples: boolean;
|
|
91
|
+
requiresAuth: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function computeConfidence(input: ConfidenceInput): number {
|
|
95
|
+
let score = 0.6;
|
|
96
|
+
if (input.hasExamples) score += 0.1;
|
|
97
|
+
if (!input.requiresAuth) score += 0.1;
|
|
98
|
+
if (input.method === 'GET') score += 0.05;
|
|
99
|
+
return Math.min(score, 0.85);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type AuthType = 'apiKey' | 'oauth2' | 'bearer' | 'basic' | 'openIdConnect';
|
|
103
|
+
|
|
104
|
+
export function detectAuth(spec: Record<string, any>): { requiresAuth: boolean; authType?: AuthType } {
|
|
105
|
+
const schemes = spec.components?.securitySchemes || {};
|
|
106
|
+
const defs = spec.securityDefinitions || {};
|
|
107
|
+
const allSchemes = { ...schemes, ...defs };
|
|
108
|
+
const security = spec.security || [];
|
|
109
|
+
|
|
110
|
+
if (Object.keys(allSchemes).length === 0 && security.length === 0) {
|
|
111
|
+
return { requiresAuth: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let authType: AuthType | undefined;
|
|
115
|
+
for (const scheme of Object.values(allSchemes) as any[]) {
|
|
116
|
+
if (scheme.type === 'http' && scheme.scheme === 'bearer') { authType = 'bearer'; break; }
|
|
117
|
+
if (scheme.type === 'http' && scheme.scheme === 'basic') { authType = 'basic'; break; }
|
|
118
|
+
if (scheme.type === 'apiKey') { authType = 'apiKey'; break; }
|
|
119
|
+
if (scheme.type === 'oauth2') { authType = 'oauth2'; break; }
|
|
120
|
+
if (scheme.type === 'openIdConnect') { authType = 'openIdConnect'; break; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { requiresAuth: true, authType };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function generateEndpointId(
|
|
127
|
+
method: string,
|
|
128
|
+
path: string,
|
|
129
|
+
operationId: string | undefined,
|
|
130
|
+
seen: Set<string>,
|
|
131
|
+
): string {
|
|
132
|
+
let base: string;
|
|
133
|
+
if (operationId) {
|
|
134
|
+
base = `${method.toLowerCase()}-${operationId.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
|
|
135
|
+
} else {
|
|
136
|
+
const segments = path.split('/').filter(s => s !== '' && !s.startsWith(':')).join('-').replace(/[^a-z0-9-]/gi, '').toLowerCase() || 'root';
|
|
137
|
+
base = `${method.toLowerCase()}-${segments}`;
|
|
138
|
+
}
|
|
139
|
+
base = base.replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
|
|
140
|
+
let id = base;
|
|
141
|
+
let counter = 2;
|
|
142
|
+
while (seen.has(id)) {
|
|
143
|
+
id = `${base}-${counter}`.slice(0, 80);
|
|
144
|
+
counter++;
|
|
145
|
+
}
|
|
146
|
+
seen.add(id);
|
|
147
|
+
return id;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] as const;
|
|
151
|
+
const MAX_ENDPOINTS = 500;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Detect whether a parsed JSON object is an OpenAPI/Swagger spec.
|
|
155
|
+
* Returns false for SkillFile objects (which have version+domain+baseUrl+endpoints).
|
|
156
|
+
*/
|
|
157
|
+
export function isOpenAPISpec(obj: unknown): boolean {
|
|
158
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
159
|
+
const o = obj as Record<string, unknown>;
|
|
160
|
+
|
|
161
|
+
// SkillFile check: if it looks like a SkillFile, it's not an OpenAPI spec
|
|
162
|
+
if (o.version && o.domain && o.baseUrl && Array.isArray(o.endpoints)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// OpenAPI 3.x or Swagger 2.0
|
|
167
|
+
if (typeof o.openapi === 'string' || typeof o.swagger === 'string') {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Extract response shape (type + top-level field names) from an OpenAPI response schema.
|
|
176
|
+
*/
|
|
177
|
+
function extractResponseShape(
|
|
178
|
+
responses: Record<string, any> | undefined,
|
|
179
|
+
spec: Record<string, any>,
|
|
180
|
+
): { type: string; fields?: string[] } {
|
|
181
|
+
if (!responses) return { type: 'unknown' };
|
|
182
|
+
|
|
183
|
+
// Try 200, then 201, then first 2xx
|
|
184
|
+
const responseObj =
|
|
185
|
+
responses['200'] || responses['201'] ||
|
|
186
|
+
Object.entries(responses).find(([k]) => k.startsWith('2'))?.[1];
|
|
187
|
+
if (!responseObj) return { type: 'unknown' };
|
|
188
|
+
|
|
189
|
+
const resolved = resolveRef(responseObj, spec);
|
|
190
|
+
if (!resolved) return { type: 'unknown' };
|
|
191
|
+
|
|
192
|
+
// OpenAPI 3.x: content -> application/json -> schema
|
|
193
|
+
let schema = resolved.content?.['application/json']?.schema;
|
|
194
|
+
// Swagger 2.0: schema directly on response
|
|
195
|
+
if (!schema && resolved.schema) schema = resolved.schema;
|
|
196
|
+
if (!schema) return { type: 'unknown' };
|
|
197
|
+
|
|
198
|
+
schema = resolveRef(schema, spec);
|
|
199
|
+
if (!schema) return { type: 'unknown' };
|
|
200
|
+
|
|
201
|
+
const type = schema.type === 'array' ? 'array' : schema.type === 'object' ? 'object' : (schema.type || 'unknown');
|
|
202
|
+
const fields: string[] = [];
|
|
203
|
+
|
|
204
|
+
if (schema.properties) {
|
|
205
|
+
fields.push(...Object.keys(schema.properties));
|
|
206
|
+
} else if (schema.type === 'array' && schema.items) {
|
|
207
|
+
const items = resolveRef(schema.items, spec);
|
|
208
|
+
if (items?.properties) {
|
|
209
|
+
fields.push(...Object.keys(items.properties));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return fields.length > 0 ? { type, fields } : { type };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extract request body template for POST/PUT/PATCH from an OpenAPI operation.
|
|
218
|
+
*/
|
|
219
|
+
function extractRequestBody(
|
|
220
|
+
operation: Record<string, any>,
|
|
221
|
+
spec: Record<string, any>,
|
|
222
|
+
): RequestBody | undefined {
|
|
223
|
+
// OpenAPI 3.x: requestBody -> content -> application/json -> schema
|
|
224
|
+
let schema: any;
|
|
225
|
+
let contentType = 'application/json';
|
|
226
|
+
|
|
227
|
+
if (operation.requestBody) {
|
|
228
|
+
const body = resolveRef(operation.requestBody, spec);
|
|
229
|
+
if (!body) return undefined;
|
|
230
|
+
const jsonContent = body.content?.['application/json'];
|
|
231
|
+
if (jsonContent?.schema) {
|
|
232
|
+
schema = resolveRef(jsonContent.schema, spec);
|
|
233
|
+
} else {
|
|
234
|
+
// Try first content type
|
|
235
|
+
const firstKey = body.content ? Object.keys(body.content)[0] : undefined;
|
|
236
|
+
if (firstKey && body.content[firstKey]?.schema) {
|
|
237
|
+
contentType = firstKey;
|
|
238
|
+
schema = resolveRef(body.content[firstKey].schema, spec);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Swagger 2.0: parameters with in=body
|
|
244
|
+
if (!schema && operation.parameters) {
|
|
245
|
+
const bodyParam = operation.parameters.find((p: any) => p.in === 'body');
|
|
246
|
+
if (bodyParam?.schema) {
|
|
247
|
+
schema = resolveRef(bodyParam.schema, spec);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!schema) return undefined;
|
|
252
|
+
|
|
253
|
+
const template: Record<string, unknown> = {};
|
|
254
|
+
const variables: string[] = [];
|
|
255
|
+
|
|
256
|
+
if (schema.properties) {
|
|
257
|
+
for (const [key, prop] of Object.entries(schema.properties) as [string, any][]) {
|
|
258
|
+
const resolvedProp = resolveRef(prop, spec) || prop;
|
|
259
|
+
if (resolvedProp.example !== undefined) {
|
|
260
|
+
template[key] = resolvedProp.example;
|
|
261
|
+
} else if (resolvedProp.default !== undefined) {
|
|
262
|
+
template[key] = resolvedProp.default;
|
|
263
|
+
} else {
|
|
264
|
+
template[key] = `{{${key}}}`;
|
|
265
|
+
variables.push(key);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
contentType,
|
|
272
|
+
template: Object.keys(template).length > 0 ? template : '{{body}}',
|
|
273
|
+
...(variables.length > 0 ? { variables } : {}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Convert an OpenAPI 3.x or Swagger 2.0 spec into an ImportResult.
|
|
279
|
+
*/
|
|
280
|
+
export function convertOpenAPISpec(
|
|
281
|
+
spec: Record<string, any>,
|
|
282
|
+
specUrl: string,
|
|
283
|
+
): ImportResult {
|
|
284
|
+
const { domain, basePath } = extractDomainAndBasePath(spec, specUrl);
|
|
285
|
+
const { requiresAuth, authType } = detectAuth(spec);
|
|
286
|
+
const specVersion: 'openapi3' | 'swagger2' = spec.swagger ? 'swagger2' : 'openapi3';
|
|
287
|
+
|
|
288
|
+
const endpoints: SkillEndpoint[] = [];
|
|
289
|
+
const seenIds = new Set<string>();
|
|
290
|
+
const paths = spec.paths || {};
|
|
291
|
+
|
|
292
|
+
for (const [pathKey, pathItem] of Object.entries(paths) as [string, any][]) {
|
|
293
|
+
if (!pathItem || typeof pathItem !== 'object') continue;
|
|
294
|
+
|
|
295
|
+
for (const method of HTTP_METHODS) {
|
|
296
|
+
const operation = pathItem[method];
|
|
297
|
+
if (!operation || typeof operation !== 'object') continue;
|
|
298
|
+
|
|
299
|
+
// Collect all parameters (path-level + operation-level)
|
|
300
|
+
const allParams: any[] = [
|
|
301
|
+
...(pathItem.parameters || []),
|
|
302
|
+
...(operation.parameters || []),
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
// Convert path: {param} -> :param, prepend basePath
|
|
306
|
+
const convertedPath = basePath + pathKey.replace(/\{([^}]+)\}/g, ':$1');
|
|
307
|
+
|
|
308
|
+
// Extract query params
|
|
309
|
+
const queryParams: SkillEndpoint['queryParams'] = {};
|
|
310
|
+
for (const param of allParams) {
|
|
311
|
+
const resolved = resolveRef(param, spec) || param;
|
|
312
|
+
if (resolved.in === 'query') {
|
|
313
|
+
const paramSchema = resolved.schema ? (resolveRef(resolved.schema, spec) || resolved.schema) : resolved;
|
|
314
|
+
const example = resolved.example ?? paramSchema?.example ?? paramSchema?.default ?? '';
|
|
315
|
+
queryParams[resolved.name] = {
|
|
316
|
+
type: paramSchema?.type || 'string',
|
|
317
|
+
example: example !== '' ? String(example) : '',
|
|
318
|
+
fromSpec: true,
|
|
319
|
+
...(resolved.required ? { required: true } : {}),
|
|
320
|
+
...(paramSchema?.enum ? { enum: paramSchema.enum } : {}),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Extract path param examples for the example URL
|
|
326
|
+
const pathParamExamples: Record<string, string> = {};
|
|
327
|
+
let hasExamples = false;
|
|
328
|
+
for (const param of allParams) {
|
|
329
|
+
const resolved = resolveRef(param, spec) || param;
|
|
330
|
+
if (resolved.in === 'path') {
|
|
331
|
+
const paramSchema = resolved.schema ? (resolveRef(resolved.schema, spec) || resolved.schema) : resolved;
|
|
332
|
+
const example = resolved.example ?? paramSchema?.example ?? paramSchema?.default;
|
|
333
|
+
if (example !== undefined) {
|
|
334
|
+
pathParamExamples[resolved.name] = String(example);
|
|
335
|
+
hasExamples = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check if query params have examples too
|
|
341
|
+
if (!hasExamples) {
|
|
342
|
+
for (const qp of Object.values(queryParams)) {
|
|
343
|
+
if (qp.example !== '') { hasExamples = true; break; }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Build example URL
|
|
348
|
+
let examplePath = basePath + pathKey;
|
|
349
|
+
// Substitute path params with examples
|
|
350
|
+
for (const [name, value] of Object.entries(pathParamExamples)) {
|
|
351
|
+
examplePath = examplePath.replace(`{${name}}`, value);
|
|
352
|
+
}
|
|
353
|
+
// Replace remaining unsubstituted path params with placeholder
|
|
354
|
+
examplePath = examplePath.replace(/\{([^}]+)\}/g, ':$1');
|
|
355
|
+
|
|
356
|
+
// Add non-empty query params to URL
|
|
357
|
+
const queryEntries = Object.entries(queryParams).filter(([, v]) => v.example !== '');
|
|
358
|
+
const queryString = queryEntries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v.example)}`).join('&');
|
|
359
|
+
const exampleUrl = `https://${domain}${examplePath}${queryString ? '?' + queryString : ''}`;
|
|
360
|
+
|
|
361
|
+
// Extract response shape
|
|
362
|
+
const responseShape = extractResponseShape(operation.responses, spec);
|
|
363
|
+
|
|
364
|
+
// Extract request body for write methods
|
|
365
|
+
const methodUpper = method.toUpperCase();
|
|
366
|
+
let requestBody: RequestBody | undefined;
|
|
367
|
+
if (['POST', 'PUT', 'PATCH'].includes(methodUpper)) {
|
|
368
|
+
requestBody = extractRequestBody(operation, spec);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Description
|
|
372
|
+
const description = operation.summary || operation.description || undefined;
|
|
373
|
+
|
|
374
|
+
// Generate endpoint ID
|
|
375
|
+
const id = generateEndpointId(method, convertedPath, operation.operationId, seenIds);
|
|
376
|
+
|
|
377
|
+
// Compute confidence
|
|
378
|
+
const confidence = computeConfidence({
|
|
379
|
+
method: methodUpper,
|
|
380
|
+
hasExamples,
|
|
381
|
+
requiresAuth,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const endpoint: SkillEndpoint = {
|
|
385
|
+
id,
|
|
386
|
+
method: methodUpper,
|
|
387
|
+
path: convertedPath,
|
|
388
|
+
queryParams,
|
|
389
|
+
headers: {},
|
|
390
|
+
responseShape,
|
|
391
|
+
examples: {
|
|
392
|
+
request: { url: exampleUrl, headers: {} },
|
|
393
|
+
responsePreview: null,
|
|
394
|
+
},
|
|
395
|
+
confidence,
|
|
396
|
+
endpointProvenance: 'openapi-import',
|
|
397
|
+
specSource: specUrl,
|
|
398
|
+
...(description ? { description } : {}),
|
|
399
|
+
...(requestBody ? { requestBody } : {}),
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
endpoints.push(endpoint);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Sort by confidence descending, then truncate
|
|
407
|
+
endpoints.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
408
|
+
if (endpoints.length > MAX_ENDPOINTS) {
|
|
409
|
+
process.stderr.write(
|
|
410
|
+
`[openapi-import] Warning: spec has ${endpoints.length} endpoints, truncating to ${MAX_ENDPOINTS}\n`,
|
|
411
|
+
);
|
|
412
|
+
endpoints.length = MAX_ENDPOINTS;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const meta: ImportMeta = {
|
|
416
|
+
specUrl,
|
|
417
|
+
specVersion,
|
|
418
|
+
title: spec.info?.title || '',
|
|
419
|
+
description: spec.info?.description || '',
|
|
420
|
+
requiresAuth,
|
|
421
|
+
...(authType ? { authType } : {}),
|
|
422
|
+
endpointCount: endpoints.length,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return { domain, endpoints, meta };
|
|
426
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -118,7 +118,7 @@ export interface SkillEndpoint {
|
|
|
118
118
|
id: string;
|
|
119
119
|
method: string;
|
|
120
120
|
path: string;
|
|
121
|
-
queryParams: Record<string, { type: string; example: string }>;
|
|
121
|
+
queryParams: Record<string, { type: string; example: string; required?: boolean; enum?: string[]; fromSpec?: boolean }>;
|
|
122
122
|
headers: Record<string, string>;
|
|
123
123
|
responseShape: { type: string; fields?: string[] };
|
|
124
124
|
examples: {
|
|
@@ -131,6 +131,11 @@ export interface SkillEndpoint {
|
|
|
131
131
|
responseBytes?: number; // v1.0: response body size in bytes
|
|
132
132
|
isolatedAuth?: boolean; // v1.1: opt out of cross-subdomain auth fallback
|
|
133
133
|
responseSchema?: SchemaNode; // v1.1: schema snapshot for contract validation
|
|
134
|
+
normalizedPath?: string;
|
|
135
|
+
confidence?: number;
|
|
136
|
+
endpointProvenance?: 'captured' | 'openapi-import';
|
|
137
|
+
specSource?: string;
|
|
138
|
+
description?: string;
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
/** The full skill file written to disk */
|
|
@@ -149,6 +154,13 @@ export interface SkillFile {
|
|
|
149
154
|
totalNetworkBytes: number; // sum of ALL response body sizes
|
|
150
155
|
totalRequests: number;
|
|
151
156
|
};
|
|
157
|
+
importHistory?: Array<{
|
|
158
|
+
specUrl: string;
|
|
159
|
+
specVersion: 'openapi3' | 'swagger2';
|
|
160
|
+
importedAt: string;
|
|
161
|
+
endpointsAdded: number;
|
|
162
|
+
endpointsEnriched: number;
|
|
163
|
+
}>;
|
|
152
164
|
};
|
|
153
165
|
provenance: 'self' | 'imported' | 'unsigned';
|
|
154
166
|
signature?: string;
|
|
@@ -245,3 +257,32 @@ export interface DiscoveryResult {
|
|
|
245
257
|
authSignals?: string[]; // reasons auth was detected
|
|
246
258
|
loginUrl?: string; // detected login page URL
|
|
247
259
|
}
|
|
260
|
+
|
|
261
|
+
/** Metadata about an OpenAPI spec import */
|
|
262
|
+
export interface ImportMeta {
|
|
263
|
+
specUrl: string;
|
|
264
|
+
specVersion: 'openapi3' | 'swagger2';
|
|
265
|
+
title: string;
|
|
266
|
+
description: string;
|
|
267
|
+
requiresAuth: boolean;
|
|
268
|
+
authType?: 'apiKey' | 'oauth2' | 'bearer' | 'basic' | 'openIdConnect';
|
|
269
|
+
endpointCount: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Result of converting an OpenAPI spec */
|
|
273
|
+
export interface ImportResult {
|
|
274
|
+
domain: string;
|
|
275
|
+
endpoints: SkillEndpoint[];
|
|
276
|
+
meta: ImportMeta;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Result of merging imported endpoints into an existing skill file */
|
|
280
|
+
export interface MergeResult {
|
|
281
|
+
skillFile: SkillFile;
|
|
282
|
+
diff: {
|
|
283
|
+
preserved: number;
|
|
284
|
+
added: number;
|
|
285
|
+
enriched: number;
|
|
286
|
+
skipped: number;
|
|
287
|
+
};
|
|
288
|
+
}
|