swagger-digest 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,505 @@
1
+ // src/version.ts
2
+ import { createRequire } from "module";
3
+ function readPackageVersion(moduleUrl) {
4
+ const require2 = createRequire(moduleUrl);
5
+ for (const candidate of ["../package.json", "../../package.json"]) {
6
+ try {
7
+ const pkg = require2(candidate);
8
+ if (typeof pkg?.version === "string" && pkg.version) return pkg.version;
9
+ } catch {
10
+ }
11
+ }
12
+ return "unknown";
13
+ }
14
+ var VERSION = readPackageVersion(import.meta.url);
15
+ var USER_AGENT = `swagger-digest/${VERSION}`;
16
+
17
+ // src/transformer/utils.ts
18
+ function isObject(value) {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value);
20
+ }
21
+ function entries(value) {
22
+ return isObject(value) ? Object.entries(value) : [];
23
+ }
24
+ function cleanText(value) {
25
+ return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
26
+ }
27
+ function markdownCell(value) {
28
+ return cleanText(value).replace(/\|/g, "\\|").replace(/`/g, "\\`") || "\u2014";
29
+ }
30
+ function safeSourceUrl(value) {
31
+ try {
32
+ const url = new URL(value);
33
+ url.username = "";
34
+ url.password = "";
35
+ for (const key of [...url.searchParams.keys()]) {
36
+ if (/(token|secret|password|signature|credential|api[-_]?key|authorization|auth)/i.test(key)) {
37
+ url.searchParams.set(key, "[redacted]");
38
+ }
39
+ }
40
+ return url.toString();
41
+ } catch {
42
+ return value;
43
+ }
44
+ }
45
+ function safeComment(value) {
46
+ return cleanText(value).replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
47
+ }
48
+ function propertyKey(value) {
49
+ return /^[$A-Z_a-z][$\w]*$/.test(value) ? value : JSON.stringify(value);
50
+ }
51
+ function toTypeScriptIdentifier(value) {
52
+ let identifier = value.replace(/[^\p{L}\p{N}_$]+/gu, "_").replace(/^([^\p{L}_$])/u, "_$1");
53
+ if (!identifier) identifier = "Unnamed";
54
+ const reserved = /* @__PURE__ */ new Set([
55
+ "abstract",
56
+ "any",
57
+ "as",
58
+ "asserts",
59
+ "async",
60
+ "await",
61
+ "boolean",
62
+ "break",
63
+ "case",
64
+ "catch",
65
+ "class",
66
+ "const",
67
+ "constructor",
68
+ "continue",
69
+ "debugger",
70
+ "declare",
71
+ "default",
72
+ "delete",
73
+ "do",
74
+ "else",
75
+ "enum",
76
+ "export",
77
+ "extends",
78
+ "false",
79
+ "finally",
80
+ "for",
81
+ "from",
82
+ "function",
83
+ "get",
84
+ "if",
85
+ "implements",
86
+ "import",
87
+ "in",
88
+ "infer",
89
+ "instanceof",
90
+ "interface",
91
+ "is",
92
+ "keyof",
93
+ "let",
94
+ "module",
95
+ "namespace",
96
+ "never",
97
+ "new",
98
+ "null",
99
+ "number",
100
+ "object",
101
+ "of",
102
+ "package",
103
+ "private",
104
+ "protected",
105
+ "public",
106
+ "readonly",
107
+ "require",
108
+ "return",
109
+ "set",
110
+ "static",
111
+ "string",
112
+ "super",
113
+ "switch",
114
+ "symbol",
115
+ "this",
116
+ "throw",
117
+ "true",
118
+ "try",
119
+ "type",
120
+ "typeof",
121
+ "undefined",
122
+ "unique",
123
+ "unknown",
124
+ "var",
125
+ "void",
126
+ "while",
127
+ "with",
128
+ "yield"
129
+ ]);
130
+ if (reserved.has(identifier)) identifier = `${identifier}Model`;
131
+ return identifier;
132
+ }
133
+
1
134
  // src/fetcher.ts
2
135
  import { load } from "cheerio";
136
+ import { parse as parseYaml2 } from "yaml";
137
+
138
+ // src/errors.ts
139
+ var FetchError = class extends Error {
140
+ kind;
141
+ constructor(kind, message) {
142
+ super(message);
143
+ this.name = "FetchError";
144
+ this.kind = kind;
145
+ }
146
+ };
147
+ var FetchSizeError = class extends FetchError {
148
+ constructor(message) {
149
+ super("size", message);
150
+ this.name = "FetchSizeError";
151
+ }
152
+ };
153
+ var BlockedNetworkError = class extends FetchError {
154
+ constructor(message) {
155
+ super("blocked", message);
156
+ this.name = "BlockedNetworkError";
157
+ }
158
+ };
159
+ var FetchNetworkError = class extends FetchError {
160
+ constructor(message) {
161
+ super("network", message);
162
+ this.name = "FetchNetworkError";
163
+ }
164
+ };
165
+ var FetchHttpError = class extends FetchError {
166
+ status;
167
+ constructor(message, status) {
168
+ super("http", message);
169
+ this.name = "FetchHttpError";
170
+ this.status = status;
171
+ }
172
+ };
173
+ function isFailFastFetchError(error) {
174
+ return error instanceof FetchError && (error.kind === "size" || error.kind === "blocked" || error.kind === "network");
175
+ }
176
+
177
+ // src/hardened-fetch.ts
178
+ import { lookup } from "dns/promises";
179
+ import { isIP } from "net";
180
+ var DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
181
+ var DEFAULT_MAX_REDIRECTS = 5;
182
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
183
+ function isPrivateAddress(address) {
184
+ const family = isIP(address);
185
+ if (family === 4) return isPrivateIPv4(address);
186
+ if (family === 6) {
187
+ const normalized = address.toLowerCase();
188
+ const mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
189
+ if (mapped?.[1]) return isPrivateIPv4(mapped[1]);
190
+ if (normalized === "::1" || normalized === "::") return true;
191
+ if (/^f[cd][0-9a-f]{2}(?::|$)/.test(normalized)) return true;
192
+ if (/^fe[89ab][0-9a-f](?::|$)/.test(normalized)) return true;
193
+ return false;
194
+ }
195
+ return true;
196
+ }
197
+ function isPrivateIPv4(address) {
198
+ const parts = address.split(".").map(Number);
199
+ if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) return true;
200
+ const [a, b] = parts;
201
+ if (a === 0) return true;
202
+ if (a === 10) return true;
203
+ if (a === 127) return true;
204
+ if (a === 169 && b === 254) return true;
205
+ if (a === 172 && b >= 16 && b <= 31) return true;
206
+ if (a === 192 && b === 168) return true;
207
+ return false;
208
+ }
209
+ async function assertPublicHost(hostname) {
210
+ if (isIP(hostname) !== 0) {
211
+ if (isPrivateAddress(hostname)) {
212
+ throw new BlockedNetworkError(
213
+ `Blocked request to private network address (host resolved to a loopback/private/metadata address). Enable this only for hosts you trust; use it against internal corporate APIs without this option.`
214
+ );
215
+ }
216
+ return;
217
+ }
218
+ let addresses;
219
+ try {
220
+ addresses = await lookup(hostname, { all: true, verbatim: true });
221
+ } catch (error) {
222
+ throw new FetchNetworkError(
223
+ `DNS resolution failed for ${hostname}: ${error instanceof Error ? error.message : String(error)}`
224
+ );
225
+ }
226
+ if (!addresses.length) {
227
+ throw new FetchNetworkError(`DNS resolution returned no addresses for ${hostname}`);
228
+ }
229
+ for (const { address } of addresses) {
230
+ if (isPrivateAddress(address)) {
231
+ throw new BlockedNetworkError(
232
+ `Blocked request to ${hostname}: it resolves to a private/loopback/metadata address while blockPrivateNetworks is enabled.`
233
+ );
234
+ }
235
+ }
236
+ }
237
+ function normalizeHeaders(headers) {
238
+ return Object.fromEntries(Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]));
239
+ }
240
+ async function fetchWithLimits(url, options) {
241
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
242
+ const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
243
+ const defaultHeaders = normalizeHeaders({
244
+ accept: "application/json, application/yaml, text/yaml, text/plain, text/html;q=0.8, */*;q=0.5",
245
+ "user-agent": USER_AGENT
246
+ });
247
+ const userHeaders = normalizeHeaders(options.headers ?? {});
248
+ return await follow(url, defaultHeaders, userHeaders, options, maxBytes, maxRedirects);
249
+ }
250
+ async function follow(url, defaultHeaders, userHeaders, options, maxBytes, redirectsLeft) {
251
+ let target;
252
+ try {
253
+ target = new URL(url);
254
+ } catch {
255
+ throw new FetchNetworkError(`Invalid URL: ${url}`);
256
+ }
257
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
258
+ throw new FetchNetworkError(`Only HTTP and HTTPS URLs are supported: ${url}`);
259
+ }
260
+ if (options.blockPrivateNetworks) {
261
+ await assertPublicHost(target.hostname);
262
+ }
263
+ const sameOrigin = !options.allowedHeaderOrigin || !options.headers || target.origin === options.allowedHeaderOrigin;
264
+ if (!sameOrigin && options.allowedHeaderOrigin && options.headers && Object.keys(options.headers).length > 0) {
265
+ options.onCrossOriginHeadersDropped?.(target.origin);
266
+ }
267
+ const requestHeaders = sameOrigin ? { ...defaultHeaders, ...userHeaders } : defaultHeaders;
268
+ const controller = new AbortController();
269
+ const timeout = AbortSignal.timeout(options.timeoutMs);
270
+ timeout.addEventListener("abort", () => controller.abort(), { once: true });
271
+ let response;
272
+ try {
273
+ response = await fetch(target, {
274
+ headers: requestHeaders,
275
+ redirect: "manual",
276
+ signal: controller.signal
277
+ });
278
+ } catch (error) {
279
+ if (error instanceof FetchError) throw error;
280
+ const reason = error instanceof Error ? error.message : String(error);
281
+ throw new FetchNetworkError(`Request to ${target} failed: ${describeNetworkError(reason)}`);
282
+ }
283
+ if (REDIRECT_STATUSES.has(response.status)) {
284
+ const location = response.headers.get("location");
285
+ if (!location) {
286
+ throw new FetchNetworkError(`Redirect response without a Location header from ${target}`);
287
+ }
288
+ if (redirectsLeft <= 0) {
289
+ throw new FetchNetworkError(`Too many redirects (limit ${options.maxRedirects ?? 5}) while fetching ${url}`);
290
+ }
291
+ try {
292
+ await response.arrayBuffer();
293
+ } catch {
294
+ }
295
+ const next = new URL(response.headers.get("location") ?? "", target);
296
+ return follow(next.toString(), defaultHeaders, userHeaders, options, maxBytes, redirectsLeft - 1);
297
+ }
298
+ if (!response.ok) {
299
+ await discardBodySafely(response);
300
+ throw new FetchHttpError(`HTTP ${response.status} ${response.statusText} for ${url}`, response.status);
301
+ }
302
+ const contentLength = response.headers.get("content-length");
303
+ if (contentLength !== null) {
304
+ const declared = Number(contentLength);
305
+ if (Number.isFinite(declared) && declared > maxBytes) {
306
+ await discardBodySafely(response);
307
+ throw new FetchSizeError(
308
+ `Response from ${target} is ${declared} bytes, exceeding the ${maxBytes} byte limit (content-length). Increase --max-bytes if the spec is genuinely large.`
309
+ );
310
+ }
311
+ }
312
+ const text = await readBodyWithCap(response, target.toString(), maxBytes);
313
+ return {
314
+ text,
315
+ contentType: response.headers.get("content-type") ?? "",
316
+ status: response.status,
317
+ url: target.toString()
318
+ };
319
+ }
320
+ function describeNetworkError(reason) {
321
+ if (/timeouterror|timed out|aborted/i.test(reason)) return "request timed out";
322
+ if (/enotfound|getaddrinfo/i.test(reason)) return "host not found (DNS)";
323
+ if (/econnrefused/i.test(reason)) return "connection refused";
324
+ return reason;
325
+ }
326
+ async function readBodyWithCap(response, url, maxBytes) {
327
+ const body = response.body;
328
+ if (!body) return "";
329
+ const chunks = [];
330
+ let total = 0;
331
+ try {
332
+ for await (const chunk of body) {
333
+ chunks.push(chunk);
334
+ total += chunk.byteLength;
335
+ if (total > maxBytes) {
336
+ try {
337
+ await body.cancel();
338
+ } catch {
339
+ }
340
+ throw new FetchSizeError(
341
+ `Response body from ${url} exceeded the ${maxBytes} byte limit while streaming (content-length was ${response.headers.get("content-length") ?? "not declared"}). Use --max-bytes to raise the cap if the spec is genuinely large.`
342
+ );
343
+ }
344
+ }
345
+ } catch (error) {
346
+ if (error instanceof FetchSizeError) throw error;
347
+ throw new FetchNetworkError(`Failed to read response body from ${url}: ${describeNetworkError(String(error))}`);
348
+ }
349
+ const decoder = new TextDecoder("utf-8");
350
+ const parts = [];
351
+ for (const chunk of chunks) parts.push(decoder.decode(chunk, { stream: true }));
352
+ parts.push(decoder.decode());
353
+ return parts.join("");
354
+ }
355
+ async function discardBodySafely(response) {
356
+ try {
357
+ await response.arrayBuffer();
358
+ } catch {
359
+ }
360
+ }
361
+
362
+ // src/bundler.ts
3
363
  import { parse as parseYaml } from "yaml";
364
+ var MAX_EXTERNAL_DOCUMENTS = 5;
365
+ async function bundleExternalRefs(spec, sourceUrl, fetchDocument) {
366
+ if (!sourceUrl) return spec;
367
+ const externalRefs = collectExternalRefs(spec);
368
+ if (externalRefs.size === 0) return spec;
369
+ const schemaBucket = openSchemaBucket(spec);
370
+ const usedNames = new Set(Object.keys(schemaBucket));
371
+ const mapping = /* @__PURE__ */ new Map();
372
+ let fetched = 0;
373
+ for (const ref of externalRefs) {
374
+ if (fetched >= MAX_EXTERNAL_DOCUMENTS) break;
375
+ const { docUrl, pointer } = splitExternalRef(ref, sourceUrl);
376
+ if (!docUrl) continue;
377
+ try {
378
+ const text = await fetchDocument(docUrl);
379
+ const parsed = parseDocumentText(text);
380
+ if (parsed === void 0) continue;
381
+ fetched += 1;
382
+ const target = resolvePointer(parsed, pointer);
383
+ if (target === void 0) continue;
384
+ const localName = uniqueNameFor(localNameFor(docUrl, pointer), usedNames);
385
+ schemaBucket[localName] = target;
386
+ mapping.set(ref, localName);
387
+ } catch {
388
+ }
389
+ }
390
+ if (mapping.size === 0) return spec;
391
+ const rewritten = rewriteRefs(spec, mapping);
392
+ return isObject(rewritten) ? rewritten : spec;
393
+ }
394
+ function collectExternalRefs(node) {
395
+ const found = /* @__PURE__ */ new Set();
396
+ const walk = (value) => {
397
+ if (Array.isArray(value)) {
398
+ for (const item of value) walk(item);
399
+ return;
400
+ }
401
+ if (!isObject(value)) return;
402
+ for (const [key, refValue] of Object.entries(value)) {
403
+ if (key === "$ref" && typeof refValue === "string" && !refValue.startsWith("#")) {
404
+ found.add(refValue);
405
+ continue;
406
+ }
407
+ if (key === "example" || key === "examples" || key === "externalDocs") continue;
408
+ walk(refValue);
409
+ }
410
+ };
411
+ walk(node);
412
+ return found;
413
+ }
414
+ function rewriteRefs(node, mapping) {
415
+ if (Array.isArray(node)) return node.map((item) => rewriteRefs(item, mapping));
416
+ if (!isObject(node)) return node;
417
+ const result = {};
418
+ for (const [key, value] of Object.entries(node)) {
419
+ if (key === "$ref" && typeof value === "string") {
420
+ const localName = mapping.get(value);
421
+ if (localName === void 0) {
422
+ result[key] = value;
423
+ continue;
424
+ }
425
+ result[key] = `#/${bucketPath(bucketStyle(value))}/${localName}`;
426
+ continue;
427
+ }
428
+ result[key] = rewriteRefs(value, mapping);
429
+ }
430
+ return result;
431
+ }
432
+ function bucketStyle(ref) {
433
+ return ref.includes("/components/") ? "openapi" : "swagger";
434
+ }
435
+ function bucketPath(style) {
436
+ return style === "openapi" ? "components/schemas" : "definitions";
437
+ }
438
+ function openSchemaBucket(spec) {
439
+ if (isObject(spec.components)) {
440
+ if (!isObject(spec.components.schemas)) spec.components.schemas = {};
441
+ return spec.components.schemas;
442
+ }
443
+ if (!isObject(spec.definitions)) spec.definitions = {};
444
+ return spec.definitions;
445
+ }
446
+ function splitExternalRef(ref, baseUrl) {
447
+ const hashIndex = ref.indexOf("#");
448
+ if (hashIndex < 0) return { docUrl: void 0, pointer: "" };
449
+ const docPart = ref.slice(0, hashIndex);
450
+ const pointer = ref.slice(hashIndex + 1);
451
+ try {
452
+ return { docUrl: new URL(docPart, baseUrl).toString(), pointer };
453
+ } catch {
454
+ return { docUrl: void 0, pointer };
455
+ }
456
+ }
457
+ function parseDocumentText(text) {
458
+ try {
459
+ return JSON.parse(text);
460
+ } catch {
461
+ try {
462
+ return parseYaml(text);
463
+ } catch {
464
+ return void 0;
465
+ }
466
+ }
467
+ }
468
+ function resolvePointer(root, pointer) {
469
+ if (!pointer.startsWith("/")) return void 0;
470
+ let current = root;
471
+ for (const rawPart of pointer.slice(1).split("/")) {
472
+ const part = rawPart.replace(/~1/g, "/").replace(/~0/g, "~");
473
+ if (!isObject(current)) return void 0;
474
+ current = current[part];
475
+ }
476
+ return current;
477
+ }
478
+ function localNameFor(docUrl, pointer) {
479
+ try {
480
+ const url = new URL(docUrl);
481
+ const file = url.pathname.split("/").pop() ?? "external";
482
+ const stem = file.replace(/\.(json|ya?ml)$/i, "").replace(/[^A-Za-z0-9_$]+/g, "_") || "external";
483
+ const segments = pointer.split("/").filter(Boolean);
484
+ const pointerName = segments.length > 0 ? segments[segments.length - 1] : void 0;
485
+ const cleanedPointer = pointerName?.replace(/[^A-Za-z0-9_$]+/g, "_");
486
+ return [stem, cleanedPointer].filter(Boolean).join("_") || "external";
487
+ } catch {
488
+ return "external";
489
+ }
490
+ }
491
+ function uniqueNameFor(base, used) {
492
+ let candidate = base;
493
+ let suffix = 2;
494
+ while (used.has(candidate)) {
495
+ candidate = `${candidate}${suffix}`;
496
+ suffix += 1;
497
+ }
498
+ used.add(candidate);
499
+ return candidate;
500
+ }
501
+
502
+ // src/fetcher.ts
4
503
  var SPEC_FILE_EXTENSION = /\.(?:json|ya?ml)$/i;
5
504
  var FALLBACK_PATHS = ["/v3/api-docs", "/swagger/v1/swagger.json", "/swagger.json", "/openapi.json", "/api-docs"];
6
505
  function isRecord(value) {
@@ -14,7 +513,7 @@ function isOpenApiSpec(value) {
14
513
  return hasVersion && hasInfo && hasPaths;
15
514
  }
16
515
  function parseSpecText(text, source = "response") {
17
- const parsers = [() => JSON.parse(text), () => parseYaml(text)];
516
+ const parsers = [() => JSON.parse(text), () => parseYaml2(text)];
18
517
  const errors = [];
19
518
  for (const parser of parsers) {
20
519
  try {
@@ -30,62 +529,164 @@ function parseSpecText(text, source = "response") {
30
529
  function isDirectSpecUrl(url) {
31
530
  return SPEC_FILE_EXTENSION.test(url.pathname);
32
531
  }
33
- async function fetchText(url, timeoutMs) {
34
- const response = await fetch(url, {
35
- headers: {
36
- accept: "application/json, application/yaml, text/yaml, text/plain, text/html;q=0.8, */*;q=0.5",
37
- "user-agent": "swagger-digest/0.1.0"
38
- },
39
- signal: AbortSignal.timeout(timeoutMs),
40
- redirect: "follow"
532
+ async function fetchText(url, ctx) {
533
+ const response = await fetchWithLimits(url, {
534
+ timeoutMs: ctx.timeoutMs,
535
+ maxBytes: ctx.maxBytes,
536
+ maxRedirects: ctx.maxRedirects,
537
+ blockPrivateNetworks: ctx.blockPrivateNetworks,
538
+ headers: ctx.headers,
539
+ allowedHeaderOrigin: ctx.allowedHeaderOrigin,
540
+ onCrossOriginHeadersDropped: ctx.onCrossOriginHeadersDropped
41
541
  });
42
- if (!response.ok) {
43
- throw new Error(`HTTP ${response.status} ${response.statusText} for ${url}`);
44
- }
45
542
  return {
46
- text: await response.text(),
47
- contentType: response.headers.get("content-type") ?? ""
543
+ text: response.text,
544
+ contentType: response.contentType
48
545
  };
49
546
  }
50
547
  function extractScripts(html) {
51
548
  const $ = load(html);
52
549
  const inline = [];
53
550
  const external = [];
54
- $("script").each((_index, element) => {
551
+ for (const element of $("script").toArray()) {
55
552
  const body = $(element).html();
56
553
  if (body?.trim()) inline.push(body);
57
554
  const src = $(element).attr("src");
58
555
  if (src?.trim()) external.push(src.trim());
59
- });
556
+ }
60
557
  return { inline, external };
61
558
  }
62
559
  function extractSpecUrlFromScripts(scripts, pageUrl) {
63
- const urlPattern = /\burl\s*:\s*(["'])(.*?)\1/s;
64
- for (const script of scripts) {
65
- const match = urlPattern.exec(script);
66
- if (!match?.[2]) continue;
67
- const candidate = match[2].trim();
68
- if (!candidate || candidate.startsWith("data:")) continue;
560
+ return extractSpecUrlsFromScripts(scripts, pageUrl)[0];
561
+ }
562
+ function extractSpecUrlsFromScripts(scripts, pageUrl) {
563
+ const found = [];
564
+ const pushCandidate = (candidate) => {
565
+ const trimmed = candidate.trim();
566
+ if (!trimmed || trimmed.startsWith("data:")) return;
69
567
  try {
70
- return new URL(candidate, pageUrl).toString();
568
+ const resolved = new URL(trimmed, pageUrl).toString();
569
+ if (!found.includes(resolved)) found.push(resolved);
71
570
  } catch {
72
571
  }
572
+ };
573
+ for (const script of scripts) {
574
+ const urlsArray = findObjectLiteralArrayAfterKey(script, "urls");
575
+ if (urlsArray) {
576
+ const entryPattern = /\burl\s*:\s*(["'])(.*?)\1/gs;
577
+ for (const match of urlsArray.matchAll(entryPattern)) {
578
+ pushCandidate(match[2] ?? "");
579
+ }
580
+ }
581
+ if (!urlsArray) {
582
+ const urlPattern = /\burl\s*:\s*(["'])(.*?)\1/s;
583
+ const match = urlPattern.exec(script);
584
+ if (match?.[2]) pushCandidate(match[2]);
585
+ }
586
+ }
587
+ return found;
588
+ }
589
+ function findObjectLiteralArrayAfterKey(source, key) {
590
+ const keyPattern = new RegExp(`\\b${key}\\s*:`);
591
+ const match = keyPattern.exec(source);
592
+ if (!match) return void 0;
593
+ let index = match.index + match[0].length;
594
+ while (/\s/.test(source[index] ?? "")) index += 1;
595
+ if (source[index] !== "[") return void 0;
596
+ const start = index;
597
+ let depth = 0;
598
+ let quote;
599
+ let escaped = false;
600
+ let lineComment = false;
601
+ let blockComment = false;
602
+ for (; index < source.length; index += 1) {
603
+ const char = source[index] ?? "";
604
+ const next = source[index + 1] ?? "";
605
+ if (lineComment) {
606
+ if (char === "\n") lineComment = false;
607
+ continue;
608
+ }
609
+ if (blockComment) {
610
+ if (char === "*" && next === "/") {
611
+ blockComment = false;
612
+ index += 1;
613
+ }
614
+ continue;
615
+ }
616
+ if (quote) {
617
+ if (escaped) {
618
+ escaped = false;
619
+ } else if (char === "\\") {
620
+ escaped = true;
621
+ } else if (char === quote) {
622
+ quote = void 0;
623
+ }
624
+ continue;
625
+ }
626
+ if (char === "/" && next === "/") {
627
+ lineComment = true;
628
+ index += 1;
629
+ continue;
630
+ }
631
+ if (char === "/" && next === "*") {
632
+ blockComment = true;
633
+ index += 1;
634
+ continue;
635
+ }
636
+ if (char === '"' || char === "'" || char === "`") {
637
+ quote = char;
638
+ continue;
639
+ }
640
+ if (char === "[") depth += 1;
641
+ if (char === "]") {
642
+ depth -= 1;
643
+ if (depth === 0) return source.slice(start, index + 1);
644
+ }
73
645
  }
74
646
  return void 0;
75
647
  }
76
- function extractSpecUrl(scripts, pageUrl) {
77
- const fromScript = extractSpecUrlFromScripts(scripts, pageUrl);
78
- if (fromScript) return fromScript;
648
+ function extractSpecUrls(scripts, pageUrl) {
649
+ const fromScripts = extractSpecUrlsFromScripts(scripts, pageUrl);
79
650
  const queryUrl = pageUrl.searchParams.get("url");
80
651
  if (queryUrl) {
81
652
  try {
82
- return new URL(queryUrl, pageUrl).toString();
653
+ const resolved = new URL(queryUrl, pageUrl).toString();
654
+ if (!fromScripts.includes(resolved)) fromScripts.push(resolved);
83
655
  } catch {
84
656
  }
85
657
  }
658
+ return fromScripts;
659
+ }
660
+ async function probeSpecUrls(candidates, ctx, recordFailure, onMultiple) {
661
+ if (candidates.length === 0) return void 0;
662
+ if (candidates.length > 1) {
663
+ onMultiple?.(candidates[0] ?? "", candidates);
664
+ }
665
+ const batchSize = 4;
666
+ for (let start = 0; start < candidates.length; start += batchSize) {
667
+ const batch = candidates.slice(start, start + batchSize);
668
+ const attempts = await Promise.all(
669
+ batch.map(async (candidate) => {
670
+ try {
671
+ return { candidate, spec: await tryFetchSpec(candidate, ctx) };
672
+ } catch (error) {
673
+ return { candidate, error };
674
+ }
675
+ })
676
+ );
677
+ const winner = attempts.find(
678
+ (attempt) => attempt.spec !== void 0
679
+ );
680
+ if (winner) {
681
+ return { spec: winner.spec, sourceUrl: winner.candidate, retrievalMethod: "html-config" };
682
+ }
683
+ for (const attempt of attempts) {
684
+ recordFailure("Configured spec", attempt.error, attempt.candidate);
685
+ }
686
+ }
86
687
  return void 0;
87
688
  }
88
- async function fetchExternalConfigScripts(srcs, pageUrl, timeoutMs, errors) {
689
+ async function fetchExternalConfigScripts(srcs, pageUrl, ctx, errors) {
89
690
  const candidates = srcs.map((src) => {
90
691
  try {
91
692
  return new URL(src, pageUrl);
@@ -100,7 +701,7 @@ async function fetchExternalConfigScripts(srcs, pageUrl, timeoutMs, errors) {
100
701
  const scripts = [];
101
702
  for (const candidate of candidates.slice(0, 10)) {
102
703
  try {
103
- const response = await fetchText(candidate.toString(), timeoutMs);
704
+ const response = await fetchText(candidate.toString(), ctx);
104
705
  scripts.push(response.text);
105
706
  if (extractSpecUrlFromScripts(scripts, pageUrl) || scripts.some((script) => findObjectLiteralAfterKey(script, "spec"))) {
106
707
  break;
@@ -195,7 +796,7 @@ function findObjectLiteralAfterKey(source, key) {
195
796
  function parseInlineObjectLiteral(literal) {
196
797
  const candidates = [
197
798
  () => JSON.parse(literal),
198
- () => parseYaml(literal),
799
+ () => parseYaml2(literal),
199
800
  // Swagger config often uses JS object syntax with trailing commas.
200
801
  () => JSON.parse(literal.replace(/,\s*([}\]])/g, "$1"))
201
802
  ];
@@ -217,11 +818,26 @@ function extractInlineSpec(scripts) {
217
818
  }
218
819
  return void 0;
219
820
  }
220
- async function tryFetchSpec(url, timeoutMs) {
221
- const response = await fetchText(url, timeoutMs);
222
- return parseSpecText(response.text, url);
821
+ async function tryFetchSpec(url, ctx) {
822
+ const response = await fetchText(url, ctx);
823
+ const parsed = parseSpecText(response.text, url);
824
+ try {
825
+ return await bundleExternalRefs(parsed, url, async (docUrl) => (await fetchText(docUrl, ctx)).text);
826
+ } catch {
827
+ return parsed;
828
+ }
829
+ }
830
+ function headersForBrowserRequest(requestUrl, headers, allowedOrigin) {
831
+ if (!headers || Object.keys(headers).length === 0) return void 0;
832
+ let requestOrigin;
833
+ try {
834
+ requestOrigin = new URL(requestUrl).origin;
835
+ } catch {
836
+ return void 0;
837
+ }
838
+ return requestOrigin === allowedOrigin ? headers : void 0;
223
839
  }
224
- async function tryBrowserFallback(pageUrl) {
840
+ async function tryBrowserFallback(pageUrl, headers) {
225
841
  let playwright;
226
842
  try {
227
843
  const optionalModuleName = "playwright";
@@ -234,6 +850,20 @@ async function tryBrowserFallback(pageUrl) {
234
850
  const browser = await playwright.chromium.launch({ headless: true });
235
851
  try {
236
852
  const page = await browser.newPage();
853
+ const allowedOrigin = new URL(pageUrl).origin;
854
+ if (headers && Object.keys(headers).length > 0) {
855
+ const browserHeaders = Object.fromEntries(
856
+ Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value])
857
+ );
858
+ await page.route("**/*", async (route) => {
859
+ const extra = headersForBrowserRequest(route.request().url(), browserHeaders, allowedOrigin);
860
+ if (!extra) {
861
+ await route.continue();
862
+ return;
863
+ }
864
+ await route.continue({ headers: { ...route.request().headers(), ...extra } });
865
+ });
866
+ }
237
867
  await page.goto(pageUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
238
868
  await page.waitForFunction(
239
869
  () => {
@@ -241,7 +871,8 @@ async function tryBrowserFallback(pageUrl) {
241
871
  try {
242
872
  const candidate = w.ui?.spec?.();
243
873
  const document = candidate && typeof candidate.toJSON === "function" ? candidate.toJSON() : candidate;
244
- return Boolean(document?.openapi || document?.swagger || w.swaggerDoc?.openapi || w.swaggerDoc?.swagger);
874
+ const doc = document;
875
+ return Boolean(doc?.openapi || doc?.swagger || w.swaggerDoc?.openapi || w.swaggerDoc?.swagger);
245
876
  } catch {
246
877
  return Boolean(w.swaggerDoc?.openapi || w.swaggerDoc?.swagger);
247
878
  }
@@ -286,18 +917,42 @@ async function fetchOpenApiSpec(input, options = {}) {
286
917
  throw new Error("Only HTTP and HTTPS URLs are supported.");
287
918
  }
288
919
  const timeoutMs = options.timeoutMs ?? 15e3;
920
+ const ctx = {
921
+ timeoutMs,
922
+ maxBytes: options.maxBytes,
923
+ maxRedirects: options.maxRedirects,
924
+ blockPrivateNetworks: options.blockPrivateNetworks,
925
+ allowedHeaderOrigin: pageUrl.origin,
926
+ headers: options.headers,
927
+ onCrossOriginHeadersDropped: (origin) => options.warn?.(
928
+ `Custom headers were withheld: ${origin} is a different origin from the URL you supplied. Only tool defaults were sent.`
929
+ )
930
+ };
289
931
  const errors = [];
932
+ const recordFailure = (label, error, subject) => {
933
+ if (isFailFastFetchError(error)) throw error;
934
+ const prefix = subject ? `${label} (${subject}):` : `${label}:`;
935
+ errors.push(`${prefix} ${error instanceof Error ? error.message : String(error)}`);
936
+ };
290
937
  if (isDirectSpecUrl(pageUrl)) {
291
938
  try {
292
- const spec = await tryFetchSpec(pageUrl.toString(), timeoutMs);
939
+ const spec = await tryFetchSpec(pageUrl.toString(), ctx);
293
940
  return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "direct-spec" };
294
941
  } catch (error) {
295
- errors.push(`Direct spec: ${error instanceof Error ? error.message : String(error)}`);
942
+ recordFailure("Direct spec", error);
943
+ }
944
+ }
945
+ if (options.specUrl) {
946
+ try {
947
+ const spec = await tryFetchSpec(options.specUrl, ctx);
948
+ return { spec, sourceUrl: options.specUrl, retrievalMethod: "direct-spec" };
949
+ } catch (error) {
950
+ recordFailure("Specified spec (--spec)", error, options.specUrl);
296
951
  }
297
952
  }
298
953
  let html;
299
954
  try {
300
- const response = await fetchText(pageUrl.toString(), timeoutMs);
955
+ const response = await fetchText(pageUrl.toString(), ctx);
301
956
  try {
302
957
  const spec = parseSpecText(response.text, pageUrl.toString());
303
958
  return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "direct-spec" };
@@ -305,35 +960,29 @@ async function fetchOpenApiSpec(input, options = {}) {
305
960
  html = response.text;
306
961
  }
307
962
  } catch (error) {
308
- errors.push(`Swagger UI page: ${error instanceof Error ? error.message : String(error)}`);
963
+ recordFailure("Swagger UI page", error);
309
964
  }
310
965
  if (html !== void 0) {
311
966
  const pageScripts = extractScripts(html);
312
- let scripts = pageScripts.inline;
313
- let specUrl = extractSpecUrl(scripts, pageUrl);
967
+ const scripts = pageScripts.inline;
314
968
  const inlineSpec = extractInlineSpec(scripts);
315
969
  if (inlineSpec) {
316
970
  return { spec: inlineSpec, sourceUrl: pageUrl.toString(), retrievalMethod: "inline-spec" };
317
971
  }
318
- if (specUrl) {
319
- try {
320
- const spec = await tryFetchSpec(specUrl, timeoutMs);
321
- return { spec, sourceUrl: specUrl, retrievalMethod: "html-config" };
322
- } catch (error) {
323
- errors.push(`Configured spec (${specUrl}): ${error instanceof Error ? error.message : String(error)}`);
324
- }
325
- }
326
- const externalScripts = await fetchExternalConfigScripts(pageScripts.external, pageUrl, timeoutMs, errors);
327
- scripts = [...scripts, ...externalScripts];
328
- specUrl = extractSpecUrl(scripts, pageUrl);
329
- if (specUrl && specUrl !== pageUrl.searchParams.get("url")) {
330
- try {
331
- const spec = await tryFetchSpec(specUrl, timeoutMs);
332
- return { spec, sourceUrl: specUrl, retrievalMethod: "html-config" };
333
- } catch (error) {
334
- errors.push(`Configured spec (${specUrl}): ${error instanceof Error ? error.message : String(error)}`);
972
+ const noteMultiple = (picked, all) => {
973
+ if (all.length > 1) {
974
+ options.warn?.(
975
+ `Multiple spec definitions found (${String(all.length)}); using ${picked}. Others: ${all.filter((url) => url !== picked).join(", ")}.`
976
+ );
335
977
  }
336
- }
978
+ };
979
+ const fromInline = await probeSpecUrls(extractSpecUrls(scripts, pageUrl), ctx, recordFailure, noteMultiple);
980
+ if (fromInline) return fromInline;
981
+ const externalScripts = await fetchExternalConfigScripts(pageScripts.external, pageUrl, ctx, errors);
982
+ const inlineUrls = extractSpecUrls(scripts, pageUrl);
983
+ const externalUrls = extractSpecUrls(externalScripts, pageUrl).filter((url) => !inlineUrls.includes(url));
984
+ const fromExternal = await probeSpecUrls([...inlineUrls, ...externalUrls], ctx, recordFailure, noteMultiple);
985
+ if (fromExternal) return fromExternal;
337
986
  for (const script of scripts) {
338
987
  const literal = findObjectLiteralAfterKey(script, "spec");
339
988
  if (!literal) continue;
@@ -341,17 +990,33 @@ async function fetchOpenApiSpec(input, options = {}) {
341
990
  if (spec) return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "inline-spec" };
342
991
  }
343
992
  }
344
- for (const candidate of getFallbackUrls(pageUrl)) {
345
- try {
346
- const spec = await tryFetchSpec(candidate, timeoutMs);
347
- return { spec, sourceUrl: candidate, retrievalMethod: "fallback-path" };
348
- } catch (error) {
349
- errors.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
993
+ const probeCtx = { ...ctx, timeoutMs: options.probeTimeoutMs ?? 5e3 };
994
+ const candidates = getFallbackUrls(pageUrl);
995
+ const probeBatchSize = 4;
996
+ for (let start = 0; start < candidates.length; start += probeBatchSize) {
997
+ const batch = candidates.slice(start, start + probeBatchSize);
998
+ const attempts = await Promise.all(
999
+ batch.map(async (candidate) => {
1000
+ try {
1001
+ return { candidate, spec: await tryFetchSpec(candidate, probeCtx) };
1002
+ } catch (error) {
1003
+ return { candidate, error };
1004
+ }
1005
+ })
1006
+ );
1007
+ const winner = attempts.find(
1008
+ (attempt) => attempt.spec !== void 0
1009
+ );
1010
+ if (winner) {
1011
+ return { spec: winner.spec, sourceUrl: winner.candidate, retrievalMethod: "fallback-path" };
1012
+ }
1013
+ for (const attempt of attempts) {
1014
+ recordFailure(attempt.candidate, attempt.error);
350
1015
  }
351
1016
  }
352
1017
  if (options.useBrowserFallback !== false) {
353
1018
  try {
354
- const spec = await tryBrowserFallback(pageUrl.toString());
1019
+ const spec = await tryBrowserFallback(pageUrl.toString(), ctx.headers);
355
1020
  return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "headless-browser" };
356
1021
  } catch (error) {
357
1022
  errors.push(`Headless browser: ${error instanceof Error ? error.message : String(error)}`);
@@ -366,171 +1031,147 @@ Check that the URL is reachable from this terminal and points to a Swagger UI pa
366
1031
  );
367
1032
  }
368
1033
 
369
- // src/transformer.ts
370
- var HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
371
- var COMMON_ERROR_CODES = /* @__PURE__ */ new Set([
372
- "400",
373
- "401",
374
- "403",
375
- "404",
376
- "405",
377
- "409",
378
- "415",
379
- "422",
380
- "429",
381
- "500",
382
- "501",
383
- "502",
384
- "503",
385
- "default"
386
- ]);
387
- function isObject(value) {
388
- return value !== null && typeof value === "object" && !Array.isArray(value);
389
- }
390
- function entries(value) {
391
- return isObject(value) ? Object.entries(value) : [];
392
- }
393
- function cleanText(value) {
394
- return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
395
- }
396
- function markdownCell(value) {
397
- return cleanText(value).replace(/\|/g, "\\|").replace(/`/g, "\\`") || "\u2014";
398
- }
399
- function safeSourceUrl(value) {
400
- try {
401
- const url = new URL(value);
402
- url.username = "";
403
- url.password = "";
404
- for (const key of [...url.searchParams.keys()]) {
405
- if (/(token|secret|password|signature|credential|api[-_]?key|authorization|auth)/i.test(key)) {
406
- url.searchParams.set(key, "[redacted]");
1034
+ // src/tokens.ts
1035
+ var MODEL_CONFIGS = {
1036
+ claude: { name: "Claude (Sonnet / Opus)", contextWindow: 2e5, ratio: 1.08 },
1037
+ openai: { name: "OpenAI (GPT-4o / o-series)", contextWindow: 128e3, ratio: 0.95 },
1038
+ gemini: { name: "Google Gemini (Pro / Flash)", contextWindow: 1e6, ratio: 0.9 },
1039
+ deepseek: { name: "DeepSeek (V3 / R1)", contextWindow: 128e3, ratio: 0.97 },
1040
+ llama: { name: "Meta Llama (3.3 / 4)", contextWindow: 128e3, ratio: 0.98 }
1041
+ };
1042
+ function estimateTokens(text) {
1043
+ const characters = text.length;
1044
+ if (characters === 0) {
1045
+ const zeroModel = ({ name, contextWindow }) => ({
1046
+ name,
1047
+ tokens: 0,
1048
+ contextWindow,
1049
+ contextPercent: 0
1050
+ });
1051
+ return {
1052
+ characters: 0,
1053
+ models: {
1054
+ claude: zeroModel(MODEL_CONFIGS.claude),
1055
+ openai: zeroModel(MODEL_CONFIGS.openai),
1056
+ gemini: zeroModel(MODEL_CONFIGS.gemini),
1057
+ deepseek: zeroModel(MODEL_CONFIGS.deepseek),
1058
+ llama: zeroModel(MODEL_CONFIGS.llama)
407
1059
  }
408
- }
409
- return url.toString();
410
- } catch {
411
- return value;
1060
+ };
412
1061
  }
1062
+ const wordCount = (text.match(/\b[\w$]+\b/gu) || []).length;
1063
+ const syntaxSymbols = (text.match(/[{}[\]()<>=:;,."`|/-]/g) || []).length;
1064
+ const whitespaces = (text.match(/\s+/g) || []).length;
1065
+ const baseTokens = wordCount * 1.18 + syntaxSymbols * 0.42 + whitespaces * 0.05;
1066
+ const calculate = (name, ratio, contextWindow) => {
1067
+ const tokens = Math.max(1, Math.round(baseTokens * ratio));
1068
+ const contextPercent = Number((tokens / contextWindow * 100).toFixed(1));
1069
+ return { name, tokens, contextWindow, contextPercent };
1070
+ };
1071
+ return {
1072
+ characters,
1073
+ models: {
1074
+ claude: calculate(MODEL_CONFIGS.claude.name, MODEL_CONFIGS.claude.ratio, MODEL_CONFIGS.claude.contextWindow),
1075
+ openai: calculate(MODEL_CONFIGS.openai.name, MODEL_CONFIGS.openai.ratio, MODEL_CONFIGS.openai.contextWindow),
1076
+ gemini: calculate(MODEL_CONFIGS.gemini.name, MODEL_CONFIGS.gemini.ratio, MODEL_CONFIGS.gemini.contextWindow),
1077
+ deepseek: calculate(
1078
+ MODEL_CONFIGS.deepseek.name,
1079
+ MODEL_CONFIGS.deepseek.ratio,
1080
+ MODEL_CONFIGS.deepseek.contextWindow
1081
+ ),
1082
+ llama: calculate(MODEL_CONFIGS.llama.name, MODEL_CONFIGS.llama.ratio, MODEL_CONFIGS.llama.contextWindow)
1083
+ }
1084
+ };
413
1085
  }
414
- function safeComment(value) {
415
- return cleanText(value).replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
1086
+
1087
+ // src/transformer/security.ts
1088
+ function describeSecurityScheme(name, scheme) {
1089
+ const type = cleanText(scheme.type).toLowerCase();
1090
+ const label = cleanText(scheme.description);
1091
+ let details = name;
1092
+ if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "bearer")
1093
+ details += " (Bearer token via Authorization header)";
1094
+ else if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "basic")
1095
+ details += " (HTTP Basic via Authorization header)";
1096
+ else if (type === "apikey" || type === "apiKey")
1097
+ details += ` (API key in ${cleanText(scheme.in) || "header"}: ${cleanText(scheme.name) || "key"})`;
1098
+ else if (type === "oauth2") {
1099
+ const flows = entries(scheme.flows).map(([flowName]) => flowName).join(", ") || cleanText(scheme.flow);
1100
+ details += ` (OAuth2${flows ? `; ${flows}` : ""})`;
1101
+ } else if (type === "openidconnect" || type === "openIdConnect") details += " (OpenID Connect)";
1102
+ else if (type === "basic") details += " (HTTP Basic)";
1103
+ else if (type) details += ` (${type})`;
1104
+ if (label) details += ` \u2014 ${label}`;
1105
+ return details;
416
1106
  }
417
- function schemaMetadata(schemaValue) {
418
- if (!isObject(schemaValue)) return [];
419
- const schema = schemaValue;
420
- const metadata = [];
421
- if (cleanText(schema.description)) metadata.push(cleanText(schema.description));
422
- if (cleanText(schema.format)) metadata.push(`format: ${cleanText(schema.format)}`);
423
- const constraints = [
424
- ["default", "default"],
425
- ["pattern", "pattern"],
426
- ["minLength", "minLength"],
427
- ["maxLength", "maxLength"],
428
- ["minimum", "minimum"],
429
- ["maximum", "maximum"],
430
- ["exclusiveMinimum", "exclusiveMinimum"],
431
- ["exclusiveMaximum", "exclusiveMaximum"],
432
- ["minItems", "minItems"],
433
- ["maxItems", "maxItems"],
434
- ["uniqueItems", "uniqueItems"],
435
- ["multipleOf", "multipleOf"]
436
- ];
437
- for (const [property, label] of constraints) {
438
- const value = schema[property];
439
- if (value === void 0) continue;
440
- if (property === "default" && (value === null || value === "")) continue;
441
- metadata.push(
442
- `${label}: ${property === "default" ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value)}`
443
- );
444
- }
445
- if (schema.readOnly === true) metadata.push("read-only");
446
- if (schema.writeOnly === true) metadata.push("write-only");
447
- if (schema.deprecated === true) metadata.push("deprecated");
448
- return [...new Set(metadata)];
1107
+ function getSecuritySchemes(spec) {
1108
+ const dictionary = spec.components?.securitySchemes ?? spec.securityDefinitions ?? {};
1109
+ return entries(dictionary).filter((entry) => isObject(entry[1]));
449
1110
  }
450
- function toTypeScriptIdentifier(value) {
451
- let identifier = value.replace(/[^\p{L}\p{N}_$]+/gu, "_").replace(/^([^\p{L}_$])/u, "_$1");
452
- if (!identifier) identifier = "Unnamed";
453
- const reserved = /* @__PURE__ */ new Set([
454
- "abstract",
455
- "any",
456
- "as",
457
- "asserts",
458
- "async",
459
- "await",
460
- "boolean",
461
- "break",
462
- "case",
463
- "catch",
464
- "class",
465
- "const",
466
- "constructor",
467
- "continue",
468
- "debugger",
469
- "declare",
470
- "default",
471
- "delete",
472
- "do",
473
- "else",
474
- "enum",
475
- "export",
476
- "extends",
477
- "false",
478
- "finally",
479
- "for",
480
- "from",
481
- "function",
482
- "get",
483
- "if",
484
- "implements",
485
- "import",
486
- "in",
487
- "infer",
488
- "instanceof",
489
- "interface",
490
- "is",
491
- "keyof",
492
- "let",
493
- "module",
494
- "namespace",
495
- "never",
496
- "new",
497
- "null",
498
- "number",
499
- "object",
500
- "of",
501
- "package",
502
- "private",
503
- "protected",
504
- "public",
505
- "readonly",
506
- "require",
507
- "return",
508
- "set",
509
- "static",
510
- "string",
511
- "super",
512
- "switch",
513
- "symbol",
514
- "this",
515
- "throw",
516
- "true",
517
- "try",
518
- "type",
519
- "typeof",
520
- "undefined",
521
- "unique",
522
- "unknown",
523
- "var",
524
- "void",
525
- "while",
526
- "with",
527
- "yield"
528
- ]);
529
- if (reserved.has(identifier)) identifier = `${identifier}Model`;
530
- return identifier;
1111
+ function securityRequirement(spec, operation) {
1112
+ const security = operation.security !== void 0 ? operation.security : spec.security;
1113
+ if (!Array.isArray(security) || security.length === 0) return "None explicitly required";
1114
+ const allSchemes = new Map(getSecuritySchemes(spec));
1115
+ return security.map((requirement) => {
1116
+ if (!isObject(requirement)) return "";
1117
+ const names = Object.entries(requirement).map(([name, scopes]) => {
1118
+ const label = allSchemes.has(name) ? name : `${name} (scheme not defined)`;
1119
+ return Array.isArray(scopes) && scopes.length ? `${label} [scopes: ${scopes.join(", ")}]` : label;
1120
+ });
1121
+ return names.length ? names.join(" + ") : "No auth";
1122
+ }).filter(Boolean).join(" OR ");
531
1123
  }
532
- function propertyKey(value) {
533
- return /^[$A-Z_a-z][$\w]*$/.test(value) ? value : JSON.stringify(value);
1124
+ function authOverview(spec) {
1125
+ const schemes = getSecuritySchemes(spec);
1126
+ if (!schemes.length) return ["- **Authentication:** No security schemes declared."];
1127
+ const globalSecurity = securityRequirement(spec, {});
1128
+ return [
1129
+ `- **Authentication required globally:** ${globalSecurity}`,
1130
+ "- **Available schemes:**",
1131
+ ...schemes.map(([name, scheme]) => ` - ${describeSecurityScheme(name, scheme)}`)
1132
+ ];
1133
+ }
1134
+
1135
+ // src/transformer/render-style.ts
1136
+ var FULL_STYLE = { includeExamples: true, collapsed: false };
1137
+ function shortenDescription(text, limit) {
1138
+ if (limit === void 0 || text.length <= limit) return text;
1139
+ return `${text.slice(0, limit).trimEnd()}\u2026`;
1140
+ }
1141
+
1142
+ // src/transformer/schema-type.ts
1143
+ function schemaMetadata(schemaValue) {
1144
+ if (!isObject(schemaValue)) return [];
1145
+ const schema = schemaValue;
1146
+ const metadata = [];
1147
+ if (cleanText(schema.description)) metadata.push(cleanText(schema.description));
1148
+ if (cleanText(schema.format)) metadata.push(`format: ${cleanText(schema.format)}`);
1149
+ const constraints = [
1150
+ ["default", "default"],
1151
+ ["pattern", "pattern"],
1152
+ ["minLength", "minLength"],
1153
+ ["maxLength", "maxLength"],
1154
+ ["minimum", "minimum"],
1155
+ ["maximum", "maximum"],
1156
+ ["exclusiveMinimum", "exclusiveMinimum"],
1157
+ ["exclusiveMaximum", "exclusiveMaximum"],
1158
+ ["minItems", "minItems"],
1159
+ ["maxItems", "maxItems"],
1160
+ ["uniqueItems", "uniqueItems"],
1161
+ ["multipleOf", "multipleOf"]
1162
+ ];
1163
+ for (const [property, label] of constraints) {
1164
+ const value = schema[property];
1165
+ if (value === void 0) continue;
1166
+ if (property === "default" && (value === null || value === "")) continue;
1167
+ metadata.push(
1168
+ `${label}: ${property === "default" ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value)}`
1169
+ );
1170
+ }
1171
+ if (schema.readOnly === true) metadata.push("read-only");
1172
+ if (schema.writeOnly === true) metadata.push("write-only");
1173
+ if (schema.deprecated === true) metadata.push("deprecated");
1174
+ return [...new Set(metadata)];
534
1175
  }
535
1176
  function refName(ref) {
536
1177
  const parts = ref.split("/");
@@ -635,80 +1276,7 @@ function getSchemaDictionary(spec) {
635
1276
  const schemas = spec.components?.schemas ?? spec.definitions ?? {};
636
1277
  return isObject(schemas) ? schemas : {};
637
1278
  }
638
- function collectSchemaRefs(value, output, root, seen = /* @__PURE__ */ new Set(), followedRefs = /* @__PURE__ */ new Set()) {
639
- if (!value || typeof value !== "object" || seen.has(value)) return;
640
- seen.add(value);
641
- if (Array.isArray(value)) {
642
- for (const item of value) collectSchemaRefs(item, output, root, seen, followedRefs);
643
- return;
644
- }
645
- const object = value;
646
- if (typeof object.$ref === "string") {
647
- if (isSchemaRef(object.$ref)) {
648
- const name = refName(object.$ref);
649
- if (name) output.add(name);
650
- } else if (root && object.$ref.startsWith("#/") && !followedRefs.has(object.$ref)) {
651
- followedRefs.add(object.$ref);
652
- const target = resolvePointer(root, object.$ref);
653
- if (target !== void 0) collectSchemaRefs(target, output, root, seen, followedRefs);
654
- }
655
- }
656
- for (const [key, child] of Object.entries(object)) {
657
- if (key !== "example" && key !== "examples" && key !== "externalDocs")
658
- collectSchemaRefs(child, output, root, seen, followedRefs);
659
- }
660
- }
661
- function referencedSchemas(spec) {
662
- const dictionary = getSchemaDictionary(spec);
663
- const knownByName = /* @__PURE__ */ new Map();
664
- for (const [name, schema] of Object.entries(dictionary)) {
665
- knownByName.set(toTypeScriptIdentifier(name), [name, schema]);
666
- }
667
- const refs = /* @__PURE__ */ new Set();
668
- collectSchemaRefs(spec.paths ?? {}, refs, spec);
669
- const selected = /* @__PURE__ */ new Set();
670
- const queue = [...refs];
671
- while (queue.length) {
672
- const name = queue.shift();
673
- if (!name || selected.has(name)) continue;
674
- selected.add(name);
675
- const model = knownByName.get(name);
676
- if (!model) continue;
677
- const childRefs = /* @__PURE__ */ new Set();
678
- collectSchemaRefs(model[1], childRefs, spec);
679
- for (const child of childRefs) if (!selected.has(child)) queue.push(child);
680
- }
681
- return [...selected].map((name) => knownByName.get(name)).filter((entry) => Boolean(entry));
682
- }
683
- function modelDeclaration(name, schema) {
684
- const identifier = toTypeScriptIdentifier(name);
685
- const description = safeComment(schema.description);
686
- const comment = description ? `/** ${description} */
687
- ` : "";
688
- if (schema.$ref) return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
689
- if (Array.isArray(schema.enum) || schema.const !== void 0 || schema.oneOf || schema.anyOf || schema.allOf) {
690
- return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
691
- }
692
- const kind = Array.isArray(schema.type) ? schema.type.find((item) => item !== "null") : schema.type;
693
- if (kind === "object" || schema.properties || schema.additionalProperties !== void 0) {
694
- const properties = entries(schema.properties);
695
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
696
- const fields = properties.map(([field, value]) => {
697
- const metadata = schemaMetadata(value).map(safeComment).filter(Boolean).join("; ");
698
- const doc = metadata ? ` /** ${metadata} */
699
- ` : "";
700
- return `${doc} ${propertyKey(field)}${required.has(field) ? "" : "?"}: ${typeExpression(value)};`;
701
- });
702
- if (schema.additionalProperties === true) fields.push(" [key: string]: unknown;");
703
- else if (isObject(schema.additionalProperties))
704
- fields.push(` [key: string]: ${typeExpression(schema.additionalProperties)};`);
705
- return `${comment}export interface ${identifier} {
706
- ${fields.join("\n")}
707
- }`;
708
- }
709
- return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
710
- }
711
- function resolvePointer(root, ref) {
1279
+ function resolvePointer2(root, ref) {
712
1280
  if (!ref.startsWith("#/")) return void 0;
713
1281
  let current = root;
714
1282
  for (const rawPart of ref.slice(2).split("/")) {
@@ -723,7 +1291,7 @@ function resolveReferenceObject(spec, value, seen = /* @__PURE__ */ new Set()) {
723
1291
  if (typeof value.$ref !== "string") return value;
724
1292
  const ref = value.$ref;
725
1293
  if (seen.has(ref)) return {};
726
- const target = resolvePointer(spec, ref);
1294
+ const target = resolvePointer2(spec, ref);
727
1295
  if (!isObject(target)) return value;
728
1296
  const nextSeen = new Set(seen).add(ref);
729
1297
  return {
@@ -731,112 +1299,8 @@ function resolveReferenceObject(spec, value, seen = /* @__PURE__ */ new Set()) {
731
1299
  ...Object.fromEntries(Object.entries(value).filter(([key]) => key !== "$ref"))
732
1300
  };
733
1301
  }
734
- function getEndpoints(spec) {
735
- const result = [];
736
- const paths = isObject(spec.paths) ? spec.paths : {};
737
- for (const [path2, pathItemValue] of Object.entries(paths)) {
738
- if (!isObject(pathItemValue)) continue;
739
- const pathParameters = Array.isArray(pathItemValue.parameters) ? pathItemValue.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
740
- for (const method of HTTP_METHODS) {
741
- const operationValue = pathItemValue[method];
742
- if (!isObject(operationValue)) continue;
743
- const firstTag = Array.isArray(operationValue.tags) && operationValue.tags.length ? String(operationValue.tags[0]) : "General";
744
- result.push({ method, path: path2, operation: operationValue, pathParameters, tag: firstTag });
745
- }
746
- }
747
- return result;
748
- }
749
- function getServers(spec) {
750
- if (Array.isArray(spec.servers) && spec.servers.length) {
751
- return spec.servers.map((server) => cleanText(server?.url)).filter(Boolean);
752
- }
753
- if (isObject(spec)) {
754
- const host = cleanText(spec.host);
755
- if (host) {
756
- const schemes = Array.isArray(spec.schemes) && spec.schemes.length ? spec.schemes : ["https"];
757
- const basePath = cleanText(spec.basePath) || "/";
758
- return schemes.map(
759
- (scheme) => `${scheme}://${host}${basePath.startsWith("/") ? basePath : `/${basePath}`}`
760
- );
761
- }
762
- }
763
- return [];
764
- }
765
- function describeSecurityScheme(name, scheme) {
766
- const type = cleanText(scheme.type).toLowerCase();
767
- const label = cleanText(scheme.description);
768
- let details = name;
769
- if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "bearer")
770
- details += " (Bearer token via Authorization header)";
771
- else if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "basic")
772
- details += " (HTTP Basic via Authorization header)";
773
- else if (type === "apikey" || type === "apiKey")
774
- details += ` (API key in ${cleanText(scheme.in) || "header"}: ${cleanText(scheme.name) || "key"})`;
775
- else if (type === "oauth2") {
776
- const flows = entries(scheme.flows).map(([flowName]) => flowName).join(", ") || cleanText(scheme.flow);
777
- details += ` (OAuth2${flows ? `; ${flows}` : ""})`;
778
- } else if (type === "openidconnect" || type === "openIdConnect") details += " (OpenID Connect)";
779
- else if (type === "basic") details += " (HTTP Basic)";
780
- else if (type) details += ` (${type})`;
781
- if (label) details += ` \u2014 ${label}`;
782
- return details;
783
- }
784
- function getSecuritySchemes(spec) {
785
- const dictionary = spec.components?.securitySchemes ?? spec.securityDefinitions ?? {};
786
- return entries(dictionary).filter((entry) => isObject(entry[1]));
787
- }
788
- function securityRequirement(spec, operation) {
789
- const security = operation.security !== void 0 ? operation.security : spec.security;
790
- if (!Array.isArray(security) || security.length === 0) return "None explicitly required";
791
- const allSchemes = new Map(getSecuritySchemes(spec));
792
- return security.map((requirement) => {
793
- if (!isObject(requirement)) return "";
794
- const names = Object.entries(requirement).map(([name, scopes]) => {
795
- const label = allSchemes.has(name) ? name : `${name} (scheme not defined)`;
796
- return Array.isArray(scopes) && scopes.length ? `${label} [scopes: ${scopes.join(", ")}]` : label;
797
- });
798
- return names.length ? names.join(" + ") : "No auth";
799
- }).filter(Boolean).join(" OR ");
800
- }
801
- function mergedParameters(spec, endpoint) {
802
- const operationParameters = Array.isArray(endpoint.operation.parameters) ? endpoint.operation.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
803
- const byIdentity = /* @__PURE__ */ new Map();
804
- for (const parameter of [...endpoint.pathParameters, ...operationParameters]) {
805
- if (parameter.in === "body") continue;
806
- const key = `${cleanText(parameter.in)}:${cleanText(parameter.name)}`;
807
- if (key !== ":") byIdentity.set(key, parameter);
808
- }
809
- return [...byIdentity.values()];
810
- }
811
- function parameterType(parameter) {
812
- if (isObject(parameter.schema)) return typeExpression(parameter.schema);
813
- if (parameter.type === "array") return `${typeExpression(parameter.items)}[]`;
814
- if (parameter.type) return typeExpression({ type: parameter.type, format: parameter.format, enum: parameter.enum });
815
- return "unknown";
816
- }
817
- function parameterDisplayType(parameter) {
818
- return parameterType(parameter).replace(/\/\*\s*([^*]+?)\s*\*\//g, "($1)");
819
- }
820
- function parameterDefault(parameter) {
821
- if (parameter.default !== void 0) return parameter.default;
822
- if (isObject(parameter.schema)) return parameter.schema.default;
823
- return void 0;
824
- }
825
- function isMeaningfulDefault(value) {
826
- return value !== void 0 && value !== null && value !== "";
827
- }
828
- function parameterList(parameters) {
829
- if (!parameters.length) return "_None._";
830
- return parameters.map((parameter) => {
831
- const required = parameter.required === true || parameter.in === "path";
832
- const type = parameterDisplayType(parameter);
833
- const defaultValue = parameterDefault(parameter);
834
- const defaultSuffix = isMeaningfulDefault(defaultValue) ? ` (default: ${typeof defaultValue === "string" ? JSON.stringify(defaultValue) : String(defaultValue)})` : "";
835
- const description = cleanText(parameter.description);
836
- const details = description ? ` \u2014 ${description}` : "";
837
- return `- \`${cleanText(parameter.name)}\` (\`${cleanText(parameter.in)}\`${required ? ", required" : ""}) \xB7 \`${type}\`${defaultSuffix}${details}`;
838
- }).join("\n");
839
- }
1302
+
1303
+ // src/transformer/examples.ts
840
1304
  function chooseMediaType(content) {
841
1305
  const mediaTypes = Object.keys(content);
842
1306
  return mediaTypes.find((type) => type.toLowerCase() === "application/json") ?? mediaTypes.find((type) => type.toLowerCase().includes("+json")) ?? mediaTypes[0];
@@ -849,7 +1313,7 @@ function firstDeclaredExample(spec, schemaValue, seen = /* @__PURE__ */ new Set(
849
1313
  let schema = schemaValue;
850
1314
  if (typeof schema.$ref === "string") {
851
1315
  if (seen.has(schema.$ref)) return void 0;
852
- const target = resolvePointer(spec, schema.$ref);
1316
+ const target = resolvePointer2(spec, schema.$ref);
853
1317
  if (!isObject(target)) return void 0;
854
1318
  const nextSeen = new Set(seen).add(schema.$ref);
855
1319
  schema = { ...target, ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "$ref")) };
@@ -866,7 +1330,7 @@ function generateSchemaExample(spec, schemaValue, depth = 0, seenRefs = /* @__PU
866
1330
  let nextSeen = seenRefs;
867
1331
  if (typeof schema.$ref === "string") {
868
1332
  if (seenRefs.has(schema.$ref)) return null;
869
- const target = resolvePointer(spec, schema.$ref);
1333
+ const target = resolvePointer2(spec, schema.$ref);
870
1334
  if (!isObject(target)) return void 0;
871
1335
  nextSeen = new Set(seenRefs).add(schema.$ref);
872
1336
  schema = { ...target, ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "$ref")) };
@@ -959,12 +1423,63 @@ function payloadExample(spec, mediaValue, schemaValue, legacyExamples) {
959
1423
  return { value: legacyExamples[mediaType], origin: "declared" };
960
1424
  }
961
1425
  }
962
- const declaredSchemaExample = firstDeclaredExample(spec, schemaValue);
963
- if (declaredSchemaExample !== void 0) return { value: declaredSchemaExample, origin: "declared" };
964
- const generated = generateSchemaExample(spec, schemaValue);
965
- return generated === void 0 ? void 0 : { value: generated, origin: "schema-derived" };
1426
+ const declaredSchemaExample = firstDeclaredExample(spec, schemaValue);
1427
+ if (declaredSchemaExample !== void 0) return { value: declaredSchemaExample, origin: "declared" };
1428
+ const generated = generateSchemaExample(spec, schemaValue);
1429
+ return generated === void 0 ? void 0 : { value: generated, origin: "schema-derived" };
1430
+ }
1431
+ function formattedExample(value) {
1432
+ try {
1433
+ return JSON.stringify(value, null, 2) ?? "null";
1434
+ } catch {
1435
+ return "null";
1436
+ }
1437
+ }
1438
+
1439
+ // src/transformer/parameters.ts
1440
+ function mergedParameters(spec, endpoint) {
1441
+ const operationParameters = Array.isArray(endpoint.operation.parameters) ? endpoint.operation.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
1442
+ const byIdentity = /* @__PURE__ */ new Map();
1443
+ for (const parameter of [...endpoint.pathParameters, ...operationParameters]) {
1444
+ if (parameter.in === "body") continue;
1445
+ const key = `${cleanText(parameter.in)}:${cleanText(parameter.name)}`;
1446
+ if (key !== ":") byIdentity.set(key, parameter);
1447
+ }
1448
+ return [...byIdentity.values()];
1449
+ }
1450
+ function parameterType(parameter) {
1451
+ if (isObject(parameter.schema)) return typeExpression(parameter.schema);
1452
+ if (parameter.type === "array") return `${typeExpression(parameter.items)}[]`;
1453
+ if (parameter.type) return typeExpression({ type: parameter.type, format: parameter.format, enum: parameter.enum });
1454
+ return "unknown";
1455
+ }
1456
+ function parameterDisplayType(parameter) {
1457
+ return parameterType(parameter).replace(/\/\*\s*([^*]+?)\s*\*\//g, "($1)");
1458
+ }
1459
+ function parameterDefault(parameter) {
1460
+ if (parameter.default !== void 0) return parameter.default;
1461
+ if (isObject(parameter.schema)) return parameter.schema.default;
1462
+ return void 0;
1463
+ }
1464
+ function isMeaningfulDefault(value) {
1465
+ return value !== void 0 && value !== null && value !== "";
1466
+ }
1467
+ function parameterList(parameters, style = FULL_STYLE) {
1468
+ if (!parameters.length) return "_None._";
1469
+ if (style.collapsed) {
1470
+ return `- _${parameters.length} parameter${parameters.length === 1 ? "" : "s"} (detail collapsed for token budget)._`;
1471
+ }
1472
+ return parameters.map((parameter) => {
1473
+ const required = parameter.required === true || parameter.in === "path";
1474
+ const type = parameterDisplayType(parameter);
1475
+ const defaultValue = parameterDefault(parameter);
1476
+ const defaultSuffix = isMeaningfulDefault(defaultValue) ? ` (default: ${typeof defaultValue === "string" ? JSON.stringify(defaultValue) : String(defaultValue)})` : "";
1477
+ const description = shortenDescription(cleanText(parameter.description), style.descriptionLimit);
1478
+ const details = description ? ` \u2014 ${description}` : "";
1479
+ return `- \`${cleanText(parameter.name)}\` (\`${cleanText(parameter.in)}\`${required ? ", required" : ""}) \xB7 \`${type}\`${defaultSuffix}${details}`;
1480
+ }).join("\n");
966
1481
  }
967
- function requestPayloadDescription(spec, operation) {
1482
+ function requestPayloadDescription(spec, operation, style = FULL_STYLE) {
968
1483
  if (isObject(operation.requestBody) || operation.requestBody?.$ref) {
969
1484
  const body = resolveReferenceObject(spec, operation.requestBody);
970
1485
  const content = isObject(body.content) ? body.content : {};
@@ -974,7 +1489,7 @@ function requestPayloadDescription(spec, operation) {
974
1489
  return {
975
1490
  type: schema ? typeExpression(schema) : "unknown",
976
1491
  mediaType: mediaType || "unknown media type",
977
- example: media || schema ? payloadExample(spec, media, schema) : void 0,
1492
+ example: style.includeExamples && (media || schema) ? payloadExample(spec, media, schema) : void 0,
978
1493
  required: body.required === true
979
1494
  };
980
1495
  }
@@ -985,13 +1500,31 @@ function requestPayloadDescription(spec, operation) {
985
1500
  return {
986
1501
  type: typeExpression(bodyParameter.schema),
987
1502
  mediaType: consumes[0] ?? "application/json",
988
- example: payloadExample(spec, void 0, bodyParameter.schema),
1503
+ example: style.includeExamples ? payloadExample(spec, void 0, bodyParameter.schema) : void 0,
989
1504
  required: bodyParameter.required === true
990
1505
  };
991
1506
  }
992
1507
  return void 0;
993
1508
  }
994
- function responseSchema(spec, responseValue) {
1509
+
1510
+ // src/transformer/responses.ts
1511
+ var COMMON_ERROR_CODES = /* @__PURE__ */ new Set([
1512
+ "400",
1513
+ "401",
1514
+ "403",
1515
+ "404",
1516
+ "405",
1517
+ "409",
1518
+ "415",
1519
+ "422",
1520
+ "429",
1521
+ "500",
1522
+ "501",
1523
+ "502",
1524
+ "503",
1525
+ "default"
1526
+ ]);
1527
+ function responseSchema(spec, responseValue, style = FULL_STYLE) {
995
1528
  const response = resolveReferenceObject(spec, responseValue);
996
1529
  if (isObject(response.content)) {
997
1530
  const mediaType = chooseMediaType(response.content);
@@ -1000,30 +1533,26 @@ function responseSchema(spec, responseValue) {
1000
1533
  return {
1001
1534
  type: schema ? typeExpression(schema) : void 0,
1002
1535
  mediaType,
1003
- example: schema || media ? payloadExample(spec, media, schema, response.examples) : void 0
1536
+ example: style.includeExamples && (schema || media) ? payloadExample(spec, media, schema, response.examples) : void 0
1004
1537
  };
1005
1538
  }
1006
1539
  if (isObject(response.schema)) {
1007
1540
  return {
1008
1541
  type: typeExpression(response.schema),
1009
1542
  mediaType: "application/json",
1010
- example: payloadExample(spec, void 0, response.schema, response.examples)
1543
+ example: style.includeExamples ? payloadExample(spec, void 0, response.schema, response.examples) : void 0
1011
1544
  };
1012
1545
  }
1013
1546
  return {};
1014
1547
  }
1015
- function formattedExample(value) {
1016
- try {
1017
- return JSON.stringify(value, null, 2) ?? "null";
1018
- } catch {
1019
- return "null";
1020
- }
1021
- }
1022
- function responseBlock(spec, code, responseValue) {
1548
+ function responseBlock(spec, code, responseValue, style = FULL_STYLE) {
1023
1549
  const response = resolveReferenceObject(spec, responseValue);
1024
- const description = cleanText(response.description);
1025
- const payload = responseSchema(spec, responseValue);
1550
+ const description = style.collapsed ? "" : shortenDescription(cleanText(response.description), style.descriptionLimit);
1551
+ const payload = responseSchema(spec, responseValue, style);
1026
1552
  const suffix = payload.type ? ` \u2014 \`${payload.type}\`${payload.mediaType ? ` (${payload.mediaType})` : ""}` : " \u2014 no body schema declared";
1553
+ if (style.collapsed) {
1554
+ return [`- **${markdownCell(code)}**${suffix}`].join("\n");
1555
+ }
1027
1556
  const lines = [`- **${markdownCell(code)}${description ? ` ${description}` : ""}**${suffix}`];
1028
1557
  if (payload.example?.origin === "declared") {
1029
1558
  const label = payload.example.name ? ` \xB7 ${payload.example.name}` : "";
@@ -1088,7 +1617,7 @@ function collapsibleErrorEntry(spec, code, responseValue) {
1088
1617
  if (schemaValue === void 0) return `\`${code}\``;
1089
1618
  return `\`${code}\` (\`${typeExpression(schemaValue)}\`)`;
1090
1619
  }
1091
- function responseSection(spec, operation) {
1620
+ function responseSection(spec, operation, style = FULL_STYLE) {
1092
1621
  const responses = entries(operation.responses).filter(([, value]) => isObject(value));
1093
1622
  if (!responses.length) return "_No responses declared._";
1094
1623
  const success = responses.filter(([code]) => /^2\d\d$/.test(code));
@@ -1096,42 +1625,96 @@ function responseSection(spec, operation) {
1096
1625
  const errors = responses.filter(
1097
1626
  ([code]) => (COMMON_ERROR_CODES.has(code) || /^([45])\d\d$/.test(code)) && !success.some(([successCode]) => successCode === code)
1098
1627
  );
1628
+ if (style.collapsed) {
1629
+ const collapsedLines = success.map(([code, response]) => responseBlock(spec, code, response, style));
1630
+ collapsedLines.push(...errors.map(([code, responseValue]) => responseBlock(spec, code, responseValue, style)));
1631
+ return collapsedLines.join("\n");
1632
+ }
1099
1633
  const detailedErrors = [];
1100
1634
  const collapsedErrors = [];
1101
1635
  for (const [code, responseValue] of errors) {
1102
1636
  const entry = collapsibleErrorEntry(spec, code, responseValue);
1103
- if (entry === void 0) detailedErrors.push(responseBlock(spec, code, responseValue));
1637
+ if (entry === void 0) detailedErrors.push(responseBlock(spec, code, responseValue, style));
1104
1638
  else collapsedErrors.push(entry);
1105
1639
  }
1106
1640
  const primarySuccess = success[0];
1107
- const lines = primarySuccess ? [responseBlock(spec, primarySuccess[0], primarySuccess[1])] : [];
1641
+ const lines = primarySuccess ? [responseBlock(spec, primarySuccess[0], primarySuccess[1], style)] : [];
1108
1642
  lines.push(...detailedErrors);
1109
1643
  if (collapsedErrors.length) lines.push(`- **Errors:** ${collapsedErrors.join(", ")}`);
1110
1644
  if (success.length > 1) {
1111
- const others = success.slice(1).map(([code, response]) => responseBlock(spec, code, response));
1645
+ const others = success.slice(1).map(([code, response]) => responseBlock(spec, code, response, style));
1112
1646
  lines.push("", "<details><summary>Other successful responses</summary>", "", ...others, "", "</details>");
1113
1647
  }
1114
1648
  if (!lines.length) {
1115
- return responses.map(([code, response]) => responseBlock(spec, code, response)).join("\n\n");
1649
+ return responses.map(([code, response]) => responseBlock(spec, code, response, style)).join("\n\n");
1116
1650
  }
1117
1651
  return lines.join("\n");
1118
1652
  }
1653
+
1654
+ // src/transformer/endpoints.ts
1655
+ var HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
1656
+ function getEndpoints(spec) {
1657
+ const result = [];
1658
+ const paths = isObject(spec.paths) ? spec.paths : {};
1659
+ for (const [path2, pathItemValue] of Object.entries(paths)) {
1660
+ if (!isObject(pathItemValue)) continue;
1661
+ const pathParameters = Array.isArray(pathItemValue.parameters) ? pathItemValue.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
1662
+ for (const method of HTTP_METHODS) {
1663
+ const operationValue = pathItemValue[method];
1664
+ if (!isObject(operationValue)) continue;
1665
+ const firstTag = Array.isArray(operationValue.tags) && operationValue.tags.length ? String(operationValue.tags[0]) : "General";
1666
+ result.push({ method, path: path2, operation: operationValue, pathParameters, tag: firstTag });
1667
+ }
1668
+ }
1669
+ return result;
1670
+ }
1671
+ function getServers(spec) {
1672
+ if (Array.isArray(spec.servers) && spec.servers.length) {
1673
+ return spec.servers.map((server) => cleanText(isObject(server) ? server.url : void 0)).filter(Boolean);
1674
+ }
1675
+ if (isObject(spec)) {
1676
+ const host = cleanText(spec.host);
1677
+ if (host) {
1678
+ const schemes = Array.isArray(spec.schemes) && spec.schemes.length ? spec.schemes : ["https"];
1679
+ const basePath = cleanText(spec.basePath) || "/";
1680
+ return schemes.map(
1681
+ (scheme) => `${scheme}://${host}${basePath.startsWith("/") ? basePath : `/${basePath}`}`
1682
+ );
1683
+ }
1684
+ }
1685
+ return [];
1686
+ }
1687
+ function collectTags(spec, endpoints) {
1688
+ const declared = Array.isArray(spec.tags) ? spec.tags.map((tag) => cleanText(isObject(tag) ? tag.name : void 0)).filter(Boolean) : [];
1689
+ const encountered = [...new Set(endpoints.map((endpoint) => endpoint.tag))];
1690
+ return [
1691
+ ...declared.filter((tag) => encountered.includes(tag)),
1692
+ ...encountered.filter((tag) => !declared.includes(tag))
1693
+ ];
1694
+ }
1695
+ function endpointTypeBase(method, path2, operation) {
1696
+ if (cleanText(operation.operationId)) return toTypeScriptIdentifier(cleanText(operation.operationId));
1697
+ const pathName = path2.replace(/[{}]/g, "").split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1698
+ return toTypeScriptIdentifier(`${method.charAt(0).toUpperCase()}${method.slice(1)}${pathName}`);
1699
+ }
1119
1700
  function shortOperationDescription(operation) {
1120
1701
  return markdownCell(operation.summary || operation.operationId || operation.description || "\u2014");
1121
1702
  }
1122
- function endpointMarkdown(spec, endpoint) {
1703
+ function endpointMarkdown(spec, endpoint, style = FULL_STYLE) {
1123
1704
  const { operation, method, path: path2 } = endpoint;
1124
1705
  const heading = `#### \`${method.toUpperCase()} ${path2}\``;
1125
1706
  const summary = cleanText(operation.summary);
1126
1707
  const description = cleanText(operation.description);
1127
1708
  const lines = [heading];
1128
- if (summary) lines.push("", summary);
1129
- if (description && description !== summary) lines.push("", description);
1709
+ if (summary) lines.push("", shortenDescription(summary, style.descriptionLimit));
1710
+ if (description && description !== summary) {
1711
+ lines.push("", shortenDescription(description, style.descriptionLimit));
1712
+ }
1130
1713
  lines.push("", `**Authentication:** ${securityRequirement(spec, operation)}`);
1131
1714
  const parameters = mergedParameters(spec, endpoint);
1132
- const request = requestPayloadDescription(spec, operation);
1715
+ const request = requestPayloadDescription(spec, operation, style);
1133
1716
  lines.push("", "**Parameters**");
1134
- lines.push(parameterList(parameters));
1717
+ lines.push(parameterList(parameters, style));
1135
1718
  if (request) {
1136
1719
  lines.push(
1137
1720
  "",
@@ -1149,31 +1732,83 @@ function endpointMarkdown(spec, endpoint) {
1149
1732
  );
1150
1733
  }
1151
1734
  }
1152
- lines.push("", "**Responses**", responseSection(spec, operation));
1735
+ lines.push("", "**Responses**", responseSection(spec, operation, style));
1153
1736
  return lines.join("\n");
1154
1737
  }
1155
- function authOverview(spec) {
1156
- const schemes = getSecuritySchemes(spec);
1157
- if (!schemes.length) return ["- **Authentication:** No security schemes declared."];
1158
- const globalSecurity = securityRequirement(spec, {});
1159
- return [
1160
- `- **Authentication required globally:** ${globalSecurity}`,
1161
- "- **Available schemes:**",
1162
- ...schemes.map(([name, scheme]) => ` - ${describeSecurityScheme(name, scheme)}`)
1163
- ];
1738
+
1739
+ // src/transformer/models.ts
1740
+ function collectSchemaRefs(value, output, root, seen = /* @__PURE__ */ new Set(), followedRefs = /* @__PURE__ */ new Set()) {
1741
+ if (!value || typeof value !== "object" || seen.has(value)) return;
1742
+ seen.add(value);
1743
+ if (Array.isArray(value)) {
1744
+ for (const item of value) collectSchemaRefs(item, output, root, seen, followedRefs);
1745
+ return;
1746
+ }
1747
+ const object = value;
1748
+ if (typeof object.$ref === "string") {
1749
+ if (isSchemaRef(object.$ref)) {
1750
+ const name = refName(object.$ref);
1751
+ if (name) output.add(name);
1752
+ } else if (root && object.$ref.startsWith("#/") && !followedRefs.has(object.$ref)) {
1753
+ followedRefs.add(object.$ref);
1754
+ const target = resolvePointer2(root, object.$ref);
1755
+ if (target !== void 0) collectSchemaRefs(target, output, root, seen, followedRefs);
1756
+ }
1757
+ }
1758
+ for (const [key, child] of Object.entries(object)) {
1759
+ if (key !== "example" && key !== "examples" && key !== "externalDocs")
1760
+ collectSchemaRefs(child, output, root, seen, followedRefs);
1761
+ }
1164
1762
  }
1165
- function collectTags(spec, endpoints) {
1166
- const declared = Array.isArray(spec.tags) ? spec.tags.map((tag) => cleanText(tag?.name)).filter(Boolean) : [];
1167
- const encountered = [...new Set(endpoints.map((endpoint) => endpoint.tag))];
1168
- return [
1169
- ...declared.filter((tag) => encountered.includes(tag)),
1170
- ...encountered.filter((tag) => !declared.includes(tag))
1171
- ];
1763
+ function referencedSchemas(spec) {
1764
+ const dictionary = getSchemaDictionary(spec);
1765
+ const knownByName = /* @__PURE__ */ new Map();
1766
+ for (const [name, schema] of Object.entries(dictionary)) {
1767
+ knownByName.set(toTypeScriptIdentifier(name), [name, schema]);
1768
+ }
1769
+ const refs = /* @__PURE__ */ new Set();
1770
+ collectSchemaRefs(spec.paths ?? {}, refs, spec);
1771
+ const selected = /* @__PURE__ */ new Set();
1772
+ const queue = [...refs];
1773
+ while (queue.length) {
1774
+ const name = queue.shift();
1775
+ if (!name || selected.has(name)) continue;
1776
+ selected.add(name);
1777
+ const model = knownByName.get(name);
1778
+ if (!model) continue;
1779
+ const childRefs = /* @__PURE__ */ new Set();
1780
+ collectSchemaRefs(model[1], childRefs, spec);
1781
+ for (const child of childRefs) if (!selected.has(child)) queue.push(child);
1782
+ }
1783
+ return [...selected].map((name) => knownByName.get(name)).filter((entry) => Boolean(entry));
1172
1784
  }
1173
- function endpointTypeBase(method, path2, operation) {
1174
- if (cleanText(operation.operationId)) return toTypeScriptIdentifier(cleanText(operation.operationId));
1175
- const pathName = path2.replace(/[{}]/g, "").split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1176
- return toTypeScriptIdentifier(`${method.charAt(0).toUpperCase()}${method.slice(1)}${pathName}`);
1785
+ function modelDeclaration(name, schema) {
1786
+ const identifier = toTypeScriptIdentifier(name);
1787
+ const description = safeComment(schema.description);
1788
+ const comment = description ? `/** ${description} */
1789
+ ` : "";
1790
+ if (schema.$ref) return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
1791
+ if (Array.isArray(schema.enum) || schema.const !== void 0 || schema.oneOf || schema.anyOf || schema.allOf) {
1792
+ return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
1793
+ }
1794
+ const kind = Array.isArray(schema.type) ? schema.type.find((item) => item !== "null") : schema.type;
1795
+ if (kind === "object" || schema.properties || schema.additionalProperties !== void 0) {
1796
+ const properties = entries(schema.properties);
1797
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
1798
+ const fields = properties.map(([field, value]) => {
1799
+ const metadata = schemaMetadata(value).map(safeComment).filter(Boolean).join("; ");
1800
+ const doc = metadata ? ` /** ${metadata} */
1801
+ ` : "";
1802
+ return `${doc} ${propertyKey(field)}${required.has(field) ? "" : "?"}: ${typeExpression(value)};`;
1803
+ });
1804
+ if (schema.additionalProperties === true) fields.push(" [key: string]: unknown;");
1805
+ else if (isObject(schema.additionalProperties))
1806
+ fields.push(` [key: string]: ${typeExpression(schema.additionalProperties)};`);
1807
+ return `${comment}export interface ${identifier} {
1808
+ ${fields.join("\n")}
1809
+ }`;
1810
+ }
1811
+ return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
1177
1812
  }
1178
1813
  function normalizeInlinePayloadSchemas(spec) {
1179
1814
  const document = structuredClone(spec);
@@ -1267,26 +1902,123 @@ function normalizeInlinePayloadSchemas(spec) {
1267
1902
  }
1268
1903
  return document;
1269
1904
  }
1270
- function transformOpenApiToMarkdown(spec, sourceUrl) {
1271
- const normalizedSpec = normalizeInlinePayloadSchemas(spec);
1272
- spec = normalizedSpec;
1905
+
1906
+ // src/transformer/index.ts
1907
+ var HTTP_METHODS2 = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
1908
+ function tagsOfEndpoint(endpoint) {
1909
+ const tags = endpoint.operation.tags;
1910
+ return Array.isArray(tags) && tags.length ? tags.map(String) : ["General"];
1911
+ }
1912
+ function filterSpec(spec, endpoints, options, warnings) {
1913
+ const includeTags = options.includeTags ?? [];
1914
+ const excludeTags = options.excludeTags ?? [];
1915
+ const hasFilter = includeTags.length > 0 || excludeTags.length > 0 || options.pathPrefix !== void 0;
1916
+ if (!hasFilter) return spec;
1917
+ const declared = Array.isArray(spec.tags) ? spec.tags.map((tag) => isObject(tag) && typeof tag.name === "string" ? tag.name : "").filter(Boolean) : [];
1918
+ const available = [.../* @__PURE__ */ new Set([...declared, ...endpoints.map((endpoint) => endpoint.tag)])];
1919
+ const unknownTags = [
1920
+ ...includeTags.filter((tag) => !available.includes(tag)),
1921
+ ...excludeTags.filter((tag) => !available.includes(tag))
1922
+ ];
1923
+ if (unknownTags.length > 0) {
1924
+ warnings.push(
1925
+ `Unknown tag(s): ${[...new Set(unknownTags)].join(", ")}. Available tags: ${available.join(", ") || "none"}.`
1926
+ );
1927
+ }
1928
+ const keep = (endpoint) => {
1929
+ if (options.pathPrefix && !endpoint.path.startsWith(options.pathPrefix)) return false;
1930
+ const tags = tagsOfEndpoint(endpoint);
1931
+ if (includeTags.length > 0 && !tags.some((tag) => includeTags.includes(tag))) return false;
1932
+ if (excludeTags.length > 0 && tags.some((tag) => excludeTags.includes(tag))) return false;
1933
+ return true;
1934
+ };
1935
+ const paths = {};
1936
+ for (const endpoint of endpoints.filter(keep)) {
1937
+ const source = isObject(spec.paths?.[endpoint.path]) ? spec.paths[endpoint.path] : {};
1938
+ const item = paths[endpoint.path] ?? { ...source };
1939
+ for (const method of HTTP_METHODS2) delete item[method];
1940
+ item[endpoint.method] = source[endpoint.method];
1941
+ paths[endpoint.path] = item;
1942
+ }
1943
+ return { ...spec, paths };
1944
+ }
1945
+ function buildTrimStages(_endpoints, tags) {
1946
+ const stages = [];
1947
+ const emptyTags = /* @__PURE__ */ new Set();
1948
+ stages.push({
1949
+ includeExamples: true,
1950
+ collapsedTags: emptyTags,
1951
+ indexOnly: false
1952
+ });
1953
+ stages.push({
1954
+ includeExamples: false,
1955
+ collapsedTags: emptyTags,
1956
+ indexOnly: false,
1957
+ action: "removed declared example payloads"
1958
+ });
1959
+ stages.push({
1960
+ includeExamples: false,
1961
+ descriptionLimit: 200,
1962
+ collapsedTags: emptyTags,
1963
+ indexOnly: false,
1964
+ action: "shortened long descriptions"
1965
+ });
1966
+ const collapsedSoFar = /* @__PURE__ */ new Set();
1967
+ for (const tag of [...tags].reverse()) {
1968
+ collapsedSoFar.add(tag);
1969
+ stages.push({
1970
+ includeExamples: false,
1971
+ descriptionLimit: 200,
1972
+ collapsedTags: new Set(collapsedSoFar),
1973
+ indexOnly: false,
1974
+ action: `collapsed parameter and response detail for tag "${tag}"`
1975
+ });
1976
+ }
1977
+ stages.push({
1978
+ includeExamples: false,
1979
+ descriptionLimit: 200,
1980
+ collapsedTags: new Set(tags),
1981
+ indexOnly: true,
1982
+ action: "kept only the Quick index entries for the remaining tags"
1983
+ });
1984
+ return stages;
1985
+ }
1986
+ function renderDigest(spec, sourceUrl, endpoints, tags, stage) {
1273
1987
  const info = isObject(spec.info) ? spec.info : {};
1274
1988
  const title = cleanText(info.title) || "API";
1275
1989
  const version = cleanText(info.version) || "unspecified";
1276
1990
  const openApiVersion = cleanText(spec.openapi) || (spec.swagger ? `Swagger ${spec.swagger}` : "OpenAPI");
1277
1991
  const servers = getServers(spec);
1278
- const endpoints = getEndpoints(spec);
1279
- const tags = collectTags(spec, endpoints);
1280
1992
  const indexRows = endpoints.map(
1281
1993
  (endpoint) => `| **${endpoint.method.toUpperCase()}** | \`${markdownCell(endpoint.path)}\` | ${shortOperationDescription(endpoint.operation)} | ${markdownCell(Array.isArray(endpoint.operation.tags) ? endpoint.operation.tags.join(", ") : endpoint.tag)} |`
1282
1994
  );
1995
+ const detailEndpoints = stage.indexOnly ? [] : endpoints.filter((endpoint) => !stage.collapsedTags.has(endpoint.tag));
1283
1996
  const groupedEndpoints = tags.map((tag) => {
1284
1997
  const members = endpoints.filter((endpoint) => endpoint.tag === tag);
1285
1998
  if (!members.length) return "";
1286
- return [`### ${tag}`, ...members.map((endpoint) => endpointMarkdown(spec, endpoint))].join("\n\n");
1999
+ const rendered = members.map(
2000
+ (endpoint) => endpointMarkdown(spec, endpoint, {
2001
+ includeExamples: stage.includeExamples,
2002
+ descriptionLimit: stage.descriptionLimit,
2003
+ collapsed: stage.collapsedTags.has(endpoint.tag)
2004
+ })
2005
+ );
2006
+ return [`### ${tag}`, ...rendered].join("\n\n");
1287
2007
  }).filter(Boolean);
1288
- const models = referencedSchemas(spec);
1289
- const modelSection = models.length ? [
2008
+ let modelSpec = spec;
2009
+ if (detailEndpoints.length !== endpoints.length) {
2010
+ const scopedPaths = {};
2011
+ for (const endpoint of detailEndpoints) {
2012
+ const source = isObject(spec.paths?.[endpoint.path]) ? spec.paths[endpoint.path] : {};
2013
+ const item = scopedPaths[endpoint.path] ?? { ...source };
2014
+ for (const method of HTTP_METHODS2) delete item[method];
2015
+ item[endpoint.method] = source[endpoint.method];
2016
+ scopedPaths[endpoint.path] = item;
2017
+ }
2018
+ modelSpec = { ...spec, paths: scopedPaths };
2019
+ }
2020
+ const models = referencedSchemas(modelSpec);
2021
+ const modelSection = !stage.indexOnly && models.length ? [
1290
2022
  "## Data types",
1291
2023
  "",
1292
2024
  "```typescript",
@@ -1309,62 +2041,122 @@ function transformOpenApiToMarkdown(spec, sourceUrl) {
1309
2041
  "",
1310
2042
  "| Method | Path | Summary | Tags |",
1311
2043
  "|---|---|---|---|",
1312
- ...indexRows.length ? indexRows : ["| \u2014 | \u2014 | No operations declared | \u2014 |"],
1313
- "",
1314
- "## Endpoints",
1315
- "",
1316
- ...groupedEndpoints.length ? groupedEndpoints : ["_No endpoints declared._"],
1317
- ...modelSection ? ["", modelSection] : []
2044
+ ...indexRows.length ? indexRows : ["| \u2014 | \u2014 | No operations declared | \u2014 |"]
1318
2045
  ];
2046
+ if (!stage.indexOnly) {
2047
+ sections.push(
2048
+ "",
2049
+ "## Endpoints",
2050
+ "",
2051
+ ...groupedEndpoints.length ? groupedEndpoints : ["_No endpoints declared._"]
2052
+ );
2053
+ }
2054
+ if (modelSection) {
2055
+ sections.push("", modelSection);
2056
+ }
1319
2057
  return `${sections.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
1320
2058
  `;
1321
2059
  }
2060
+ function appendTrimNote(markdown, actions, budget) {
2061
+ return `${markdown}
2062
+ ## Trimmed
1322
2063
 
1323
- // src/tokens.ts
1324
- function estimateTokens(text) {
1325
- const characters = text.length;
1326
- if (characters === 0) {
1327
- const zeroModel = (name, window2) => ({
1328
- name,
1329
- tokens: 0,
1330
- contextWindow: window2,
1331
- contextPercent: 0
1332
- });
1333
- return {
1334
- characters: 0,
1335
- models: {
1336
- claude: zeroModel("Claude (Sonnet / Opus)", 2e5),
1337
- openai: zeroModel("OpenAI (GPT-4o / o-series)", 128e3),
1338
- gemini: zeroModel("Google Gemini (Pro / Flash)", 1e6),
1339
- deepseek: zeroModel("DeepSeek (V3 / R1)", 128e3),
1340
- llama: zeroModel("Meta Llama (3.3 / 4)", 128e3)
1341
- }
1342
- };
2064
+ > _Trimmed to fit the ~${budget.toLocaleString()}-token budget: ${actions.join("; ")}._
2065
+ `;
2066
+ }
2067
+ function transformOpenApiToMarkdown(spec, sourceUrl, options = {}) {
2068
+ const warnings = [];
2069
+ const filteredSpec = filterSpec(spec, getEndpoints(spec), options, warnings);
2070
+ for (const message of warnings) options.onWarning?.(message);
2071
+ const normalizedSpec = normalizeInlinePayloadSchemas(filteredSpec);
2072
+ const endpoints = getEndpoints(normalizedSpec);
2073
+ const tags = collectTags(normalizedSpec, endpoints);
2074
+ const fullStage = {
2075
+ includeExamples: true,
2076
+ collapsedTags: /* @__PURE__ */ new Set(),
2077
+ indexOnly: false
2078
+ };
2079
+ if (options.maxTokens === void 0) {
2080
+ return renderDigest(normalizedSpec, sourceUrl, endpoints, tags, fullStage);
1343
2081
  }
1344
- const wordCount = (text.match(/\b[\w$]+\b/gu) || []).length;
1345
- const syntaxSymbols = (text.match(/[{}[\]()<>=:;,."`|/-]/g) || []).length;
1346
- const whitespaces = (text.match(/\s+/g) || []).length;
1347
- const baseTokens = wordCount * 1.18 + syntaxSymbols * 0.42 + whitespaces * 0.05;
1348
- const calculate = (name, ratio, contextWindow) => {
1349
- const tokens = Math.max(1, Math.round(baseTokens * ratio));
1350
- const contextPercent = Number((tokens / contextWindow * 100).toFixed(1));
1351
- return { name, tokens, contextWindow, contextPercent };
2082
+ const worst = (markdown2) => {
2083
+ const report = estimateTokens(markdown2);
2084
+ return Math.max(...Object.values(report.models).map((model) => model.tokens));
1352
2085
  };
1353
- return {
1354
- characters,
1355
- models: {
1356
- // Claude BPE splits subwords and code tokens slightly finer
1357
- claude: calculate("Claude (Sonnet / Opus)", 1.08, 2e5),
1358
- // OpenAI o200k_base has efficient compression on whitespace and symbols
1359
- openai: calculate("OpenAI (GPT-4o / o-series)", 0.95, 128e3),
1360
- // Gemini 256k vocab is the most compressed on markdown & code
1361
- gemini: calculate("Google Gemini (Pro / Flash)", 0.9, 1e6),
1362
- // DeepSeek 128k tokenizer
1363
- deepseek: calculate("DeepSeek (V3 / R1)", 0.97, 128e3),
1364
- // Llama 3/4 128k tokenizer
1365
- llama: calculate("Meta Llama (3.3 / 4)", 0.98, 128e3)
2086
+ const fits = (markdown2) => worst(markdown2) <= options.maxTokens;
2087
+ const full = renderDigest(normalizedSpec, sourceUrl, endpoints, tags, fullStage);
2088
+ if (fits(full)) return full;
2089
+ const stages = buildTrimStages(endpoints, tags);
2090
+ for (let i = 1; i < stages.length; i += 1) {
2091
+ const stage = stages[i];
2092
+ const markdown2 = renderDigest(normalizedSpec, sourceUrl, endpoints, tags, stage);
2093
+ if (fits(markdown2)) {
2094
+ return appendTrimNote(
2095
+ markdown2,
2096
+ stages.slice(1, i + 1).map((entry) => entry.action).filter((action) => Boolean(action)),
2097
+ options.maxTokens
2098
+ );
1366
2099
  }
1367
- };
2100
+ }
2101
+ const last = stages[stages.length - 1];
2102
+ const markdown = renderDigest(normalizedSpec, sourceUrl, endpoints, tags, last);
2103
+ const allActions = stages.slice(1).map((stage) => stage.action).filter((action) => Boolean(action));
2104
+ return appendTrimNote(markdown, allActions, options.maxTokens);
2105
+ }
2106
+
2107
+ // src/headers.ts
2108
+ var HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
2109
+ function validateHeaderName(name) {
2110
+ if (!name) throw new Error('Header name must not be empty. Expected "Name: value".');
2111
+ if (/[\r\n]/.test(name)) {
2112
+ throw new Error(`Invalid header name: header names must not contain CR/LF characters.`);
2113
+ }
2114
+ if (!HEADER_NAME_PATTERN.test(name)) {
2115
+ throw new Error(`Invalid header name "${name}": use a valid HTTP header token (letters, digits and -_).`);
2116
+ }
2117
+ }
2118
+ function validateHeaderValue(name, value) {
2119
+ if (/[\r\n]/.test(value)) {
2120
+ throw new Error(`Invalid value for header "${name}": values must not contain CR/LF.`);
2121
+ }
2122
+ if (/\0/.test(value)) {
2123
+ throw new Error(`Invalid value for header "${name}": values must not contain NUL bytes.`);
2124
+ }
2125
+ }
2126
+ function parseHeaderArgument(raw) {
2127
+ const separator = raw.indexOf(":");
2128
+ if (separator === -1) {
2129
+ if (/[\r\n]/.test(raw)) {
2130
+ throw new Error(`Invalid header: names and values must not contain CR/LF.`);
2131
+ }
2132
+ throw new Error(`Invalid header \u2014 expected "Name: value" format (for example "Authorization: Bearer <token>").`);
2133
+ }
2134
+ const name = raw.slice(0, separator).trim();
2135
+ const value = raw.slice(separator + 1).trim();
2136
+ validateHeaderName(name);
2137
+ validateHeaderValue(name, value);
2138
+ return { [name.toLowerCase()]: value };
2139
+ }
2140
+ function parseHeaderPairs(raw) {
2141
+ const result = {};
2142
+ for (const line of raw.split(/\r?\n/)) {
2143
+ if (!line.trim()) continue;
2144
+ Object.assign(result, parseHeaderArgument(line));
2145
+ }
2146
+ return result;
2147
+ }
2148
+ function mergeHeaders(...sources) {
2149
+ const result = {};
2150
+ for (const source of sources) {
2151
+ if (!source) continue;
2152
+ for (const [name, value] of Object.entries(source)) {
2153
+ if (/[\r\n]/.test(name) || /[\r\n]/.test(value)) {
2154
+ throw new Error(`Invalid header "${name}": CR/LF is not allowed in header names or values.`);
2155
+ }
2156
+ result[name.toLowerCase()] = value;
2157
+ }
2158
+ }
2159
+ return result;
1368
2160
  }
1369
2161
 
1370
2162
  // src/writer.ts
@@ -1398,12 +2190,16 @@ async function writeDigest(options) {
1398
2190
  }
1399
2191
 
1400
2192
  export {
2193
+ VERSION,
2194
+ toTypeScriptIdentifier,
1401
2195
  isOpenApiSpec,
1402
2196
  parseSpecText,
1403
2197
  fetchOpenApiSpec,
1404
- toTypeScriptIdentifier,
1405
- transformOpenApiToMarkdown,
1406
2198
  estimateTokens,
2199
+ transformOpenApiToMarkdown,
2200
+ parseHeaderArgument,
2201
+ parseHeaderPairs,
2202
+ mergeHeaders,
1407
2203
  defaultOutputDirectory,
1408
2204
  defaultFilename,
1409
2205
  resolveOutputDirectory,