swagger-digest 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,18 +1,76 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ VERSION,
3
4
  defaultFilename,
4
5
  defaultOutputDirectory,
5
6
  estimateTokens,
6
7
  fetchOpenApiSpec,
7
8
  transformOpenApiToMarkdown,
8
9
  writeDigest
9
- } from "./chunk-UZQM2SHL.js";
10
+ } from "./chunk-CBSDTIKN.js";
10
11
 
11
12
  // src/cli.ts
12
13
  import { input } from "@inquirer/prompts";
13
14
  import chalk from "chalk";
14
15
  import { Command } from "commander";
15
16
  import ora from "ora";
17
+
18
+ // src/headers.ts
19
+ var HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
20
+ function validateHeaderName(name) {
21
+ if (!name) throw new Error('Header name must not be empty. Expected "Name: value".');
22
+ if (/[\r\n]/.test(name)) {
23
+ throw new Error(`Invalid header name: header names must not contain CR/LF characters.`);
24
+ }
25
+ if (!HEADER_NAME_PATTERN.test(name)) {
26
+ throw new Error(`Invalid header name "${name}": use a valid HTTP header token (letters, digits and -_).`);
27
+ }
28
+ }
29
+ function validateHeaderValue(name, value) {
30
+ if (/[\r\n]/.test(value)) {
31
+ throw new Error(`Invalid value for header "${name}": values must not contain CR/LF.`);
32
+ }
33
+ if (/\0/.test(value)) {
34
+ throw new Error(`Invalid value for header "${name}": values must not contain NUL bytes.`);
35
+ }
36
+ }
37
+ function parseHeaderArgument(raw) {
38
+ const separator = raw.indexOf(":");
39
+ if (separator === -1) {
40
+ if (/[\r\n]/.test(raw)) {
41
+ throw new Error(`Invalid header: names and values must not contain CR/LF.`);
42
+ }
43
+ throw new Error(`Invalid header \u2014 expected "Name: value" format (for example "Authorization: Bearer <token>").`);
44
+ }
45
+ const name = raw.slice(0, separator).trim();
46
+ const value = raw.slice(separator + 1).trim();
47
+ validateHeaderName(name);
48
+ validateHeaderValue(name, value);
49
+ return { [name.toLowerCase()]: value };
50
+ }
51
+ function parseHeaderPairs(raw) {
52
+ const result = {};
53
+ for (const line of raw.split(/\r?\n/)) {
54
+ if (!line.trim()) continue;
55
+ Object.assign(result, parseHeaderArgument(line));
56
+ }
57
+ return result;
58
+ }
59
+ function mergeHeaders(...sources) {
60
+ const result = {};
61
+ for (const source of sources) {
62
+ if (!source) continue;
63
+ for (const [name, value] of Object.entries(source)) {
64
+ if (/[\r\n]/.test(name) || /[\r\n]/.test(value)) {
65
+ throw new Error(`Invalid header "${name}": CR/LF is not allowed in header names or values.`);
66
+ }
67
+ result[name.toLowerCase()] = value;
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+
73
+ // src/cli.ts
16
74
  function validateUrl(value) {
17
75
  try {
18
76
  const url = new URL(value);
@@ -25,11 +83,65 @@ function validateUrl(value) {
25
83
  function isInteractive() {
26
84
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
27
85
  }
86
+ function parseMaxBytes(value) {
87
+ if (value === void 0) return void 0;
88
+ const parsed = Number(value);
89
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
90
+ throw new Error(`Invalid --max-bytes value: ${value}. Use a positive integer number of bytes.`);
91
+ }
92
+ return parsed;
93
+ }
94
+ function parseMaxTokens(value) {
95
+ if (value === void 0) return void 0;
96
+ const parsed = Number(value);
97
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
98
+ throw new Error(`Invalid --max-tokens value: ${value}. Use a positive integer number of tokens.`);
99
+ }
100
+ return parsed;
101
+ }
102
+ function collectOption(value, previous) {
103
+ return [...previous ?? [], value];
104
+ }
105
+ function buildRequestHeaders(options) {
106
+ const fromEnv = process.env.SWAGGER_DIGEST_HEADER ? parseHeaderPairs(process.env.SWAGGER_DIGEST_HEADER) : {};
107
+ const fromFlags = (options.headers ?? []).map(parseHeaderArgument);
108
+ const cookieHeader = options.cookie !== void 0 ? parseHeaderArgument(`Cookie: ${options.cookie}`) : {};
109
+ return mergeHeaders(fromEnv, ...fromFlags, cookieHeader);
110
+ }
28
111
  async function main() {
29
112
  const program = new Command();
30
- program.name("swagger-digest").description("Convert a Swagger UI page or OpenAPI URL into compact, AI-friendly Markdown.").version("0.1.1").option("-u, --url <url>", "Swagger UI page URL or direct OpenAPI spec URL").option("-d, --out-dir <directory>", "Directory where the Markdown digest will be saved").option("-f, --filename <filename>", "Output filename (defaults to <api-title>-digest.md)").option("--no-browser", "Disable the optional Playwright headless-browser fallback").addHelpText(
113
+ program.name("swagger-digest").description("Convert a Swagger UI page or OpenAPI URL into compact, AI-friendly Markdown.").version(VERSION).option("-u, --url <url>", "Swagger UI page URL or direct OpenAPI spec URL").option("-d, --out-dir <directory>", "Directory where the Markdown digest will be saved").option("-f, --filename <filename>", "Output filename (defaults to <api-title>-digest.md)").option("--no-browser", "Disable the optional Playwright headless-browser fallback").option("--max-bytes <bytes>", "Maximum response size in bytes (default 10000000 = 10 MB)", "10000000").option(
114
+ '-H, --header <"Name: value">',
115
+ "Custom request header, repeatable. Sent only to the URL you supplied.",
116
+ collectOption,
117
+ []
118
+ ).option("--cookie <string>", "Convenience for a Cookie request header (same-origin scoping applies)").option("--include-tag <tag>", "Keep only operations with this tag, repeatable", collectOption, []).option("--exclude-tag <tag>", "Drop operations with this tag, repeatable", collectOption, []).option("--path-prefix <prefix>", "Keep only operations whose path starts with this prefix").option("--max-tokens <n>", "Trim the digest until the approximate token estimate fits this budget").option("--spec <url>", "Explicit OpenAPI spec URL; overrides discovery from the Swagger UI page").option("--stdout", "Print the digest to standard output instead of writing a file").addHelpText(
31
119
  "after",
32
- "\nExamples:\n npx swagger-digest\n npx swagger-digest --url https://petstore.swagger.io/ --out-dir ./docs\n npx swagger-digest -u https://example.com/openapi.yaml -f payments-api.md"
120
+ [
121
+ "",
122
+ "Examples:",
123
+ " npx swagger-digest",
124
+ " npx swagger-digest --url https://petstore.swagger.io/ --out-dir ./docs",
125
+ " npx swagger-digest -u https://example.com/openapi.yaml -f payments-api.md",
126
+ ' npx swagger-digest -u https://api.example.com/swagger-ui -H "Authorization: Bearer <token>"',
127
+ "",
128
+ "Authentication:",
129
+ " -H/--header and --cookie values are sent ONLY to the origin of --url.",
130
+ ' You can also set SWAGGER_DIGEST_HEADER with newline-separated "Name: value" pairs;',
131
+ " -H flags override it on conflicts. Avoid passing secrets on the command line if",
132
+ " your shell saves history \u2014 prefer the environment variable.",
133
+ "",
134
+ "Filtering and token budget:",
135
+ " --include-tag/--exclude-tag/--path-prefix remove operations (and the data",
136
+ " models only they referenced) before rendering.",
137
+ " --max-tokens trims the digest in this order until the estimate fits:",
138
+ " 1. drop declared example payloads \u2192 2. shorten descriptions \u2192",
139
+ " 3. collapse parameter/response detail per tag (lowest-priority tag first:",
140
+ " the tag that appears last in the document is collapsed first) \u2192",
141
+ ' 4. keep only the Quick index entries. A "Trimmed" note records what was',
142
+ " removed. The estimator is a heuristic, NOT tokenizer-accurate \u2014 treat the",
143
+ " budget as a guide, not a guarantee."
144
+ ].join("\n")
33
145
  );
34
146
  program.parse(process.argv);
35
147
  const options = program.opts();
@@ -42,22 +154,45 @@ async function main() {
42
154
  });
43
155
  const validUrl = validateUrl(url);
44
156
  if (validUrl !== true) throw new Error(validUrl);
45
- const outputDirectory = options.outDir ?? (isInteractive() ? await input({
157
+ if (options.stdout && (options.outDir !== void 0 || options.filename !== void 0)) {
158
+ throw new Error(
159
+ "--stdout cannot be combined with --out-dir or --filename: it prints the digest to standard output instead of writing a file. Drop --stdout, or remove --out-dir/--filename."
160
+ );
161
+ }
162
+ const outputDirectory = options.outDir ?? (isInteractive() && !options.stdout ? await input({
46
163
  message: "Destination directory",
47
164
  default: defaultOutputDirectory(),
48
165
  validate: (value) => value.trim() ? true : "Enter a destination directory."
49
166
  }) : defaultOutputDirectory());
167
+ const maxBytes = parseMaxBytes(options.maxBytes);
168
+ const requestHeaders = buildRequestHeaders(options);
50
169
  const spinner = ora({ text: "Fetching and parsing OpenAPI definition\u2026", color: "cyan" }).start();
51
170
  let result;
52
171
  try {
53
- result = await fetchOpenApiSpec(url, { useBrowserFallback: options.browser !== false });
172
+ result = await fetchOpenApiSpec(url, {
173
+ useBrowserFallback: options.browser !== false,
174
+ maxBytes,
175
+ headers: requestHeaders,
176
+ specUrl: options.spec,
177
+ warn: (message) => console.error(chalk.yellow(`Warning: ${message}`))
178
+ });
54
179
  } catch (error) {
55
180
  spinner.fail("Could not extract an OpenAPI definition");
56
181
  throw error;
57
182
  }
58
183
  const title = typeof result.spec.info?.title === "string" ? result.spec.info.title : "API";
59
- const markdown = transformOpenApiToMarkdown(result.spec, result.sourceUrl);
184
+ const markdown = transformOpenApiToMarkdown(result.spec, result.sourceUrl, {
185
+ includeTags: options.includeTag,
186
+ excludeTags: options.excludeTag,
187
+ pathPrefix: options.pathPrefix,
188
+ maxTokens: parseMaxTokens(options.maxTokens),
189
+ onWarning: (message) => console.error(chalk.yellow(`Warning: ${message}`))
190
+ });
60
191
  spinner.succeed(`Parsed ${title} (${result.retrievalMethod})`);
192
+ if (options.stdout) {
193
+ process.stdout.write(markdown);
194
+ return;
195
+ }
61
196
  const filename = options.filename ?? (isInteractive() ? await input({
62
197
  message: "Output filename",
63
198
  default: defaultFilename(title),
@@ -71,7 +206,8 @@ async function main() {
71
206
  const endpointCount = Object.values(result.spec.paths ?? {}).reduce((count, pathItem) => {
72
207
  if (!pathItem || typeof pathItem !== "object") return count;
73
208
  const methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
74
- return count + methods.filter((method) => pathItem[method] && typeof pathItem[method] === "object").length;
209
+ const operations = pathItem;
210
+ return count + methods.filter((method) => operations[method] && typeof operations[method] === "object").length;
75
211
  }, 0);
76
212
  const modelCount = Object.keys(result.spec.components?.schemas ?? result.spec.definitions ?? {}).length;
77
213
  console.log("");
@@ -82,7 +218,7 @@ async function main() {
82
218
  }
83
219
  console.log(` ${chalk.dim("\u2022")} Characters: ${chalk.dim(report.characters.toLocaleString())}`);
84
220
  console.log("");
85
- console.log(chalk.bold("Estimated token usage (5 Frontier Models):"));
221
+ console.log(chalk.bold("Estimated token usage (approximate):"));
86
222
  for (const model of Object.values(report.models)) {
87
223
  const windowLabel = model.contextWindow >= 1e6 ? `${model.contextWindow / 1e6}M` : `${model.contextWindow / 1e3}k`;
88
224
  const paddedName = `${model.name}:`.padEnd(30, " ");
package/dist/index.d.ts CHANGED
@@ -2,6 +2,29 @@ type JsonObject = Record<string, any>;
2
2
  interface FetchOptions {
3
3
  timeoutMs?: number;
4
4
  useBrowserFallback?: boolean;
5
+ /** Maximum response body size in bytes (default 10 MB). */
6
+ maxBytes?: number;
7
+ /** Maximum number of redirects to follow (default 5). */
8
+ maxRedirects?: number;
9
+ /** Opt-in: reject hostnames resolving to private/loopback/metadata addresses. */
10
+ blockPrivateNetworks?: boolean;
11
+ /** Timeout for fallback-path probes (default 5000ms; main requests use timeoutMs). */
12
+ probeTimeoutMs?: number;
13
+ /**
14
+ * Custom request headers (e.g. Authorization or Cookie). These are sent ONLY
15
+ * to the origin of the URL passed to fetchOpenApiSpec; cross-origin requests
16
+ * (including cross-origin redirects and third-party browser requests) never
17
+ * receive them.
18
+ */
19
+ headers?: Record<string, string>;
20
+ /** Receives non-fatal notices, e.g. headers withheld on a cross-origin fetch. */
21
+ warn?: (message: string) => void;
22
+ /**
23
+ * Explicit spec URL that overrides discovery entirely: it is fetched first
24
+ * (with the same header scoping as the main URL) and the page is never
25
+ * consulted when it parses successfully.
26
+ */
27
+ specUrl?: string;
5
28
  }
6
29
  interface FetchResult {
7
30
  spec: JsonObject;
@@ -16,8 +39,24 @@ declare function parseSpecText(text: string, source?: string): JsonObject;
16
39
  declare function fetchOpenApiSpec(input: string, options?: FetchOptions): Promise<FetchResult>;
17
40
 
18
41
  declare function toTypeScriptIdentifier(value: string): string;
42
+
43
+ interface TransformOptions {
44
+ /** Keep only operations carrying at least one of these tags. Repeatable. */
45
+ includeTags?: string[];
46
+ /** Drop operations carrying any of these tags. Repeatable. */
47
+ excludeTags?: string[];
48
+ /** Keep only operations whose path starts with this prefix. */
49
+ pathPrefix?: string;
50
+ /**
51
+ * Trim the digest until the (approximate, heuristic) token estimate fits this
52
+ * budget. The estimator is not tokenizer-accurate; treat the budget as a guide.
53
+ */
54
+ maxTokens?: number;
55
+ /** Receives non-fatal notices, e.g. unknown tag names. */
56
+ onWarning?: (message: string) => void;
57
+ }
19
58
  /** Convert an OpenAPI 2.0, 3.0, or 3.1 document to a compact Markdown digest. */
20
- declare function transformOpenApiToMarkdown(spec: JsonObject, sourceUrl?: string): string;
59
+ declare function transformOpenApiToMarkdown(spec: JsonObject, sourceUrl?: string, options?: TransformOptions): string;
21
60
 
22
61
  interface ModelTokenEstimate {
23
62
  name: string;
@@ -36,8 +75,9 @@ interface DigestTokenReport {
36
75
  };
37
76
  }
38
77
  /**
39
- * Estimates token counts for Markdown/code across 5 frontier model tokenizers.
40
- * Accurately models differences between cl100k/o200k, Claude BPE, SentencePiece 256k, and 128k BPE.
78
+ * Produces ROUGH token-count estimates for Markdown/code. The numbers are a
79
+ * heuristic (word + symbol density with per-model multipliers), not accurate
80
+ * tokenizer counts — use them as an order-of-magnitude guide only.
41
81
  */
42
82
  declare function estimateTokens(text: string): DigestTokenReport;
43
83
 
@@ -52,4 +92,4 @@ declare function writeDigest(options: {
52
92
  }): Promise<string>;
53
93
 
54
94
  export { defaultFilename, defaultOutputDirectory, estimateTokens, fetchOpenApiSpec, isOpenApiSpec, normalizeFilename, parseSpecText, resolveOutputDirectory, toTypeScriptIdentifier, transformOpenApiToMarkdown, writeDigest };
55
- export type { DigestTokenReport, FetchOptions, FetchResult, JsonObject, ModelTokenEstimate };
95
+ export type { DigestTokenReport, FetchOptions, FetchResult, JsonObject, ModelTokenEstimate, TransformOptions };
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  toTypeScriptIdentifier,
11
11
  transformOpenApiToMarkdown,
12
12
  writeDigest
13
- } from "./chunk-UZQM2SHL.js";
13
+ } from "./chunk-CBSDTIKN.js";
14
14
  export {
15
15
  defaultFilename,
16
16
  defaultOutputDirectory,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "swagger-digest",
3
- "version": "0.1.1",
4
- "description": "Interactive CLI that converts Swagger UI pages and OpenAPI specs into compact Markdown for AI coding agents.",
3
+ "version": "0.1.2",
4
+ "description": "Turn Swagger UI pages and OpenAPI specs into compact, AI-friendly Markdown API digests.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  },
14
14
  "bin": {
15
- "swagger-digest": "dist/cli.js"
15
+ "swagger-digest": "./dist/cli.js"
16
16
  },
17
17
  "author": "VladLegkowski <VladLegkowski@users.noreply.github.com>",
18
18
  "repository": {
@@ -39,10 +39,10 @@
39
39
  "lint": "eslint .",
40
40
  "prepare": "husky",
41
41
  "prepack": "npm run build",
42
- "prepublishOnly": "npm run build && npm test && npm run lint"
42
+ "prepublishOnly": "npm test"
43
43
  },
44
44
  "engines": {
45
- "node": ">=20.0.0"
45
+ "node": ">=20"
46
46
  },
47
47
  "dependencies": {
48
48
  "@inquirer/prompts": "^7.3.2",
@@ -73,14 +73,12 @@
73
73
  }
74
74
  },
75
75
  "keywords": [
76
- "swagger",
77
76
  "openapi",
78
- "cli",
79
- "ai",
80
- "llm",
77
+ "swagger",
81
78
  "markdown",
82
- "docs",
83
- "ai-coding-agent"
79
+ "llm",
80
+ "ai-coding-agent",
81
+ "cli"
84
82
  ],
85
83
  "license": "MIT"
86
84
  }