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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "3.0.381",
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";
@@ -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 && candidate.method.path === command.path);
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 = [...operationParams.params, ...qualifiedRequestBodyParams.params, ...transportParams];
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: [...operationParams.preflightBlocks, ...qualifiedRequestBodyParams.preflightBlocks],
370
+ preflightBlocks: [
371
+ ...operationParams.preflightBlocks,
372
+ ...qualifiedRequestBodyParams.preflightBlocks
373
+ ],
359
374
  requestFields: [...operationParams.requestFields, ...qualifiedRequestBodyParams.requestFields],
360
- sectionRenders: { ...operationParams.sectionRenders, ...qualifiedRequestBodyParams.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)) ?? 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");
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([createJsonBodyField({
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
- })], bodyOptional, requestBody.description, "inline", undefined, bodyMode, multipartBinaryFields);
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" && 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`)))) {
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: { kind: "queryArray", reference: { kind: "param", paramName }, serialization: "comma" },
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 === "*/*" || 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"));
1144
1182
  }
1145
1183
  function isTextMediaType(mediaType) {
1146
1184
  const normalized = mediaType.toLowerCase();
1147
- return normalized.startsWith("text/") || normalized === "plain/text" || normalized === "application/jwt";
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)) ? normalized : `api-${normalized || "operation"}`;
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
  }