poe-code 3.0.382 → 3.0.383
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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/packages/toolcraft-openapi/dist/bin/generate.js +148 -5
- package/packages/toolcraft-openapi/dist/generate.d.ts +8 -0
- package/packages/toolcraft-openapi/dist/generate.js +238 -19
- package/packages/toolcraft-openapi/dist/index.d.ts +2 -2
- package/packages/toolcraft-openapi/dist/index.js +1 -1
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ import { withOutputFormat } from "toolcraft-design";
|
|
|
10
10
|
import { readToolcraftConfig } from "../config.js";
|
|
11
11
|
import { diagnose } from "../diagnose.js";
|
|
12
12
|
import { formatDiagnostics } from "../diagnostics.js";
|
|
13
|
-
import { generate } from "../generate.js";
|
|
13
|
+
import { generate, generateSkill } from "../generate.js";
|
|
14
14
|
import { inspectOpenApiSource } from "../inspect-source.js";
|
|
15
15
|
import { renderOpenApiInspection } from "../render-inspection.js";
|
|
16
16
|
import { parseOpenApiDocument, readOpenApiSourceText } from "../spec-source.js";
|
|
@@ -113,31 +113,50 @@ export async function syncGeneratedClient(options, services) {
|
|
|
113
113
|
? configResult.diagnostics
|
|
114
114
|
: [...configResult.diagnostics, ...diagnose(configResult.config, document)];
|
|
115
115
|
const effectiveConfig = hasErrorDiagnostics(diagnostics) ? undefined : configResult.config;
|
|
116
|
-
const generatedFiles = generate(document, {
|
|
116
|
+
const generatedFiles = generate(document, {
|
|
117
|
+
specSha,
|
|
118
|
+
config: effectiveConfig
|
|
119
|
+
});
|
|
120
|
+
const generatedSkill = generateSkill(document, {
|
|
121
|
+
config: effectiveConfig,
|
|
122
|
+
commandName: await inferPackageCommandName(services)
|
|
123
|
+
});
|
|
117
124
|
const outputDir = path.resolve(services.cwd, options.outputDir);
|
|
118
125
|
const lockPath = path.resolve(services.cwd, options.lockPath);
|
|
126
|
+
const skillFile = createGeneratedSkillFile(generatedSkill, services.cwd);
|
|
119
127
|
const currentLockContents = await readOpenApiLockText(services.fs, lockPath);
|
|
120
128
|
const desiredLockContents = stringifyOpenApiLock({ specSha });
|
|
121
129
|
const currentFiles = await readGeneratedFiles(services.fs, outputDir);
|
|
130
|
+
const currentSkillContents = await readOptionalFile(services.fs, skillFile.path);
|
|
122
131
|
const desiredFiles = new Map([
|
|
123
132
|
...generatedFiles.map((file) => [path.resolve(outputDir, file.path), file.contents]),
|
|
124
133
|
...createDownloadedSpecFiles(options.input, sourceText).map((file) => [path.resolve(outputDir, file.path), file.contents])
|
|
125
134
|
]);
|
|
135
|
+
const currentSkillFiles = currentSkillContents === undefined
|
|
136
|
+
? new Map()
|
|
137
|
+
: new Map([[skillFile.path, currentSkillContents]]);
|
|
138
|
+
const desiredSkillFiles = new Map([[skillFile.path, skillFile.contents]]);
|
|
126
139
|
const updatedFiles = collectUpdatedFiles(currentFiles, desiredFiles);
|
|
140
|
+
const updatedSkillFiles = collectUpdatedFiles(currentSkillFiles, desiredSkillFiles);
|
|
127
141
|
const updatedLockFile = currentLockContents === desiredLockContents
|
|
128
142
|
? undefined
|
|
129
143
|
: { path: lockPath, contents: desiredLockContents, previousContents: currentLockContents };
|
|
130
144
|
const deletedFiles = collectDeletedFiles(currentFiles, desiredFiles);
|
|
131
|
-
const drifted = updatedFiles.length > 0 ||
|
|
145
|
+
const drifted = updatedFiles.length > 0 ||
|
|
146
|
+
updatedSkillFiles.length > 0 ||
|
|
147
|
+
updatedLockFile !== undefined ||
|
|
148
|
+
deletedFiles.length > 0;
|
|
132
149
|
if (!options.check && !options.diff && drifted && !hasErrorDiagnostics(diagnostics)) {
|
|
133
150
|
try {
|
|
134
151
|
await writeGeneratedFiles(services.fs, outputDir, updatedFiles);
|
|
152
|
+
await writeGeneratedSkillFiles(services.fs, services.cwd, updatedSkillFiles);
|
|
135
153
|
await deleteGeneratedFiles(services.fs, outputDir, deletedFiles);
|
|
136
154
|
if (updatedLockFile !== undefined) {
|
|
137
155
|
await writeOpenApiLock(services.fs, lockPath, { specSha });
|
|
138
156
|
}
|
|
139
157
|
}
|
|
140
158
|
catch (error) {
|
|
159
|
+
await restoreGeneratedSkillFiles(services.fs, services.cwd, currentSkillFiles, updatedSkillFiles);
|
|
141
160
|
await restoreGeneratedFiles(services.fs, outputDir, currentFiles, updatedFiles, deletedFiles);
|
|
142
161
|
throw error;
|
|
143
162
|
}
|
|
@@ -148,8 +167,10 @@ export async function syncGeneratedClient(options, services) {
|
|
|
148
167
|
diagnostics,
|
|
149
168
|
drifted,
|
|
150
169
|
specSha,
|
|
151
|
-
updatedFiles: updatedLockFile === undefined
|
|
152
|
-
|
|
170
|
+
updatedFiles: updatedLockFile === undefined
|
|
171
|
+
? [...updatedFiles, ...updatedSkillFiles]
|
|
172
|
+
: [...updatedFiles, ...updatedSkillFiles, updatedLockFile],
|
|
173
|
+
updatedFileCount: updatedFiles.length + updatedSkillFiles.length + (updatedLockFile === undefined ? 0 : 1)
|
|
153
174
|
};
|
|
154
175
|
}
|
|
155
176
|
async function readAdjacentToolcraftConfig(input, services) {
|
|
@@ -177,6 +198,51 @@ function resolveAdjacentConfigPath(input, cwd) {
|
|
|
177
198
|
function hasErrorDiagnostics(diagnostics) {
|
|
178
199
|
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
179
200
|
}
|
|
201
|
+
async function inferPackageCommandName(services) {
|
|
202
|
+
const packageJsonPath = path.resolve(services.cwd, "package.json");
|
|
203
|
+
const source = await readOptionalFile(services.fs, packageJsonPath);
|
|
204
|
+
if (source === undefined) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(source);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
throw new UserError(`Failed to parse package.json for generated skill command name: ${getErrorMessage(error)}`, { cause: error });
|
|
213
|
+
}
|
|
214
|
+
if (!isPlainObject(parsed)) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
const packageName = typeof parsed.name === "string" ? normalizePackageCommandName(parsed.name) : undefined;
|
|
218
|
+
const bin = parsed.bin;
|
|
219
|
+
if (typeof bin === "string") {
|
|
220
|
+
return packageName;
|
|
221
|
+
}
|
|
222
|
+
if (!isPlainObject(bin)) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
const binNames = Object.keys(bin);
|
|
226
|
+
if (packageName !== undefined && binNames.includes(packageName)) {
|
|
227
|
+
return packageName;
|
|
228
|
+
}
|
|
229
|
+
return binNames.find((name) => !isMcpBinaryName(name)) ?? binNames[0];
|
|
230
|
+
}
|
|
231
|
+
function normalizePackageCommandName(packageName) {
|
|
232
|
+
const parts = packageName.split("/");
|
|
233
|
+
const name = parts[parts.length - 1]?.trim();
|
|
234
|
+
return name === undefined || name.length === 0 ? undefined : name;
|
|
235
|
+
}
|
|
236
|
+
function isMcpBinaryName(name) {
|
|
237
|
+
const words = name.toLowerCase().split("-");
|
|
238
|
+
return words.includes("mcp");
|
|
239
|
+
}
|
|
240
|
+
function createGeneratedSkillFile(skill, cwd) {
|
|
241
|
+
return {
|
|
242
|
+
path: path.resolve(cwd, ".claude", "skills", skill.name, "SKILL.md"),
|
|
243
|
+
contents: skill.contents
|
|
244
|
+
};
|
|
245
|
+
}
|
|
180
246
|
function renderGeneratedDiff(result, outputDir) {
|
|
181
247
|
const sections = [];
|
|
182
248
|
for (const file of result.updatedFiles) {
|
|
@@ -320,6 +386,17 @@ async function readOpenApiLockText(fs, lockPath) {
|
|
|
320
386
|
throw error;
|
|
321
387
|
}
|
|
322
388
|
}
|
|
389
|
+
async function readOptionalFile(fs, filePath) {
|
|
390
|
+
try {
|
|
391
|
+
return await fs.readFile(filePath, "utf8");
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (isNotFoundError(error)) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
323
400
|
function parseOpenApiLock(contents, lockPath) {
|
|
324
401
|
let parsed;
|
|
325
402
|
try {
|
|
@@ -440,6 +517,13 @@ async function writeGeneratedFiles(fs, outputDir, filesToWrite) {
|
|
|
440
517
|
await atomicWriteGeneratedFile(fs, outputDir, file.path, file.contents);
|
|
441
518
|
}
|
|
442
519
|
}
|
|
520
|
+
async function writeGeneratedSkillFiles(fs, cwd, filesToWrite) {
|
|
521
|
+
for (const file of filesToWrite) {
|
|
522
|
+
await fs.mkdir(path.dirname(file.path), { recursive: true });
|
|
523
|
+
await assertSafeProjectPath(fs, cwd, file.path);
|
|
524
|
+
await atomicWriteProjectFile(fs, cwd, file.path, file.contents);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
443
527
|
async function assertSafeOutputPath(fs, outputDir, filePath) {
|
|
444
528
|
const canonicalOutputDir = await fs.realpath(outputDir);
|
|
445
529
|
const canonicalFileParent = await fs.realpath(path.dirname(filePath));
|
|
@@ -461,6 +545,31 @@ async function assertSafeOutputPath(fs, outputDir, filePath) {
|
|
|
461
545
|
}
|
|
462
546
|
}
|
|
463
547
|
}
|
|
548
|
+
async function assertSafeProjectPath(fs, cwd, filePath) {
|
|
549
|
+
const rootPath = path.resolve(cwd);
|
|
550
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
551
|
+
const relativePath = path.relative(rootPath, resolvedFilePath);
|
|
552
|
+
if (relativePath === ".." ||
|
|
553
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
554
|
+
path.isAbsolute(relativePath)) {
|
|
555
|
+
throw new Error("Generated skill output must remain inside the project directory.");
|
|
556
|
+
}
|
|
557
|
+
const parentOfRoot = path.dirname(rootPath);
|
|
558
|
+
let currentPath = resolvedFilePath;
|
|
559
|
+
while (currentPath !== parentOfRoot) {
|
|
560
|
+
try {
|
|
561
|
+
if ((await fs.lstat(currentPath)).isSymbolicLink()) {
|
|
562
|
+
throw new Error("Generated skill output must remain inside the project directory.");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
if (!isNotFoundError(error)) {
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
currentPath = path.dirname(currentPath);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
464
573
|
async function atomicWriteGeneratedFile(fs, outputDir, filePath, contents) {
|
|
465
574
|
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.tmp`);
|
|
466
575
|
let tempCreated = false;
|
|
@@ -479,12 +588,43 @@ async function atomicWriteGeneratedFile(fs, outputDir, filePath, contents) {
|
|
|
479
588
|
throw error;
|
|
480
589
|
}
|
|
481
590
|
}
|
|
591
|
+
async function atomicWriteProjectFile(fs, cwd, filePath, contents) {
|
|
592
|
+
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.tmp`);
|
|
593
|
+
let tempCreated = false;
|
|
594
|
+
try {
|
|
595
|
+
await assertSafeProjectPath(fs, cwd, tempPath);
|
|
596
|
+
await fs.writeFile(tempPath, contents, { encoding: "utf8", flag: "wx" });
|
|
597
|
+
tempCreated = true;
|
|
598
|
+
await assertSafeProjectPath(fs, cwd, filePath);
|
|
599
|
+
await fs.rename(tempPath, filePath);
|
|
600
|
+
tempCreated = false;
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
if (tempCreated || !isAlreadyExistsError(error)) {
|
|
604
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
605
|
+
}
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
482
609
|
async function deleteGeneratedFiles(fs, outputDir, filePaths) {
|
|
483
610
|
for (const filePath of filePaths) {
|
|
484
611
|
await assertSafeOutputPath(fs, outputDir, filePath);
|
|
485
612
|
await fs.rm(filePath, { force: true });
|
|
486
613
|
}
|
|
487
614
|
}
|
|
615
|
+
async function restoreGeneratedSkillFiles(fs, cwd, currentFiles, updatedFiles) {
|
|
616
|
+
for (const file of updatedFiles) {
|
|
617
|
+
const previousContents = currentFiles.get(file.path);
|
|
618
|
+
if (previousContents === undefined) {
|
|
619
|
+
await assertSafeProjectPath(fs, cwd, file.path);
|
|
620
|
+
await fs.rm(file.path, { force: true });
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
await fs.mkdir(path.dirname(file.path), { recursive: true });
|
|
624
|
+
await assertSafeProjectPath(fs, cwd, file.path);
|
|
625
|
+
await atomicWriteProjectFile(fs, cwd, file.path, previousContents);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
488
628
|
async function restoreGeneratedFiles(fs, outputDir, currentFiles, updatedFiles, deletedFiles) {
|
|
489
629
|
for (const file of updatedFiles) {
|
|
490
630
|
const previousContents = currentFiles.get(file.path);
|
|
@@ -522,6 +662,9 @@ function getErrorCode(error) {
|
|
|
522
662
|
function getErrorMessage(error) {
|
|
523
663
|
return error instanceof Error ? error.message : String(error);
|
|
524
664
|
}
|
|
665
|
+
function isPlainObject(value) {
|
|
666
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
667
|
+
}
|
|
525
668
|
function isDirectExecution(moduleUrl, argv) {
|
|
526
669
|
const entryPoint = argv[1];
|
|
527
670
|
if (entryPoint === undefined) {
|
|
@@ -106,6 +106,10 @@ export interface GeneratedFile {
|
|
|
106
106
|
path: string;
|
|
107
107
|
contents: string;
|
|
108
108
|
}
|
|
109
|
+
export interface GeneratedSkill {
|
|
110
|
+
name: string;
|
|
111
|
+
contents: string;
|
|
112
|
+
}
|
|
109
113
|
export interface GeneratedCommand {
|
|
110
114
|
noun: string;
|
|
111
115
|
verb: string;
|
|
@@ -252,6 +256,10 @@ interface SchemaOptionEntry {
|
|
|
252
256
|
}
|
|
253
257
|
type QueryArraySerialization = "repeat" | "brackets" | "comma" | "pipe";
|
|
254
258
|
export declare function generate(document: OpenApiDocument, options: GenerateOptions): GeneratedFile[];
|
|
259
|
+
export declare function generateSkill(document: OpenApiDocument, options?: {
|
|
260
|
+
commandName?: string | undefined;
|
|
261
|
+
config?: ToolcraftConfig | undefined;
|
|
262
|
+
}): GeneratedSkill;
|
|
255
263
|
export declare function collectGeneratedCommands(document: OpenApiDocument, config?: ToolcraftConfig): GeneratedCommand[];
|
|
256
264
|
export declare function collectGeneratedCommand(document: OpenApiDocument, path: string, method: HttpMethod): GeneratedCommand;
|
|
257
265
|
export declare function collectSchemaOptionEntries(param: RenderSchemaOptionsInput): SchemaOptionEntry[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ToolcraftBugError, UserError } from "toolcraft";
|
|
2
|
-
import { METHOD_DEFAULTS, deriveDisambiguatedVerb, deriveNoun, derivePathDisambiguatedVerb, deriveVerb, isIdentifierName, normalizeNoun, normalizeParamName, toCamelCase, toPascalCase } from "./naming.js";
|
|
2
|
+
import { METHOD_DEFAULTS, deriveDisambiguatedVerb, deriveNoun, derivePathDisambiguatedVerb, deriveVerb, isIdentifierName, normalizeNoun, normalizeParamName, toCliFlag, toCamelCase, toPascalCase } from "./naming.js";
|
|
3
3
|
import { groupByNoun } from "./group-by-noun.js";
|
|
4
4
|
import { renderPreflightBlock, renderRequestShape } from "./interpreter.js";
|
|
5
5
|
import { normalizeOpenApiDocument } from "./normalize-swagger.js";
|
|
@@ -132,6 +132,16 @@ export function generate(document, options) {
|
|
|
132
132
|
createMcpFile()
|
|
133
133
|
];
|
|
134
134
|
}
|
|
135
|
+
export function generateSkill(document, options = {}) {
|
|
136
|
+
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
137
|
+
const commands = collectGeneratedCommands(normalizedDocument, options.config);
|
|
138
|
+
const label = normalizedDocument.info?.title ?? "Toolcraft";
|
|
139
|
+
return createSkill({
|
|
140
|
+
commands,
|
|
141
|
+
commandName: options.commandName,
|
|
142
|
+
label
|
|
143
|
+
});
|
|
144
|
+
}
|
|
135
145
|
export function collectGeneratedCommands(document, config) {
|
|
136
146
|
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
137
147
|
const paths = normalizedDocument.paths;
|
|
@@ -150,7 +160,8 @@ function applyConfiguredCommandShape(commands, config) {
|
|
|
150
160
|
return;
|
|
151
161
|
}
|
|
152
162
|
for (const command of commands) {
|
|
153
|
-
const match = configured.find((candidate) => candidate.method.method.toUpperCase() === command.method &&
|
|
163
|
+
const match = configured.find((candidate) => candidate.method.method.toUpperCase() === command.method &&
|
|
164
|
+
candidate.method.path === command.path);
|
|
154
165
|
if (match === undefined) {
|
|
155
166
|
continue;
|
|
156
167
|
}
|
|
@@ -340,7 +351,11 @@ function collectParams(document, entry, operation, operationId, auth, responseMo
|
|
|
340
351
|
const operationParams = collectOperationParameters(document, entry.path, entry.pathItem.parameters ?? [], operation.parameters ?? [], operationId, auth);
|
|
341
352
|
const requestBodyParams = collectRequestBodyParams(document, operation, operationId, entry.method);
|
|
342
353
|
const qualifiedRequestBodyParams = qualifyBodyParamCollisions(requestBodyParams, new Set([...operationParams.params, ...transportParams].map((param) => param.paramName)));
|
|
343
|
-
const params = [
|
|
354
|
+
const params = [
|
|
355
|
+
...operationParams.params,
|
|
356
|
+
...qualifiedRequestBodyParams.params,
|
|
357
|
+
...transportParams
|
|
358
|
+
];
|
|
344
359
|
const deduped = new Map();
|
|
345
360
|
for (const param of params) {
|
|
346
361
|
const existing = deduped.get(param.paramName);
|
|
@@ -352,9 +367,15 @@ function collectParams(document, entry, operation, operationId, auth, responseMo
|
|
|
352
367
|
return {
|
|
353
368
|
params: [...deduped.values()],
|
|
354
369
|
paramsSchemaOptions: qualifiedRequestBodyParams.paramsSchemaOptions,
|
|
355
|
-
preflightBlocks: [
|
|
370
|
+
preflightBlocks: [
|
|
371
|
+
...operationParams.preflightBlocks,
|
|
372
|
+
...qualifiedRequestBodyParams.preflightBlocks
|
|
373
|
+
],
|
|
356
374
|
requestFields: [...operationParams.requestFields, ...qualifiedRequestBodyParams.requestFields],
|
|
357
|
-
sectionRenders: {
|
|
375
|
+
sectionRenders: {
|
|
376
|
+
...operationParams.sectionRenders,
|
|
377
|
+
...qualifiedRequestBodyParams.sectionRenders
|
|
378
|
+
},
|
|
358
379
|
optionalSections: new Set([
|
|
359
380
|
...operationParams.optionalSections,
|
|
360
381
|
...qualifiedRequestBodyParams.optionalSections
|
|
@@ -495,7 +516,12 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
495
516
|
}
|
|
496
517
|
const requestBody = expectRequestBody(document, operation.requestBody, operationId, "requestBody");
|
|
497
518
|
const contentEntries = Object.entries(requestBody.content ?? {});
|
|
498
|
-
const contentEntry = contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isJsonMediaType(mediaType)) ??
|
|
519
|
+
const contentEntry = contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isJsonMediaType(mediaType)) ??
|
|
520
|
+
contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined &&
|
|
521
|
+
mediaType.toLowerCase() === "application/x-www-form-urlencoded") ??
|
|
522
|
+
contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isTextMediaType(mediaType)) ??
|
|
523
|
+
contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isBinaryMediaType(mediaType)) ??
|
|
524
|
+
contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && mediaType.toLowerCase() === "multipart/form-data");
|
|
499
525
|
const content = contentEntry?.[1];
|
|
500
526
|
const requestMediaType = contentEntry?.[0];
|
|
501
527
|
const bodyMode = requestMediaType?.toLowerCase() === "application/x-www-form-urlencoded"
|
|
@@ -537,7 +563,8 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
537
563
|
}
|
|
538
564
|
if ((schema.additionalProperties !== undefined && schema.additionalProperties !== false) ||
|
|
539
565
|
schema.properties === undefined) {
|
|
540
|
-
return createCollectedRequestBodyParams([
|
|
566
|
+
return createCollectedRequestBodyParams([
|
|
567
|
+
createJsonBodyField({
|
|
541
568
|
document,
|
|
542
569
|
name: "body",
|
|
543
570
|
description: schema.description ?? requestBody.description,
|
|
@@ -546,7 +573,8 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
546
573
|
operationId,
|
|
547
574
|
context: "requestBody",
|
|
548
575
|
location: "body"
|
|
549
|
-
})
|
|
576
|
+
})
|
|
577
|
+
], bodyOptional, requestBody.description, "inline", undefined, bodyMode, multipartBinaryFields);
|
|
550
578
|
}
|
|
551
579
|
const required = new Set(schema.required ?? []);
|
|
552
580
|
const assemblies = [];
|
|
@@ -565,12 +593,14 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
565
593
|
params: [],
|
|
566
594
|
paramsSchemaOptions: schema.additionalProperties === false ? { additionalProperties: false } : undefined,
|
|
567
595
|
preflightBlocks: [],
|
|
568
|
-
requestFields: [
|
|
596
|
+
requestFields: [
|
|
597
|
+
{
|
|
569
598
|
location: "body",
|
|
570
599
|
wireName: "body",
|
|
571
600
|
value: { kind: "emptyObject" },
|
|
572
601
|
omitWhenUndefinedReference: { kind: "resolved", resolvedName: "emptyBody" }
|
|
573
|
-
}
|
|
602
|
+
}
|
|
603
|
+
],
|
|
574
604
|
sectionRenders: { body: "inline" },
|
|
575
605
|
optionalSections: new Set(),
|
|
576
606
|
requestBodyDescription: requestBody.description,
|
|
@@ -633,7 +663,9 @@ function createGeneratedParameter(document, parameter, operationId, auth) {
|
|
|
633
663
|
if (parameter.in === "query" &&
|
|
634
664
|
parameter.style === "deepObject" &&
|
|
635
665
|
(getCompositionKeyword(schema) !== undefined ||
|
|
636
|
-
(schema.type === "array" &&
|
|
666
|
+
(schema.type === "array" &&
|
|
667
|
+
schema.items !== undefined &&
|
|
668
|
+
isComplexJsonBodySchema(document, schema.items, operationId, `${context} items`)))) {
|
|
637
669
|
return createJsonQueryField({
|
|
638
670
|
document,
|
|
639
671
|
name: parameter.name,
|
|
@@ -973,19 +1005,25 @@ function createPathArrayParam(options) {
|
|
|
973
1005
|
const definition = createParamDefinition(options.document, options.schema, options.operationId, options.context);
|
|
974
1006
|
const paramName = options.name;
|
|
975
1007
|
return {
|
|
976
|
-
params: [
|
|
1008
|
+
params: [
|
|
1009
|
+
{
|
|
977
1010
|
paramName,
|
|
978
1011
|
sourceName: options.name,
|
|
979
1012
|
location: "path",
|
|
980
1013
|
description: options.description,
|
|
981
1014
|
optional: false,
|
|
982
1015
|
definition
|
|
983
|
-
}
|
|
1016
|
+
}
|
|
1017
|
+
],
|
|
984
1018
|
preflightBlocks: [],
|
|
985
1019
|
requestField: {
|
|
986
1020
|
location: "path",
|
|
987
1021
|
wireName: options.name,
|
|
988
|
-
value: {
|
|
1022
|
+
value: {
|
|
1023
|
+
kind: "queryArray",
|
|
1024
|
+
reference: { kind: "param", paramName },
|
|
1025
|
+
serialization: "comma"
|
|
1026
|
+
},
|
|
989
1027
|
omitWhenUndefinedReference: { kind: "param", paramName }
|
|
990
1028
|
}
|
|
991
1029
|
};
|
|
@@ -1137,11 +1175,16 @@ function isAsciiDigit(character) {
|
|
|
1137
1175
|
}
|
|
1138
1176
|
function isJsonMediaType(mediaType) {
|
|
1139
1177
|
const normalized = mediaType.toLowerCase();
|
|
1140
|
-
return normalized === "*/*" ||
|
|
1178
|
+
return (normalized === "*/*" ||
|
|
1179
|
+
normalized === "text/json" ||
|
|
1180
|
+
normalized.includes("application/json") ||
|
|
1181
|
+
normalized.includes("+json"));
|
|
1141
1182
|
}
|
|
1142
1183
|
function isTextMediaType(mediaType) {
|
|
1143
1184
|
const normalized = mediaType.toLowerCase();
|
|
1144
|
-
return normalized.startsWith("text/") ||
|
|
1185
|
+
return (normalized.startsWith("text/") ||
|
|
1186
|
+
normalized === "plain/text" ||
|
|
1187
|
+
normalized === "application/jwt");
|
|
1145
1188
|
}
|
|
1146
1189
|
function isBinaryMediaType(mediaType) {
|
|
1147
1190
|
const normalized = mediaType.toLowerCase();
|
|
@@ -1354,8 +1397,7 @@ function normalizeNullableTypeArray(schema) {
|
|
|
1354
1397
|
}
|
|
1355
1398
|
function getCompositionKeyword(schema) {
|
|
1356
1399
|
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
1357
|
-
if (Object.prototype.hasOwnProperty.call(schema, keyword) &&
|
|
1358
|
-
schema[keyword] !== undefined) {
|
|
1400
|
+
if (Object.prototype.hasOwnProperty.call(schema, keyword) && schema[keyword] !== undefined) {
|
|
1359
1401
|
return keyword;
|
|
1360
1402
|
}
|
|
1361
1403
|
}
|
|
@@ -1702,7 +1744,9 @@ function renderObjectKey(name) {
|
|
|
1702
1744
|
}
|
|
1703
1745
|
function createSafeGeneratedNoun(noun) {
|
|
1704
1746
|
const normalized = normalizeNoun(noun);
|
|
1705
|
-
return isTypeScriptIdentifier(toCamelCase(normalized))
|
|
1747
|
+
return isTypeScriptIdentifier(toCamelCase(normalized))
|
|
1748
|
+
? normalized
|
|
1749
|
+
: `api-${normalized || "operation"}`;
|
|
1706
1750
|
}
|
|
1707
1751
|
export function collectSchemaOptionEntries(param) {
|
|
1708
1752
|
return SCHEMA_OPTION_SOURCES.flatMap(({ key, get }) => {
|
|
@@ -1916,6 +1960,181 @@ function createMcpFile() {
|
|
|
1916
1960
|
].join("\n")
|
|
1917
1961
|
};
|
|
1918
1962
|
}
|
|
1963
|
+
function createSkill(options) {
|
|
1964
|
+
const commandName = options.commandName ?? "<cli>";
|
|
1965
|
+
const groups = groupByNoun(options.commands);
|
|
1966
|
+
const skillName = createSkillName(options.commandName ?? options.label);
|
|
1967
|
+
const description = createSkillDescription(options.label, groups.map((group) => group.noun));
|
|
1968
|
+
const quickStartCommands = collectQuickStartCommands(commandName, options.commands);
|
|
1969
|
+
const lines = [
|
|
1970
|
+
"---",
|
|
1971
|
+
`name: ${skillName}`,
|
|
1972
|
+
`description: ${JSON.stringify(description)}`,
|
|
1973
|
+
"---",
|
|
1974
|
+
"",
|
|
1975
|
+
`# ${options.label}`,
|
|
1976
|
+
"",
|
|
1977
|
+
`Use \`${commandName}\` for CLI examples. If MCP tools from this package are registered, prefer them for structured calls; their names mirror the command groups and verbs below.`,
|
|
1978
|
+
"",
|
|
1979
|
+
"## Quick Start",
|
|
1980
|
+
"",
|
|
1981
|
+
"```sh",
|
|
1982
|
+
`${commandName} --help`,
|
|
1983
|
+
...quickStartCommands,
|
|
1984
|
+
"```",
|
|
1985
|
+
"",
|
|
1986
|
+
"## Command Groups",
|
|
1987
|
+
""
|
|
1988
|
+
];
|
|
1989
|
+
if (groups.length === 0) {
|
|
1990
|
+
lines.push("No OpenAPI operations were generated.", "");
|
|
1991
|
+
}
|
|
1992
|
+
else {
|
|
1993
|
+
for (const group of groups) {
|
|
1994
|
+
lines.push(`- \`${group.noun}\`: ${group.commands.map((command) => `\`${command.verb}\``).join(", ")}`);
|
|
1995
|
+
}
|
|
1996
|
+
lines.push("");
|
|
1997
|
+
}
|
|
1998
|
+
const catalogCommands = options.commands.slice(0, 80);
|
|
1999
|
+
if (catalogCommands.length > 0) {
|
|
2000
|
+
lines.push("## Commands", "");
|
|
2001
|
+
for (const command of catalogCommands) {
|
|
2002
|
+
lines.push(renderSkillCommandCatalogLine(commandName, command));
|
|
2003
|
+
}
|
|
2004
|
+
const omittedCount = options.commands.length - catalogCommands.length;
|
|
2005
|
+
if (omittedCount > 0) {
|
|
2006
|
+
lines.push(`- ${omittedCount} more commands omitted. Run \`${commandName} --help\` and group help for the full surface.`);
|
|
2007
|
+
}
|
|
2008
|
+
lines.push("");
|
|
2009
|
+
}
|
|
2010
|
+
const exampleBlocks = options.commands.flatMap((command) => (command.examples ?? []).map((example) => ({ command, example })));
|
|
2011
|
+
if (exampleBlocks.length > 0) {
|
|
2012
|
+
lines.push("## Examples", "");
|
|
2013
|
+
for (const { command, example } of exampleBlocks.slice(0, 5)) {
|
|
2014
|
+
lines.push(`### ${oneLine(example.title)}`, "");
|
|
2015
|
+
lines.push("```sh", renderSkillCommandLine(commandName, command), "```", "");
|
|
2016
|
+
lines.push("Params:");
|
|
2017
|
+
lines.push("```json", `${JSON.stringify(example.params, null, 2)}`, "```", "");
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
lines.push("## Output", "", "Commands return structured output. Use command or group `--help` to inspect the full generated option list before calling commands with complex request bodies.", "");
|
|
2021
|
+
return {
|
|
2022
|
+
name: skillName,
|
|
2023
|
+
contents: lines.join("\n")
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
function createSkillName(label) {
|
|
2027
|
+
const normalized = normalizeNoun(label);
|
|
2028
|
+
const fallback = normalized.length === 0 ? "openapi-tools" : normalized;
|
|
2029
|
+
if (fallback.length <= 63) {
|
|
2030
|
+
return fallback;
|
|
2031
|
+
}
|
|
2032
|
+
return trimTrailingHyphens(fallback.slice(0, 63));
|
|
2033
|
+
}
|
|
2034
|
+
function createSkillDescription(label, groupNames) {
|
|
2035
|
+
const groupSummary = groupNames.length === 0
|
|
2036
|
+
? ""
|
|
2037
|
+
: ` Includes command groups: ${groupNames.slice(0, 12).join(", ")}${groupNames.length > 12 ? ", and more" : ""}.`;
|
|
2038
|
+
return `Use ${label} generated OpenAPI CLI or MCP tools. Use when Codex needs to call this API, inspect available commands, or run generated operations from the OpenAPI specification.${groupSummary}`;
|
|
2039
|
+
}
|
|
2040
|
+
function collectQuickStartCommands(commandName, commands) {
|
|
2041
|
+
const lines = [];
|
|
2042
|
+
const firstGroup = groupByNoun(commands)[0];
|
|
2043
|
+
if (firstGroup !== undefined) {
|
|
2044
|
+
lines.push(`${commandName} ${firstGroup.noun} --help`);
|
|
2045
|
+
}
|
|
2046
|
+
for (const command of commands) {
|
|
2047
|
+
if (lines.length >= 4) {
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
if (!isReadCommand(command)) {
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
const requiredNamedParams = command.params.filter((param) => !param.optional &&
|
|
2054
|
+
param.global !== true &&
|
|
2055
|
+
param.location !== "transport" &&
|
|
2056
|
+
!command.positional.includes(param.paramName));
|
|
2057
|
+
if (requiredNamedParams.length === 0) {
|
|
2058
|
+
lines.push(renderSkillCommandLine(commandName, command));
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return lines;
|
|
2062
|
+
}
|
|
2063
|
+
function isReadCommand(command) {
|
|
2064
|
+
return command.method === "GET" || command.method === "HEAD" || command.method === "OPTIONS";
|
|
2065
|
+
}
|
|
2066
|
+
function renderSkillCommandCatalogLine(commandName, command) {
|
|
2067
|
+
const description = command.description === undefined ? "" : ` - ${truncate(oneLine(command.description), 140)}`;
|
|
2068
|
+
return `- \`${renderSkillCommandLine(commandName, command)}\`${description} (\`${command.method} ${command.path}\`)`;
|
|
2069
|
+
}
|
|
2070
|
+
function renderSkillCommandLine(commandName, command) {
|
|
2071
|
+
const parts = [
|
|
2072
|
+
commandName,
|
|
2073
|
+
command.noun,
|
|
2074
|
+
command.verb,
|
|
2075
|
+
...command.positional.map((paramName) => `<${paramName}>`)
|
|
2076
|
+
];
|
|
2077
|
+
const requiredFlags = command.params.filter((param) => !param.optional &&
|
|
2078
|
+
param.global !== true &&
|
|
2079
|
+
param.location !== "transport" &&
|
|
2080
|
+
!command.positional.includes(param.paramName));
|
|
2081
|
+
const renderedFlags = requiredFlags
|
|
2082
|
+
.filter((param) => param.definition.kind !== "object")
|
|
2083
|
+
.slice(0, 4)
|
|
2084
|
+
.map(renderRequiredSkillFlag);
|
|
2085
|
+
parts.push(...renderedFlags);
|
|
2086
|
+
if (requiredFlags.length > renderedFlags.length) {
|
|
2087
|
+
parts.push("[required options...]");
|
|
2088
|
+
}
|
|
2089
|
+
return parts.join(" ");
|
|
2090
|
+
}
|
|
2091
|
+
function renderRequiredSkillFlag(param) {
|
|
2092
|
+
const flag = `--${toCliFlag(param.paramName)}`;
|
|
2093
|
+
if (param.definition.kind === "boolean") {
|
|
2094
|
+
return flag;
|
|
2095
|
+
}
|
|
2096
|
+
if (param.definition.kind === "array") {
|
|
2097
|
+
return `${flag} <value...>`;
|
|
2098
|
+
}
|
|
2099
|
+
if (param.definition.kind === "enum") {
|
|
2100
|
+
return `${flag} <${param.definition.enumValues.map((value) => String(value)).join("|")}>`;
|
|
2101
|
+
}
|
|
2102
|
+
return `${flag} <value>`;
|
|
2103
|
+
}
|
|
2104
|
+
function oneLine(value) {
|
|
2105
|
+
const words = [];
|
|
2106
|
+
let current = "";
|
|
2107
|
+
for (const character of value) {
|
|
2108
|
+
if (isWhitespace(character)) {
|
|
2109
|
+
if (current.length > 0) {
|
|
2110
|
+
words.push(current);
|
|
2111
|
+
current = "";
|
|
2112
|
+
}
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
current += character;
|
|
2116
|
+
}
|
|
2117
|
+
if (current.length > 0) {
|
|
2118
|
+
words.push(current);
|
|
2119
|
+
}
|
|
2120
|
+
return words.join(" ");
|
|
2121
|
+
}
|
|
2122
|
+
function truncate(value, maxLength) {
|
|
2123
|
+
if (value.length <= maxLength) {
|
|
2124
|
+
return value;
|
|
2125
|
+
}
|
|
2126
|
+
return `${value.slice(0, Math.max(0, maxLength - 1)).trimEnd()}...`;
|
|
2127
|
+
}
|
|
2128
|
+
function trimTrailingHyphens(value) {
|
|
2129
|
+
let endIndex = value.length;
|
|
2130
|
+
while (endIndex > 0 && value[endIndex - 1] === "-") {
|
|
2131
|
+
endIndex -= 1;
|
|
2132
|
+
}
|
|
2133
|
+
return value.slice(0, endIndex);
|
|
2134
|
+
}
|
|
2135
|
+
function isWhitespace(value) {
|
|
2136
|
+
return value === " " || value === "\n" || value === "\r" || value === "\t" || value === "\f";
|
|
2137
|
+
}
|
|
1919
2138
|
function createGeneratedTypeScriptFile(bodyLines, metadataLines = []) {
|
|
1920
2139
|
return [...createGeneratedTypeScriptFileLines(metadataLines), ...bodyLines].join("\n");
|
|
1921
2140
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { defineApiCommand } from "./api-command.js";
|
|
2
2
|
export { defineClient } from "./define-client.js";
|
|
3
|
-
export { generate } from "./generate.js";
|
|
4
|
-
export type {
|
|
3
|
+
export { generate, generateSkill } from "./generate.js";
|
|
4
|
+
export type { GeneratedFile, GeneratedSkill, GenerateOptions, OpenApiDocument } from "./generate.js";
|
|
5
5
|
export { SUPPORTED_TOOLCRAFT_EDITION, mergeToolcraftConfig, readToolcraftConfig, validateToolcraftConfig } from "./config.js";
|
|
6
6
|
export type { ToolcraftConfig, ToolcraftMethodConfig, ToolcraftResourceConfig } from "./config.js";
|
|
7
7
|
export { diagnose } from "./diagnose.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { defineApiCommand } from "./api-command.js";
|
|
2
2
|
export { defineClient } from "./define-client.js";
|
|
3
|
-
export { generate } from "./generate.js";
|
|
3
|
+
export { generate, generateSkill } from "./generate.js";
|
|
4
4
|
export { SUPPORTED_TOOLCRAFT_EDITION, mergeToolcraftConfig, readToolcraftConfig, validateToolcraftConfig } from "./config.js";
|
|
5
5
|
export { diagnose } from "./diagnose.js";
|
|
6
6
|
export { DIAGNOSTIC_CODES, formatDiagnostic, formatDiagnostics } from "./diagnostics.js";
|