@skill-tools/gen 0.2.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/LICENSE +190 -0
- package/README.md +61 -0
- package/dist/chunk-EMDMG3H6.js +609 -0
- package/dist/chunk-EMDMG3H6.js.map +1 -0
- package/dist/chunk-PZXBN36T.cjs +609 -0
- package/dist/chunk-PZXBN36T.cjs.map +1 -0
- package/dist/cli.cjs +76 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +76 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +15 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +196 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
// src/openapi.ts
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
function parseOpenApi(content) {
|
|
4
|
+
const MAX_SPEC_SIZE = 1e7;
|
|
5
|
+
if (content.length > MAX_SPEC_SIZE) {
|
|
6
|
+
throw new Error(`OpenAPI spec exceeds maximum size (${MAX_SPEC_SIZE} bytes)`);
|
|
7
|
+
}
|
|
8
|
+
const doc = parseDocument(content);
|
|
9
|
+
const version = doc.openapi;
|
|
10
|
+
if (!version || !version.startsWith("3.")) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Unsupported OpenAPI version: ${version ?? "unknown"}. Only OpenAPI 3.x is supported.`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
const title = doc.info?.title ?? "Untitled API";
|
|
16
|
+
const description = doc.info?.description ?? "";
|
|
17
|
+
const apiVersion = doc.info?.version ?? "0.0.0";
|
|
18
|
+
const servers = extractServers(doc);
|
|
19
|
+
const auth = extractAuthSchemes(doc);
|
|
20
|
+
const endpoints = extractEndpoints(doc);
|
|
21
|
+
return {
|
|
22
|
+
title,
|
|
23
|
+
description,
|
|
24
|
+
version: apiVersion,
|
|
25
|
+
servers,
|
|
26
|
+
auth,
|
|
27
|
+
endpoints
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function parseDocument(content) {
|
|
31
|
+
const trimmed = content.trim();
|
|
32
|
+
if (trimmed.startsWith("{")) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(trimmed);
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return YAML.parse(content);
|
|
39
|
+
}
|
|
40
|
+
function resolveRef(doc, ref) {
|
|
41
|
+
if (!ref.startsWith("#/")) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
const path = ref.slice(2).split("/");
|
|
45
|
+
let current = doc;
|
|
46
|
+
for (const segment of path) {
|
|
47
|
+
if (current == null || typeof current !== "object") return {};
|
|
48
|
+
current = current[segment];
|
|
49
|
+
}
|
|
50
|
+
return current ?? {};
|
|
51
|
+
}
|
|
52
|
+
function deref(doc, obj) {
|
|
53
|
+
if (typeof obj.$ref === "string") {
|
|
54
|
+
return resolveRef(doc, obj.$ref);
|
|
55
|
+
}
|
|
56
|
+
return obj;
|
|
57
|
+
}
|
|
58
|
+
function extractServers(doc) {
|
|
59
|
+
const servers = doc.servers;
|
|
60
|
+
if (!Array.isArray(servers)) return [];
|
|
61
|
+
return servers.map((s) => s.url).filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
function extractAuthSchemes(doc) {
|
|
64
|
+
const components = doc.components;
|
|
65
|
+
const schemes = components?.securitySchemes;
|
|
66
|
+
if (!schemes) return [];
|
|
67
|
+
return Object.entries(schemes).map(([name, raw]) => {
|
|
68
|
+
const scheme = deref(doc, raw);
|
|
69
|
+
return {
|
|
70
|
+
type: scheme.type ?? "apiKey",
|
|
71
|
+
name,
|
|
72
|
+
description: scheme.description,
|
|
73
|
+
in: scheme.in,
|
|
74
|
+
scheme: scheme.scheme
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function extractEndpoints(doc) {
|
|
79
|
+
const paths = doc.paths;
|
|
80
|
+
if (!paths) return [];
|
|
81
|
+
const endpoints = [];
|
|
82
|
+
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
83
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
84
|
+
const resolved = deref(doc, pathItem);
|
|
85
|
+
const pathParams = Array.isArray(resolved.parameters) ? resolved.parameters.map((p) => extractParameter(doc, p)) : [];
|
|
86
|
+
for (const method of httpMethods) {
|
|
87
|
+
const operation = resolved[method];
|
|
88
|
+
if (!operation) continue;
|
|
89
|
+
const opParams = Array.isArray(operation.parameters) ? operation.parameters.map((p) => extractParameter(doc, p)) : [];
|
|
90
|
+
const paramMap = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const p of pathParams) paramMap.set(`${p.in}:${p.name}`, p);
|
|
92
|
+
for (const p of opParams) paramMap.set(`${p.in}:${p.name}`, p);
|
|
93
|
+
const requestBody = operation.requestBody ? extractRequestBody(doc, deref(doc, operation.requestBody)) : void 0;
|
|
94
|
+
const responses = extractResponses(
|
|
95
|
+
doc,
|
|
96
|
+
operation.responses
|
|
97
|
+
);
|
|
98
|
+
endpoints.push({
|
|
99
|
+
method: method.toUpperCase(),
|
|
100
|
+
path,
|
|
101
|
+
operationId: operation.operationId,
|
|
102
|
+
summary: operation.summary,
|
|
103
|
+
description: operation.description,
|
|
104
|
+
tags: Array.isArray(operation.tags) ? operation.tags : [],
|
|
105
|
+
parameters: Array.from(paramMap.values()),
|
|
106
|
+
requestBody,
|
|
107
|
+
responses
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return endpoints;
|
|
112
|
+
}
|
|
113
|
+
function extractParameter(doc, raw) {
|
|
114
|
+
const param = deref(doc, raw);
|
|
115
|
+
const schema = param.schema ? deref(doc, param.schema) : {};
|
|
116
|
+
return {
|
|
117
|
+
name: param.name ?? "",
|
|
118
|
+
in: param.in ?? "query",
|
|
119
|
+
description: param.description,
|
|
120
|
+
required: param.required ?? false,
|
|
121
|
+
type: schema.type,
|
|
122
|
+
example: param.example
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function extractRequestBody(doc, body) {
|
|
126
|
+
const content = body.content;
|
|
127
|
+
if (!content) return void 0;
|
|
128
|
+
const contentType = "application/json" in content ? "application/json" : Object.keys(content)[0];
|
|
129
|
+
if (!contentType) return void 0;
|
|
130
|
+
const mediaType = content[contentType];
|
|
131
|
+
if (!mediaType) return void 0;
|
|
132
|
+
const schema = mediaType.schema ? deref(doc, mediaType.schema) : {};
|
|
133
|
+
return {
|
|
134
|
+
description: body.description,
|
|
135
|
+
contentType,
|
|
136
|
+
required: body.required ?? false,
|
|
137
|
+
properties: extractProperties(doc, schema),
|
|
138
|
+
example: mediaType.example ?? schema.example
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function extractProperties(doc, schema) {
|
|
142
|
+
const properties = schema.properties;
|
|
143
|
+
if (!properties) return [];
|
|
144
|
+
const requiredFields = new Set(
|
|
145
|
+
Array.isArray(schema.required) ? schema.required : []
|
|
146
|
+
);
|
|
147
|
+
return Object.entries(properties).map(([name, raw]) => {
|
|
148
|
+
const prop = deref(doc, raw);
|
|
149
|
+
return {
|
|
150
|
+
name,
|
|
151
|
+
type: prop.type ?? "unknown",
|
|
152
|
+
description: prop.description,
|
|
153
|
+
required: requiredFields.has(name),
|
|
154
|
+
example: prop.example
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function extractResponses(doc, responses) {
|
|
159
|
+
if (!responses) return [];
|
|
160
|
+
return Object.entries(responses).map(([statusCode, raw]) => {
|
|
161
|
+
const response = deref(doc, raw);
|
|
162
|
+
const content = response.content;
|
|
163
|
+
let contentType;
|
|
164
|
+
let properties = [];
|
|
165
|
+
if (content) {
|
|
166
|
+
contentType = "application/json" in content ? "application/json" : Object.keys(content)[0];
|
|
167
|
+
if (contentType && content[contentType]) {
|
|
168
|
+
const mediaType = content[contentType];
|
|
169
|
+
const schema = mediaType.schema ? deref(doc, mediaType.schema) : {};
|
|
170
|
+
properties = extractProperties(doc, schema);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
statusCode,
|
|
175
|
+
description: response.description,
|
|
176
|
+
contentType,
|
|
177
|
+
properties
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/renderer.ts
|
|
183
|
+
function renderSkillMd(spec, options = {}) {
|
|
184
|
+
const mode = options.mode ?? "unified";
|
|
185
|
+
const files = /* @__PURE__ */ new Map();
|
|
186
|
+
if (mode === "unified") {
|
|
187
|
+
const name = options.name ?? toKebabCase(spec.title);
|
|
188
|
+
const content = renderUnified(spec, { ...options, name });
|
|
189
|
+
files.set(`${name}/SKILL.md`, content);
|
|
190
|
+
} else {
|
|
191
|
+
for (const endpoint of spec.endpoints) {
|
|
192
|
+
const epName = endpoint.operationId ? toKebabCase(endpoint.operationId) : toKebabCase(`${endpoint.method}-${endpoint.path}`);
|
|
193
|
+
const content = renderSingleEndpoint(spec, endpoint, {
|
|
194
|
+
...options,
|
|
195
|
+
name: epName
|
|
196
|
+
});
|
|
197
|
+
files.set(`${epName}/SKILL.md`, content);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return files;
|
|
201
|
+
}
|
|
202
|
+
function renderUnified(spec, options) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
lines.push("---");
|
|
205
|
+
lines.push(`name: ${options.name}`);
|
|
206
|
+
lines.push(`description: >-`, ` ${options.description ?? buildUnifiedDescription(spec)}`);
|
|
207
|
+
lines.push("---");
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push(`# ${spec.title}`);
|
|
210
|
+
lines.push("");
|
|
211
|
+
if (spec.description) {
|
|
212
|
+
lines.push(spec.description);
|
|
213
|
+
lines.push("");
|
|
214
|
+
}
|
|
215
|
+
if (spec.servers.length > 0) {
|
|
216
|
+
lines.push("## Base URL");
|
|
217
|
+
lines.push("");
|
|
218
|
+
for (const server of spec.servers) {
|
|
219
|
+
lines.push(`- \`${server}\``);
|
|
220
|
+
}
|
|
221
|
+
lines.push("");
|
|
222
|
+
}
|
|
223
|
+
if (spec.auth.length > 0) {
|
|
224
|
+
lines.push("## Authentication");
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push(renderAuth(spec.auth));
|
|
227
|
+
lines.push("");
|
|
228
|
+
}
|
|
229
|
+
lines.push("## Endpoints");
|
|
230
|
+
lines.push("");
|
|
231
|
+
const grouped = groupByTag(spec.endpoints);
|
|
232
|
+
for (const [tag, endpoints] of grouped) {
|
|
233
|
+
if (tag !== "_untagged") {
|
|
234
|
+
lines.push(`### ${tag}`);
|
|
235
|
+
lines.push("");
|
|
236
|
+
}
|
|
237
|
+
for (const ep of endpoints) {
|
|
238
|
+
lines.push(renderEndpointSection(ep));
|
|
239
|
+
lines.push("");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (options.includeErrorHandling !== false) {
|
|
243
|
+
lines.push("## Error Handling");
|
|
244
|
+
lines.push("");
|
|
245
|
+
lines.push(renderErrorHandling(spec));
|
|
246
|
+
lines.push("");
|
|
247
|
+
}
|
|
248
|
+
return lines.join("\n");
|
|
249
|
+
}
|
|
250
|
+
function renderSingleEndpoint(spec, endpoint, options) {
|
|
251
|
+
const lines = [];
|
|
252
|
+
const summary = endpoint.summary ?? endpoint.description ?? `${endpoint.method} ${endpoint.path}`;
|
|
253
|
+
lines.push("---");
|
|
254
|
+
lines.push(`name: ${options.name}`);
|
|
255
|
+
lines.push(
|
|
256
|
+
`description: >-`,
|
|
257
|
+
` ${options.description ?? `Use when the user wants to ${summary.toLowerCase()}. Calls ${endpoint.method} ${endpoint.path}.`}`
|
|
258
|
+
);
|
|
259
|
+
lines.push("---");
|
|
260
|
+
lines.push("");
|
|
261
|
+
lines.push(`# ${summary}`);
|
|
262
|
+
lines.push("");
|
|
263
|
+
if (endpoint.description && endpoint.description !== endpoint.summary) {
|
|
264
|
+
lines.push(endpoint.description);
|
|
265
|
+
lines.push("");
|
|
266
|
+
}
|
|
267
|
+
if (spec.servers.length > 0) {
|
|
268
|
+
lines.push(`**Base URL:** \`${spec.servers[0]}\``);
|
|
269
|
+
lines.push("");
|
|
270
|
+
}
|
|
271
|
+
if (spec.auth.length > 0) {
|
|
272
|
+
lines.push("## Authentication");
|
|
273
|
+
lines.push("");
|
|
274
|
+
lines.push(renderAuth(spec.auth));
|
|
275
|
+
lines.push("");
|
|
276
|
+
}
|
|
277
|
+
lines.push(`## ${endpoint.method} \`${endpoint.path}\``);
|
|
278
|
+
lines.push("");
|
|
279
|
+
if (endpoint.parameters.length > 0) {
|
|
280
|
+
lines.push("### Parameters");
|
|
281
|
+
lines.push("");
|
|
282
|
+
lines.push(renderParametersTable(endpoint));
|
|
283
|
+
lines.push("");
|
|
284
|
+
}
|
|
285
|
+
if (endpoint.requestBody) {
|
|
286
|
+
lines.push("### Request Body");
|
|
287
|
+
lines.push("");
|
|
288
|
+
lines.push(renderRequestBody(endpoint.requestBody, options));
|
|
289
|
+
lines.push("");
|
|
290
|
+
}
|
|
291
|
+
if (endpoint.responses.length > 0) {
|
|
292
|
+
lines.push("### Responses");
|
|
293
|
+
lines.push("");
|
|
294
|
+
lines.push(renderResponses(endpoint));
|
|
295
|
+
lines.push("");
|
|
296
|
+
}
|
|
297
|
+
if (options.includeExamples !== false) {
|
|
298
|
+
lines.push("## Example");
|
|
299
|
+
lines.push("");
|
|
300
|
+
lines.push(renderExample(spec, endpoint));
|
|
301
|
+
lines.push("");
|
|
302
|
+
}
|
|
303
|
+
if (options.includeErrorHandling !== false) {
|
|
304
|
+
lines.push("## Error Handling");
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push(renderEndpointErrorHandling(endpoint));
|
|
307
|
+
lines.push("");
|
|
308
|
+
}
|
|
309
|
+
return lines.join("\n");
|
|
310
|
+
}
|
|
311
|
+
function buildUnifiedDescription(spec) {
|
|
312
|
+
const endpointCount = spec.endpoints.length;
|
|
313
|
+
const methods = new Set(spec.endpoints.map((e) => e.method));
|
|
314
|
+
const methodList = Array.from(methods).join(", ");
|
|
315
|
+
if (spec.description) {
|
|
316
|
+
return `${spec.description.split(".")[0]}. Use when working with the ${spec.title} (${endpointCount} endpoints, ${methodList}).`;
|
|
317
|
+
}
|
|
318
|
+
return `Interact with the ${spec.title}. Use when the user needs to call any of the ${endpointCount} available endpoints (${methodList}).`;
|
|
319
|
+
}
|
|
320
|
+
function renderAuth(auth) {
|
|
321
|
+
const lines = [];
|
|
322
|
+
for (const scheme of auth) {
|
|
323
|
+
switch (scheme.type) {
|
|
324
|
+
case "http":
|
|
325
|
+
if (scheme.scheme === "bearer") {
|
|
326
|
+
lines.push("Use Bearer token authentication:");
|
|
327
|
+
lines.push("```");
|
|
328
|
+
lines.push("Authorization: Bearer <token>");
|
|
329
|
+
lines.push("```");
|
|
330
|
+
} else if (scheme.scheme === "basic") {
|
|
331
|
+
lines.push("Use HTTP Basic authentication:");
|
|
332
|
+
lines.push("```");
|
|
333
|
+
lines.push("Authorization: Basic <base64(username:password)>");
|
|
334
|
+
lines.push("```");
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
case "apiKey":
|
|
338
|
+
lines.push(`Pass the API key via ${scheme.in ?? "header"}:`);
|
|
339
|
+
lines.push("```");
|
|
340
|
+
lines.push(`${scheme.name}: <api-key>`);
|
|
341
|
+
lines.push("```");
|
|
342
|
+
break;
|
|
343
|
+
case "oauth2":
|
|
344
|
+
lines.push("Uses OAuth 2.0 authentication. Obtain an access token first.");
|
|
345
|
+
break;
|
|
346
|
+
default:
|
|
347
|
+
if (scheme.description) {
|
|
348
|
+
lines.push(scheme.description);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return lines.join("\n");
|
|
353
|
+
}
|
|
354
|
+
function groupByTag(endpoints) {
|
|
355
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
356
|
+
for (const ep of endpoints) {
|
|
357
|
+
const tag = ep.tags.length > 0 ? ep.tags[0] : "_untagged";
|
|
358
|
+
const list = grouped.get(tag) ?? [];
|
|
359
|
+
list.push(ep);
|
|
360
|
+
grouped.set(tag, list);
|
|
361
|
+
}
|
|
362
|
+
return grouped;
|
|
363
|
+
}
|
|
364
|
+
function renderEndpointSection(ep) {
|
|
365
|
+
const lines = [];
|
|
366
|
+
const heading = ep.summary ?? `${ep.method} ${ep.path}`;
|
|
367
|
+
lines.push(`#### ${ep.method} \`${ep.path}\` \u2014 ${heading}`);
|
|
368
|
+
lines.push("");
|
|
369
|
+
if (ep.description && ep.description !== ep.summary) {
|
|
370
|
+
lines.push(ep.description);
|
|
371
|
+
lines.push("");
|
|
372
|
+
}
|
|
373
|
+
if (ep.parameters.length > 0) {
|
|
374
|
+
lines.push(renderParametersTable(ep));
|
|
375
|
+
lines.push("");
|
|
376
|
+
}
|
|
377
|
+
if (ep.requestBody) {
|
|
378
|
+
lines.push(`**Request body** (\`${ep.requestBody.contentType}\`):`);
|
|
379
|
+
lines.push("");
|
|
380
|
+
lines.push(renderPropertiesTable(ep.requestBody.properties));
|
|
381
|
+
lines.push("");
|
|
382
|
+
}
|
|
383
|
+
const successResponse = ep.responses.find((r) => r.statusCode.startsWith("2"));
|
|
384
|
+
if (successResponse) {
|
|
385
|
+
lines.push(
|
|
386
|
+
`**Response:** ${successResponse.statusCode} \u2014 ${successResponse.description ?? "Success"}`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
return lines.join("\n");
|
|
390
|
+
}
|
|
391
|
+
function renderParametersTable(ep) {
|
|
392
|
+
const lines = [];
|
|
393
|
+
lines.push("| Parameter | In | Type | Required | Description |");
|
|
394
|
+
lines.push("|-----------|-----|------|----------|-------------|");
|
|
395
|
+
for (const param of ep.parameters) {
|
|
396
|
+
const req = param.required ? "Yes" : "No";
|
|
397
|
+
const desc = param.description ?? "";
|
|
398
|
+
lines.push(`| \`${param.name}\` | ${param.in} | ${param.type ?? "-"} | ${req} | ${desc} |`);
|
|
399
|
+
}
|
|
400
|
+
return lines.join("\n");
|
|
401
|
+
}
|
|
402
|
+
function renderPropertiesTable(properties) {
|
|
403
|
+
if (properties.length === 0) return "";
|
|
404
|
+
const lines = [];
|
|
405
|
+
lines.push("| Property | Type | Required | Description |");
|
|
406
|
+
lines.push("|----------|------|----------|-------------|");
|
|
407
|
+
for (const prop of properties) {
|
|
408
|
+
const req = prop.required ? "Yes" : "No";
|
|
409
|
+
const desc = prop.description ?? "";
|
|
410
|
+
lines.push(`| \`${prop.name}\` | ${prop.type} | ${req} | ${desc} |`);
|
|
411
|
+
}
|
|
412
|
+
return lines.join("\n");
|
|
413
|
+
}
|
|
414
|
+
function renderRequestBody(body, options) {
|
|
415
|
+
const lines = [];
|
|
416
|
+
if (body.description) {
|
|
417
|
+
lines.push(body.description);
|
|
418
|
+
lines.push("");
|
|
419
|
+
}
|
|
420
|
+
lines.push(`Content-Type: \`${body.contentType}\``);
|
|
421
|
+
lines.push("");
|
|
422
|
+
if (body.properties.length > 0) {
|
|
423
|
+
lines.push(renderPropertiesTable(body.properties));
|
|
424
|
+
}
|
|
425
|
+
if (options.includeExamples !== false && body.example) {
|
|
426
|
+
lines.push("");
|
|
427
|
+
lines.push("**Example:**");
|
|
428
|
+
lines.push("```json");
|
|
429
|
+
lines.push(JSON.stringify(body.example, null, 2));
|
|
430
|
+
lines.push("```");
|
|
431
|
+
}
|
|
432
|
+
return lines.join("\n");
|
|
433
|
+
}
|
|
434
|
+
function renderResponses(ep) {
|
|
435
|
+
const lines = [];
|
|
436
|
+
for (const resp of ep.responses) {
|
|
437
|
+
lines.push(`**${resp.statusCode}** \u2014 ${resp.description ?? ""}`);
|
|
438
|
+
if (resp.properties.length > 0) {
|
|
439
|
+
lines.push("");
|
|
440
|
+
lines.push(renderPropertiesTable(resp.properties));
|
|
441
|
+
}
|
|
442
|
+
lines.push("");
|
|
443
|
+
}
|
|
444
|
+
return lines.join("\n");
|
|
445
|
+
}
|
|
446
|
+
function renderExample(spec, ep) {
|
|
447
|
+
const baseUrl = spec.servers[0] ?? "https://api.example.com";
|
|
448
|
+
const lines = [];
|
|
449
|
+
lines.push("```bash");
|
|
450
|
+
lines.push(`curl -X ${ep.method} "${baseUrl}${ep.path}" \\`);
|
|
451
|
+
if (spec.auth.length > 0) {
|
|
452
|
+
const authScheme = spec.auth[0];
|
|
453
|
+
if (authScheme.type === "http" && authScheme.scheme === "bearer") {
|
|
454
|
+
lines.push(' -H "Authorization: Bearer $TOKEN" \\');
|
|
455
|
+
} else if (authScheme.type === "apiKey") {
|
|
456
|
+
lines.push(` -H "${authScheme.name}: $API_KEY" \\`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
lines.push(' -H "Content-Type: application/json"');
|
|
460
|
+
if (ep.requestBody?.example) {
|
|
461
|
+
lines.push(` -d '${JSON.stringify(ep.requestBody.example)}'`);
|
|
462
|
+
}
|
|
463
|
+
lines.push("```");
|
|
464
|
+
return lines.join("\n");
|
|
465
|
+
}
|
|
466
|
+
function renderErrorHandling(spec) {
|
|
467
|
+
const errorCodes = /* @__PURE__ */ new Set();
|
|
468
|
+
for (const ep of spec.endpoints) {
|
|
469
|
+
for (const resp of ep.responses) {
|
|
470
|
+
if (resp.statusCode.startsWith("4") || resp.statusCode.startsWith("5")) {
|
|
471
|
+
errorCodes.add(resp.statusCode);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const lines = [];
|
|
476
|
+
lines.push("Common error responses:");
|
|
477
|
+
lines.push("");
|
|
478
|
+
if (errorCodes.size === 0) {
|
|
479
|
+
lines.push("- **4xx**: Client error \u2014 check request parameters and authentication");
|
|
480
|
+
lines.push("- **5xx**: Server error \u2014 retry with exponential backoff");
|
|
481
|
+
} else {
|
|
482
|
+
for (const code of Array.from(errorCodes).sort()) {
|
|
483
|
+
lines.push(`- **${code}**: ${httpStatusDescription(code)}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
lines.push("");
|
|
487
|
+
lines.push("When an error occurs:");
|
|
488
|
+
lines.push("1. Check the response body for a detailed error message");
|
|
489
|
+
lines.push("2. Verify authentication credentials are valid");
|
|
490
|
+
lines.push("3. For 429 (rate limit), wait and retry with exponential backoff");
|
|
491
|
+
lines.push("4. For 5xx errors, retry up to 3 times with backoff");
|
|
492
|
+
return lines.join("\n");
|
|
493
|
+
}
|
|
494
|
+
function renderEndpointErrorHandling(ep) {
|
|
495
|
+
const errorResponses = ep.responses.filter(
|
|
496
|
+
(r) => r.statusCode.startsWith("4") || r.statusCode.startsWith("5")
|
|
497
|
+
);
|
|
498
|
+
const lines = [];
|
|
499
|
+
if (errorResponses.length > 0) {
|
|
500
|
+
lines.push("Possible errors:");
|
|
501
|
+
lines.push("");
|
|
502
|
+
for (const resp of errorResponses) {
|
|
503
|
+
lines.push(
|
|
504
|
+
`- **${resp.statusCode}**: ${resp.description ?? httpStatusDescription(resp.statusCode)}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
lines.push("- Check authentication credentials if you receive a 401/403");
|
|
509
|
+
lines.push("- Validate request parameters for 400 errors");
|
|
510
|
+
lines.push("- Retry on 5xx with exponential backoff");
|
|
511
|
+
}
|
|
512
|
+
return lines.join("\n");
|
|
513
|
+
}
|
|
514
|
+
function httpStatusDescription(code) {
|
|
515
|
+
const descriptions = {
|
|
516
|
+
"400": "Bad Request \u2014 check request parameters",
|
|
517
|
+
"401": "Unauthorized \u2014 check authentication",
|
|
518
|
+
"403": "Forbidden \u2014 insufficient permissions",
|
|
519
|
+
"404": "Not Found \u2014 resource does not exist",
|
|
520
|
+
"405": "Method Not Allowed",
|
|
521
|
+
"409": "Conflict \u2014 resource state conflict",
|
|
522
|
+
"422": "Unprocessable Entity \u2014 validation error",
|
|
523
|
+
"429": "Too Many Requests \u2014 rate limited, retry after backoff",
|
|
524
|
+
"500": "Internal Server Error \u2014 retry with backoff",
|
|
525
|
+
"502": "Bad Gateway \u2014 upstream service error",
|
|
526
|
+
"503": "Service Unavailable \u2014 retry later"
|
|
527
|
+
};
|
|
528
|
+
return descriptions[code] ?? `HTTP ${code} error`;
|
|
529
|
+
}
|
|
530
|
+
function toKebabCase(input) {
|
|
531
|
+
return input.replace(/[^a-zA-Z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").replace(/-+/g, "-").toLowerCase().replace(/^-|-$/g, "").slice(0, 64);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/generator.ts
|
|
535
|
+
import { readFile } from "fs/promises";
|
|
536
|
+
import { countTokens } from "@skill-tools/core";
|
|
537
|
+
async function generateFromOpenApi(specPath, options = {}) {
|
|
538
|
+
try {
|
|
539
|
+
const content = await readFile(specPath, "utf-8");
|
|
540
|
+
const spec = parseOpenApi(content);
|
|
541
|
+
return generateFromSpec(spec, options);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
return {
|
|
544
|
+
ok: false,
|
|
545
|
+
error: err instanceof Error ? err.message : String(err)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function generateFromSpec(spec, options = {}) {
|
|
550
|
+
try {
|
|
551
|
+
const files = renderSkillMd(spec, options);
|
|
552
|
+
let totalTokens = 0;
|
|
553
|
+
for (const content of files.values()) {
|
|
554
|
+
totalTokens += countTokens(content);
|
|
555
|
+
}
|
|
556
|
+
const maxTokens = options.maxTokens ?? 4e3;
|
|
557
|
+
if (options.mode !== "per-endpoint" && totalTokens > maxTokens) {
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
files,
|
|
562
|
+
endpointCount: spec.endpoints.length,
|
|
563
|
+
tokenCount: totalTokens
|
|
564
|
+
};
|
|
565
|
+
} catch (err) {
|
|
566
|
+
return {
|
|
567
|
+
ok: false,
|
|
568
|
+
error: err instanceof Error ? err.message : String(err)
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function generateFromText(name, description, instructions = "") {
|
|
573
|
+
const kebabName = name.replace(/[^a-zA-Z0-9\s-]/g, "").replace(/\s+/g, "-").toLowerCase().slice(0, 64);
|
|
574
|
+
const lines = [];
|
|
575
|
+
lines.push("---");
|
|
576
|
+
lines.push(`name: ${kebabName}`);
|
|
577
|
+
lines.push(`description: >-`);
|
|
578
|
+
lines.push(` ${description}`);
|
|
579
|
+
lines.push("---");
|
|
580
|
+
lines.push("");
|
|
581
|
+
lines.push(`# ${name}`);
|
|
582
|
+
lines.push("");
|
|
583
|
+
lines.push(description);
|
|
584
|
+
lines.push("");
|
|
585
|
+
if (instructions) {
|
|
586
|
+
lines.push("## Instructions");
|
|
587
|
+
lines.push("");
|
|
588
|
+
lines.push(instructions);
|
|
589
|
+
lines.push("");
|
|
590
|
+
}
|
|
591
|
+
const content = lines.join("\n");
|
|
592
|
+
const files = /* @__PURE__ */ new Map();
|
|
593
|
+
files.set(`${kebabName}/SKILL.md`, content);
|
|
594
|
+
return {
|
|
595
|
+
ok: true,
|
|
596
|
+
files,
|
|
597
|
+
endpointCount: 0,
|
|
598
|
+
tokenCount: countTokens(content)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export {
|
|
603
|
+
parseOpenApi,
|
|
604
|
+
renderSkillMd,
|
|
605
|
+
generateFromOpenApi,
|
|
606
|
+
generateFromSpec,
|
|
607
|
+
generateFromText
|
|
608
|
+
};
|
|
609
|
+
//# sourceMappingURL=chunk-EMDMG3H6.js.map
|