poe-code 3.0.381 → 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 +240 -27
- 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";
|
|
@@ -124,10 +124,7 @@ export function generate(document, options) {
|
|
|
124
124
|
return [
|
|
125
125
|
...commands.map((command) => ({
|
|
126
126
|
path: command.filePath,
|
|
127
|
-
contents: createCommandFile(
|
|
128
|
-
specSha: options.specSha,
|
|
129
|
-
...command
|
|
130
|
-
})
|
|
127
|
+
contents: createCommandFile(command)
|
|
131
128
|
})),
|
|
132
129
|
createIndexFile(commands),
|
|
133
130
|
createClientFile(),
|
|
@@ -135,6 +132,16 @@ export function generate(document, options) {
|
|
|
135
132
|
createMcpFile()
|
|
136
133
|
];
|
|
137
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
|
+
}
|
|
138
145
|
export function collectGeneratedCommands(document, config) {
|
|
139
146
|
const normalizedDocument = normalizeOpenApiDocument(document);
|
|
140
147
|
const paths = normalizedDocument.paths;
|
|
@@ -153,7 +160,8 @@ function applyConfiguredCommandShape(commands, config) {
|
|
|
153
160
|
return;
|
|
154
161
|
}
|
|
155
162
|
for (const command of commands) {
|
|
156
|
-
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);
|
|
157
165
|
if (match === undefined) {
|
|
158
166
|
continue;
|
|
159
167
|
}
|
|
@@ -343,7 +351,11 @@ function collectParams(document, entry, operation, operationId, auth, responseMo
|
|
|
343
351
|
const operationParams = collectOperationParameters(document, entry.path, entry.pathItem.parameters ?? [], operation.parameters ?? [], operationId, auth);
|
|
344
352
|
const requestBodyParams = collectRequestBodyParams(document, operation, operationId, entry.method);
|
|
345
353
|
const qualifiedRequestBodyParams = qualifyBodyParamCollisions(requestBodyParams, new Set([...operationParams.params, ...transportParams].map((param) => param.paramName)));
|
|
346
|
-
const params = [
|
|
354
|
+
const params = [
|
|
355
|
+
...operationParams.params,
|
|
356
|
+
...qualifiedRequestBodyParams.params,
|
|
357
|
+
...transportParams
|
|
358
|
+
];
|
|
347
359
|
const deduped = new Map();
|
|
348
360
|
for (const param of params) {
|
|
349
361
|
const existing = deduped.get(param.paramName);
|
|
@@ -355,9 +367,15 @@ function collectParams(document, entry, operation, operationId, auth, responseMo
|
|
|
355
367
|
return {
|
|
356
368
|
params: [...deduped.values()],
|
|
357
369
|
paramsSchemaOptions: qualifiedRequestBodyParams.paramsSchemaOptions,
|
|
358
|
-
preflightBlocks: [
|
|
370
|
+
preflightBlocks: [
|
|
371
|
+
...operationParams.preflightBlocks,
|
|
372
|
+
...qualifiedRequestBodyParams.preflightBlocks
|
|
373
|
+
],
|
|
359
374
|
requestFields: [...operationParams.requestFields, ...qualifiedRequestBodyParams.requestFields],
|
|
360
|
-
sectionRenders: {
|
|
375
|
+
sectionRenders: {
|
|
376
|
+
...operationParams.sectionRenders,
|
|
377
|
+
...qualifiedRequestBodyParams.sectionRenders
|
|
378
|
+
},
|
|
361
379
|
optionalSections: new Set([
|
|
362
380
|
...operationParams.optionalSections,
|
|
363
381
|
...qualifiedRequestBodyParams.optionalSections
|
|
@@ -498,7 +516,12 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
498
516
|
}
|
|
499
517
|
const requestBody = expectRequestBody(document, operation.requestBody, operationId, "requestBody");
|
|
500
518
|
const contentEntries = Object.entries(requestBody.content ?? {});
|
|
501
|
-
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");
|
|
502
525
|
const content = contentEntry?.[1];
|
|
503
526
|
const requestMediaType = contentEntry?.[0];
|
|
504
527
|
const bodyMode = requestMediaType?.toLowerCase() === "application/x-www-form-urlencoded"
|
|
@@ -540,7 +563,8 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
540
563
|
}
|
|
541
564
|
if ((schema.additionalProperties !== undefined && schema.additionalProperties !== false) ||
|
|
542
565
|
schema.properties === undefined) {
|
|
543
|
-
return createCollectedRequestBodyParams([
|
|
566
|
+
return createCollectedRequestBodyParams([
|
|
567
|
+
createJsonBodyField({
|
|
544
568
|
document,
|
|
545
569
|
name: "body",
|
|
546
570
|
description: schema.description ?? requestBody.description,
|
|
@@ -549,7 +573,8 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
549
573
|
operationId,
|
|
550
574
|
context: "requestBody",
|
|
551
575
|
location: "body"
|
|
552
|
-
})
|
|
576
|
+
})
|
|
577
|
+
], bodyOptional, requestBody.description, "inline", undefined, bodyMode, multipartBinaryFields);
|
|
553
578
|
}
|
|
554
579
|
const required = new Set(schema.required ?? []);
|
|
555
580
|
const assemblies = [];
|
|
@@ -568,12 +593,14 @@ function collectRequestBodyParams(document, operation, operationId, method) {
|
|
|
568
593
|
params: [],
|
|
569
594
|
paramsSchemaOptions: schema.additionalProperties === false ? { additionalProperties: false } : undefined,
|
|
570
595
|
preflightBlocks: [],
|
|
571
|
-
requestFields: [
|
|
596
|
+
requestFields: [
|
|
597
|
+
{
|
|
572
598
|
location: "body",
|
|
573
599
|
wireName: "body",
|
|
574
600
|
value: { kind: "emptyObject" },
|
|
575
601
|
omitWhenUndefinedReference: { kind: "resolved", resolvedName: "emptyBody" }
|
|
576
|
-
}
|
|
602
|
+
}
|
|
603
|
+
],
|
|
577
604
|
sectionRenders: { body: "inline" },
|
|
578
605
|
optionalSections: new Set(),
|
|
579
606
|
requestBodyDescription: requestBody.description,
|
|
@@ -636,7 +663,9 @@ function createGeneratedParameter(document, parameter, operationId, auth) {
|
|
|
636
663
|
if (parameter.in === "query" &&
|
|
637
664
|
parameter.style === "deepObject" &&
|
|
638
665
|
(getCompositionKeyword(schema) !== undefined ||
|
|
639
|
-
(schema.type === "array" &&
|
|
666
|
+
(schema.type === "array" &&
|
|
667
|
+
schema.items !== undefined &&
|
|
668
|
+
isComplexJsonBodySchema(document, schema.items, operationId, `${context} items`)))) {
|
|
640
669
|
return createJsonQueryField({
|
|
641
670
|
document,
|
|
642
671
|
name: parameter.name,
|
|
@@ -976,19 +1005,25 @@ function createPathArrayParam(options) {
|
|
|
976
1005
|
const definition = createParamDefinition(options.document, options.schema, options.operationId, options.context);
|
|
977
1006
|
const paramName = options.name;
|
|
978
1007
|
return {
|
|
979
|
-
params: [
|
|
1008
|
+
params: [
|
|
1009
|
+
{
|
|
980
1010
|
paramName,
|
|
981
1011
|
sourceName: options.name,
|
|
982
1012
|
location: "path",
|
|
983
1013
|
description: options.description,
|
|
984
1014
|
optional: false,
|
|
985
1015
|
definition
|
|
986
|
-
}
|
|
1016
|
+
}
|
|
1017
|
+
],
|
|
987
1018
|
preflightBlocks: [],
|
|
988
1019
|
requestField: {
|
|
989
1020
|
location: "path",
|
|
990
1021
|
wireName: options.name,
|
|
991
|
-
value: {
|
|
1022
|
+
value: {
|
|
1023
|
+
kind: "queryArray",
|
|
1024
|
+
reference: { kind: "param", paramName },
|
|
1025
|
+
serialization: "comma"
|
|
1026
|
+
},
|
|
992
1027
|
omitWhenUndefinedReference: { kind: "param", paramName }
|
|
993
1028
|
}
|
|
994
1029
|
};
|
|
@@ -1140,11 +1175,16 @@ function isAsciiDigit(character) {
|
|
|
1140
1175
|
}
|
|
1141
1176
|
function isJsonMediaType(mediaType) {
|
|
1142
1177
|
const normalized = mediaType.toLowerCase();
|
|
1143
|
-
return normalized === "*/*" ||
|
|
1178
|
+
return (normalized === "*/*" ||
|
|
1179
|
+
normalized === "text/json" ||
|
|
1180
|
+
normalized.includes("application/json") ||
|
|
1181
|
+
normalized.includes("+json"));
|
|
1144
1182
|
}
|
|
1145
1183
|
function isTextMediaType(mediaType) {
|
|
1146
1184
|
const normalized = mediaType.toLowerCase();
|
|
1147
|
-
return normalized.startsWith("text/") ||
|
|
1185
|
+
return (normalized.startsWith("text/") ||
|
|
1186
|
+
normalized === "plain/text" ||
|
|
1187
|
+
normalized === "application/jwt");
|
|
1148
1188
|
}
|
|
1149
1189
|
function isBinaryMediaType(mediaType) {
|
|
1150
1190
|
const normalized = mediaType.toLowerCase();
|
|
@@ -1357,8 +1397,7 @@ function normalizeNullableTypeArray(schema) {
|
|
|
1357
1397
|
}
|
|
1358
1398
|
function getCompositionKeyword(schema) {
|
|
1359
1399
|
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
1360
|
-
if (Object.prototype.hasOwnProperty.call(schema, keyword) &&
|
|
1361
|
-
schema[keyword] !== undefined) {
|
|
1400
|
+
if (Object.prototype.hasOwnProperty.call(schema, keyword) && schema[keyword] !== undefined) {
|
|
1362
1401
|
return keyword;
|
|
1363
1402
|
}
|
|
1364
1403
|
}
|
|
@@ -1488,10 +1527,7 @@ function createCommandFile(options) {
|
|
|
1488
1527
|
...(usesMultipartFileInputs ? ["prepareMultipartFileInputs"] : []),
|
|
1489
1528
|
...(usesBinaryOutput ? ["writeBinaryResponseOutput"] : [])
|
|
1490
1529
|
];
|
|
1491
|
-
const lines = createGeneratedTypeScriptFileLines([
|
|
1492
|
-
`spec-sha: ${options.specSha}`,
|
|
1493
|
-
`operation-id: ${options.operationId}`
|
|
1494
|
-
]);
|
|
1530
|
+
const lines = createGeneratedTypeScriptFileLines([`operation-id: ${options.operationId}`]);
|
|
1495
1531
|
lines.push(requiresUserError
|
|
1496
1532
|
? 'import { S, UserError } from "toolcraft";'
|
|
1497
1533
|
: 'import { S } from "toolcraft";', `import { ${openApiImports.join(", ")} } from "toolcraft-openapi";`, "", `export const ${options.exportName} = defineApiCommand({`, ` name: ${JSON.stringify(options.verb)},`);
|
|
@@ -1708,7 +1744,9 @@ function renderObjectKey(name) {
|
|
|
1708
1744
|
}
|
|
1709
1745
|
function createSafeGeneratedNoun(noun) {
|
|
1710
1746
|
const normalized = normalizeNoun(noun);
|
|
1711
|
-
return isTypeScriptIdentifier(toCamelCase(normalized))
|
|
1747
|
+
return isTypeScriptIdentifier(toCamelCase(normalized))
|
|
1748
|
+
? normalized
|
|
1749
|
+
: `api-${normalized || "operation"}`;
|
|
1712
1750
|
}
|
|
1713
1751
|
export function collectSchemaOptionEntries(param) {
|
|
1714
1752
|
return SCHEMA_OPTION_SOURCES.flatMap(({ key, get }) => {
|
|
@@ -1922,6 +1960,181 @@ function createMcpFile() {
|
|
|
1922
1960
|
].join("\n")
|
|
1923
1961
|
};
|
|
1924
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
|
+
}
|
|
1925
2138
|
function createGeneratedTypeScriptFile(bodyLines, metadataLines = []) {
|
|
1926
2139
|
return [...createGeneratedTypeScriptFileLines(metadataLines), ...bodyLines].join("\n");
|
|
1927
2140
|
}
|