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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "3.0.382",
3
+ "version": "3.0.383",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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, { specSha, config: effectiveConfig });
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 || updatedLockFile !== undefined || deletedFiles.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 ? updatedFiles : [...updatedFiles, updatedLockFile],
152
- updatedFileCount: updatedFiles.length + (updatedLockFile === undefined ? 0 : 1)
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 && candidate.method.path === command.path);
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 = [...operationParams.params, ...qualifiedRequestBodyParams.params, ...transportParams];
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: [...operationParams.preflightBlocks, ...qualifiedRequestBodyParams.preflightBlocks],
370
+ preflightBlocks: [
371
+ ...operationParams.preflightBlocks,
372
+ ...qualifiedRequestBodyParams.preflightBlocks
373
+ ],
356
374
  requestFields: [...operationParams.requestFields, ...qualifiedRequestBodyParams.requestFields],
357
- sectionRenders: { ...operationParams.sectionRenders, ...qualifiedRequestBodyParams.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)) ?? contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && mediaType.toLowerCase() === "application/x-www-form-urlencoded") ?? contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isTextMediaType(mediaType)) ?? contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && isBinaryMediaType(mediaType)) ?? contentEntries.find(([mediaType, mediaTypeObject]) => mediaTypeObject !== undefined && mediaType.toLowerCase() === "multipart/form-data");
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([createJsonBodyField({
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
- })], bodyOptional, requestBody.description, "inline", undefined, bodyMode, multipartBinaryFields);
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" && schema.items !== undefined && isComplexJsonBodySchema(document, schema.items, operationId, `${context} items`)))) {
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: { kind: "queryArray", reference: { kind: "param", paramName }, serialization: "comma" },
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 === "*/*" || normalized === "text/json" || normalized.includes("application/json") || normalized.includes("+json");
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/") || normalized === "plain/text" || normalized === "application/jwt";
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)) ? normalized : `api-${normalized || "operation"}`;
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 { GenerateOptions, GeneratedFile, OpenApiDocument } from "./generate.js";
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";