openapi-agent-reference 1.0.0 → 2.0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. The project follows [Semantic Versioning](https://semver.org/).
4
+
5
+ ## Unreleased
6
+
7
+ ## 2.0.0 - 2026-07-13
8
+
9
+ ### Added
10
+
11
+ - `openapi-md` executable and `groups` / `generate` subcommands.
12
+ - JSON group discovery with `groups --json`.
13
+ - Standard input and standard output support through `-`.
14
+ - Public CommonJS library entry point.
15
+ - Cross-platform CLI integration tests and npm publishing safeguards.
16
+
17
+ ### Changed
18
+
19
+ - The installed CLI command is now `openapi-md`.
20
+ - The minimum supported Node.js version is now 22.
21
+
22
+ ## 1.0.2
23
+
24
+ - Added exact primary-tag group filtering and group discovery.
@@ -0,0 +1,28 @@
1
+ # Contributing
2
+
3
+ ## Development
4
+
5
+ Use Node.js 22 or newer.
6
+
7
+ ```bash
8
+ npm ci
9
+ npm test
10
+ ```
11
+
12
+ Run the CLI directly while developing:
13
+
14
+ ```bash
15
+ node bin/openapi-md.js groups openapi.json
16
+ node bin/openapi-md.js generate openapi.json --group "Access" --output -
17
+ ```
18
+
19
+ ## Pull requests
20
+
21
+ - Add or update tests for behavior changes.
22
+ - Keep the CLI backward compatible unless the change is intentionally released as a new major version.
23
+ - Update `README.md` and `CHANGELOG.md` for user-visible changes.
24
+ - Run `npm pack --dry-run` and inspect the publish file list before releasing.
25
+
26
+ ## Releases
27
+
28
+ Use Semantic Versioning. Publishing is performed by the GitHub Actions release workflow through npm trusted publishing; do not add npm access tokens to the repository.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dikong1
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,43 +1,161 @@
1
- # OpenAPI Agent Reference Generator
1
+ # OpenAPI Agent Reference
2
2
 
3
- A zero-runtime-dependency Node.js CLI that turns an OpenAPI JSON document into a single navigable Markdown API reference. The output is designed for both developers and AI agents.
3
+ Generate compact, navigable Markdown API references from OpenAPI JSON. The output is designed for developers and AI agents and includes authentication, parameters, request and response bodies, examples, and reusable schemas.
4
4
 
5
- It includes:
5
+ ## Requirements
6
6
 
7
- - endpoints grouped by their primary OpenAPI tag;
8
- - operation IDs, authentication requirements, deprecation status, and descriptions;
9
- - path, query, header, and cookie parameters with constraints;
10
- - request bodies and response bodies for every documented media type;
11
- - explicit and schema-generated JSON examples;
12
- - response headers and OpenAPI links;
13
- - OAuth, bearer, API-key, and other security scheme details;
14
- - a reusable schema catalog with required fields and `$ref` links;
15
- - OpenAPI 3.x support plus practical Swagger 2.0 compatibility.
7
+ Node.js 22 or newer. The package has no runtime dependencies.
16
8
 
17
- ## Requirements
9
+ ## Quick start
10
+
11
+ Run the published package without installing it:
12
+
13
+ ```bash
14
+ npx openapi-agent-reference@latest groups openapi.json
15
+ npx openapi-agent-reference@latest generate openapi.json --group "Equipment Service" -o EQUIPMENT_SERVICE.md
16
+ ```
17
+
18
+ Or install it in a project:
19
+
20
+ ```bash
21
+ npm install --save-dev openapi-agent-reference
22
+ ```
23
+
24
+ The short command is then available through `npx` and npm scripts:
25
+
26
+ ```bash
27
+ npx openapi-md groups openapi.json
28
+ npx openapi-md generate openapi.json -o API_REFERENCE.md
29
+ ```
30
+
31
+ ## CLI
32
+
33
+ ### Discover groups
34
+
35
+ Groups are primary OpenAPI operation tags. List their exact, case-sensitive names before generating a focused reference:
36
+
37
+ ```bash
38
+ openapi-md groups openapi.json
39
+ ```
40
+
41
+ Example output:
42
+
43
+ ```text
44
+ GROUP OPERATIONS
45
+ ------------------------ ----------
46
+ Access 9
47
+ Equipment Service 49
48
+ ```
49
+
50
+ For machine-readable output:
51
+
52
+ ```bash
53
+ openapi-md groups openapi.json --json
54
+ ```
18
55
 
19
- Node.js 18 or newer. No package installation is required.
56
+ ```json
57
+ [
58
+ { "name": "Access", "operations": 9 },
59
+ { "name": "Equipment Service", "operations": 49 }
60
+ ]
61
+ ```
20
62
 
21
- ## Usage
63
+ ### Generate one group
22
64
 
23
65
  ```bash
24
- node src/openapi-to-markdown.js openapi.json -o API_REFERENCE.md
66
+ openapi-md generate openapi.json --group "Access" --output ACCESS.md
25
67
  ```
26
68
 
27
- Or use the included npm script for the default paths:
69
+ The result contains only the selected group's endpoints and the reusable schemas they reference, including transitive schema dependencies. A misspelled group produces a close-match suggestion and the complete list of valid groups.
70
+
71
+ ### Generate the complete reference
72
+
73
+ ```bash
74
+ openapi-md generate openapi.json --output API_REFERENCE.md
75
+ ```
76
+
77
+ ### Pipelines and standard streams
78
+
79
+ Use `-` for standard input or standard output:
28
80
 
29
81
  ```bash
30
- npm run generate
82
+ curl https://example.com/openapi.json | openapi-md groups - --json
83
+ openapi-md generate openapi.json --group Users --output -
31
84
  ```
32
85
 
33
- Options:
86
+ Diagnostics are written to stderr, so Markdown and JSON output can be piped safely.
87
+
88
+ ### Options
34
89
 
35
90
  ```text
36
- -o, --output <file> Output file (default: API_REFERENCE.md)
37
- --no-schema-catalog Omit reusable schemas
38
- --no-examples Omit generated JSON examples
39
- --include-extensions Include x-* operation extensions
40
- -h, --help Show CLI help
91
+ -o, --output <file|-> Output file, or - for stdout
92
+ -g, --group <name> Generate only the exact, case-sensitive group
93
+ --json JSON output for the groups command
94
+ --no-schema-catalog Omit reusable schemas
95
+ --no-examples Omit generated JSON examples
96
+ --include-extensions Include x-* operation extensions
97
+ -v, --version Show the installed version
98
+ -h, --help Show CLI help
99
+ ```
100
+
101
+ The original flag-based syntax remains available for backward compatibility:
102
+
103
+ ```bash
104
+ openapi-md openapi.json --list-groups
105
+ openapi-md openapi.json --group Access -o ACCESS.md
106
+ ```
107
+
108
+ ## Programmatic API
109
+
110
+ The CommonJS entry point exposes the generator and OpenAPI inspection helpers:
111
+
112
+ ```js
113
+ const fs = require('node:fs');
114
+ const {
115
+ generateMarkdown,
116
+ groupList,
117
+ } = require('openapi-agent-reference');
118
+
119
+ const spec = JSON.parse(fs.readFileSync('openapi.json', 'utf8'));
120
+
121
+ console.log(groupList(spec));
122
+
123
+ const markdown = generateMarkdown(spec, {
124
+ group: 'Access',
125
+ examples: true,
126
+ schemaCatalog: true,
127
+ });
41
128
  ```
42
129
 
43
- The generator never calls the API and does not resolve external `$ref` URLs. Local component references such as `#/components/schemas/User` are linked to the schema catalog.
130
+ Public exports:
131
+
132
+ - `generateMarkdown(spec, options)`
133
+ - `groupList(spec)`
134
+ - `collectOperations(spec)`
135
+ - `formatGroupList(spec)`
136
+ - `referencedSchemaNames(groups, spec)`
137
+ - `sampleForSchema(schema, spec)`
138
+
139
+ ## Generated reference content
140
+
141
+ - Endpoints grouped by their primary OpenAPI tag
142
+ - Operation IDs, descriptions, authentication, and deprecation state
143
+ - Path, query, header, and cookie parameters with constraints
144
+ - Request and response bodies for every documented media type
145
+ - Explicit and schema-generated JSON examples
146
+ - Response headers and OpenAPI links
147
+ - OAuth, bearer, API-key, and other security scheme details
148
+ - Linked reusable schemas with required fields and recursive-reference protection
149
+ - OpenAPI 3.x support and practical Swagger 2.0 compatibility
150
+
151
+ Local `$ref` values are resolved and linked. External `$ref` URLs are preserved but are not downloaded.
152
+
153
+ ## Development and releases
154
+
155
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development and release guidance. Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
156
+
157
+ Publishing uses npm trusted publishing from GitHub Actions. Before the first automated release, configure `npm-publish.yml` as the trusted publisher in the package settings on npmjs.com.
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('../src/cli');
4
+
5
+ main();
package/package.json CHANGED
@@ -1,17 +1,24 @@
1
1
  {
2
2
  "name": "openapi-agent-reference",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Generate agent-friendly Markdown API reference documentation from OpenAPI JSON.",
5
5
  "type": "commonjs",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./package.json": "./package.json"
10
+ },
6
11
  "bin": {
7
- "openapi-to-md": "src/openapi-to-markdown.js"
12
+ "openapi-md": "bin/openapi-md.js"
8
13
  },
9
14
  "scripts": {
10
- "generate": "node src/openapi-to-markdown.js openapi.json -o API_REFERENCE.md",
11
- "test": "node --test"
15
+ "generate": "node bin/openapi-md.js generate openapi.json -o API_REFERENCE.md",
16
+ "groups": "node bin/openapi-md.js groups openapi.json",
17
+ "test": "node --test",
18
+ "prepublishOnly": "npm test"
12
19
  },
13
20
  "engines": {
14
- "node": ">=18"
21
+ "node": ">=22"
15
22
  },
16
23
  "license": "MIT",
17
24
  "repository": {
@@ -23,7 +30,22 @@
23
30
  "url": "https://github.com/Dikong1/extractOpenApi/issues"
24
31
  },
25
32
  "files": [
33
+ "bin/",
26
34
  "src/",
27
- "README.md"
28
- ]
35
+ "README.md",
36
+ "LICENSE",
37
+ "CHANGELOG.md",
38
+ "CONTRIBUTING.md"
39
+ ],
40
+ "keywords": [
41
+ "openapi",
42
+ "swagger",
43
+ "markdown",
44
+ "api-documentation",
45
+ "ai-agent",
46
+ "cli"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
29
51
  }
package/src/cli.js ADDED
@@ -0,0 +1,167 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const packageJson = require('../package.json');
4
+ const {
5
+ collectOperations,
6
+ formatGroupList,
7
+ generateMarkdown,
8
+ groupList,
9
+ } = require('./openapi-to-markdown');
10
+
11
+ function usage() {
12
+ return `Usage:
13
+ openapi-md groups [openapi.json|-] [--json]
14
+ openapi-md generate [openapi.json|-] [options]
15
+ openapi-md [openapi.json|-] [options] Legacy syntax
16
+
17
+ Commands:
18
+ groups List exact primary-tag group names and operation counts
19
+ generate Generate a full or single-group Markdown reference
20
+
21
+ Options:
22
+ -o, --output <file|-> Output Markdown file, or - for stdout (default: API_REFERENCE.md)
23
+ -g, --group <name> Generate only the exact, case-sensitive group
24
+ --json Emit machine-readable JSON from the groups command
25
+ --list-groups Legacy alias for the groups command
26
+ --no-schema-catalog Omit the reusable schema catalog
27
+ --no-examples Omit generated JSON examples
28
+ --include-extensions Include x-* operation extensions
29
+ -v, --version Show the package version
30
+ -h, --help Show this help
31
+
32
+ Use - as the input path to read OpenAPI JSON from stdin.
33
+ `;
34
+ }
35
+
36
+ function requiredValue(argv, index, option) {
37
+ const value = argv[index + 1];
38
+ if (value === undefined || value.startsWith('-')) throw new Error(`${option} requires a value`);
39
+ return value;
40
+ }
41
+
42
+ function parseArgs(argv) {
43
+ const args = [...argv];
44
+ let command = 'generate';
45
+ if (args[0] === 'groups' || args[0] === 'generate') command = args.shift();
46
+
47
+ const options = {
48
+ command,
49
+ input: 'openapi.json',
50
+ output: 'API_REFERENCE.md',
51
+ schemaCatalog: true,
52
+ examples: true,
53
+ includeExtensions: false,
54
+ group: undefined,
55
+ json: false,
56
+ };
57
+ let inputSet = false;
58
+ for (let index = 0; index < args.length; index += 1) {
59
+ const arg = args[index];
60
+ if (arg === '-h' || arg === '--help') options.help = true;
61
+ else if (arg === '-v' || arg === '--version') options.version = true;
62
+ else if (arg === '--list-groups') options.command = 'groups';
63
+ else if (arg === '--json') options.json = true;
64
+ else if (arg === '--no-schema-catalog') options.schemaCatalog = false;
65
+ else if (arg === '--no-examples') options.examples = false;
66
+ else if (arg === '--include-extensions') options.includeExtensions = true;
67
+ else if (arg === '-g' || arg === '--group') {
68
+ options.group = requiredValue(args, index, arg);
69
+ index += 1;
70
+ } else if (arg === '-o' || arg === '--output') {
71
+ const value = args[index + 1];
72
+ if (value === undefined) throw new Error(`${arg} requires a value`);
73
+ options.output = value;
74
+ index += 1;
75
+ } else if (arg === '-' && !inputSet) {
76
+ options.input = arg;
77
+ inputSet = true;
78
+ } else if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}`);
79
+ else if (!inputSet) {
80
+ options.input = arg;
81
+ inputSet = true;
82
+ } else throw new Error(`Unexpected argument: ${arg}`);
83
+ }
84
+ if (options.command === 'groups' && options.group !== undefined) {
85
+ throw new Error('The groups command cannot be combined with --group');
86
+ }
87
+ if (options.command !== 'groups' && options.json) {
88
+ throw new Error('--json is only available with the groups command');
89
+ }
90
+ return options;
91
+ }
92
+
93
+ function readSpec(input) {
94
+ const label = input === '-' ? 'stdin' : path.resolve(input);
95
+ const source = fs.readFileSync(input === '-' ? 0 : label, 'utf8').replace(/^\uFEFF/, '');
96
+ let spec;
97
+ try { spec = JSON.parse(source); } catch (error) { throw new Error(`Invalid JSON in ${label}: ${error.message}`); }
98
+ if (!spec || typeof spec !== 'object' || (!spec.openapi && !spec.swagger) || !spec.paths || typeof spec.paths !== 'object') {
99
+ throw new Error('Input is not a valid OpenAPI document with a paths object');
100
+ }
101
+ return spec;
102
+ }
103
+
104
+ function writeFileSafely(outputPath, content) {
105
+ const absolutePath = path.resolve(outputPath);
106
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
107
+ const temporaryPath = path.join(
108
+ path.dirname(absolutePath),
109
+ `.${path.basename(absolutePath)}.${process.pid}.${Date.now()}.tmp`,
110
+ );
111
+ fs.writeFileSync(temporaryPath, content, 'utf8');
112
+ try {
113
+ fs.renameSync(temporaryPath, absolutePath);
114
+ } catch (error) {
115
+ if (!['EEXIST', 'EPERM'].includes(error.code)) throw error;
116
+ fs.copyFileSync(temporaryPath, absolutePath);
117
+ fs.unlinkSync(temporaryPath);
118
+ } finally {
119
+ if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
120
+ }
121
+ return absolutePath;
122
+ }
123
+
124
+ function run(argv = process.argv.slice(2), streams = process) {
125
+ let options;
126
+ try { options = parseArgs(argv); } catch (error) {
127
+ streams.stderr.write(`Error: ${error.message}\n\n${usage()}`);
128
+ return 1;
129
+ }
130
+ if (options.help) {
131
+ streams.stdout.write(usage());
132
+ return 0;
133
+ }
134
+ if (options.version) {
135
+ streams.stdout.write(`${packageJson.version}\n`);
136
+ return 0;
137
+ }
138
+ try {
139
+ const spec = readSpec(options.input);
140
+ if (options.command === 'groups') {
141
+ const output = options.json ? `${JSON.stringify(groupList(spec), null, 2)}\n` : formatGroupList(spec);
142
+ streams.stdout.write(output);
143
+ return 0;
144
+ }
145
+
146
+ const markdown = generateMarkdown(spec, options);
147
+ const groups = collectOperations(spec).filter(([name]) => options.group === undefined || name === options.group);
148
+ const operations = groups.reduce((count, [, items]) => count + items.length, 0);
149
+ if (options.output === '-') {
150
+ streams.stdout.write(markdown);
151
+ return 0;
152
+ }
153
+ const outputPath = writeFileSafely(options.output, markdown);
154
+ const scope = options.group === undefined ? '' : ` for group ${JSON.stringify(options.group)}`;
155
+ streams.stdout.write(`Generated ${outputPath}${scope} (${operations} operations, ${Buffer.byteLength(markdown)} bytes)\n`);
156
+ return 0;
157
+ } catch (error) {
158
+ streams.stderr.write(`Error: ${error.message}\n`);
159
+ return 1;
160
+ }
161
+ }
162
+
163
+ function main(argv = process.argv.slice(2)) {
164
+ process.exitCode = run(argv);
165
+ }
166
+
167
+ module.exports = { main, parseArgs, readSpec, run, usage, writeFileSafely };
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ const {
2
+ collectOperations,
3
+ formatGroupList,
4
+ generateMarkdown,
5
+ groupList,
6
+ referencedSchemaNames,
7
+ sampleForSchema,
8
+ } = require('./openapi-to-markdown');
9
+
10
+ module.exports = {
11
+ collectOperations,
12
+ formatGroupList,
13
+ generateMarkdown,
14
+ groupList,
15
+ referencedSchemaNames,
16
+ sampleForSchema,
17
+ };
@@ -1,49 +1,5 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
-
6
1
  const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
7
2
 
8
- function usage() {
9
- return `Usage: openapi-to-md [openapi.json] [options]
10
-
11
- Options:
12
- -o, --output <file> Output Markdown file (default: API_REFERENCE.md)
13
- --no-schema-catalog Omit the reusable schema catalog
14
- --no-examples Omit generated JSON examples
15
- --include-extensions Include x-* operation extensions
16
- -h, --help Show this help
17
- `;
18
- }
19
-
20
- function parseArgs(argv) {
21
- const options = {
22
- input: 'openapi.json',
23
- output: 'API_REFERENCE.md',
24
- schemaCatalog: true,
25
- examples: true,
26
- includeExtensions: false,
27
- };
28
- let inputSet = false;
29
- for (let index = 0; index < argv.length; index += 1) {
30
- const arg = argv[index];
31
- if (arg === '-h' || arg === '--help') options.help = true;
32
- else if (arg === '--no-schema-catalog') options.schemaCatalog = false;
33
- else if (arg === '--no-examples') options.examples = false;
34
- else if (arg === '--include-extensions') options.includeExtensions = true;
35
- else if (arg === '-o' || arg === '--output') {
36
- if (!argv[index + 1]) throw new Error(`${arg} requires a file path`);
37
- options.output = argv[++index];
38
- } else if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}`);
39
- else if (!inputSet) {
40
- options.input = arg;
41
- inputSet = true;
42
- } else throw new Error(`Unexpected argument: ${arg}`);
43
- }
44
- return options;
45
- }
46
-
47
3
  function escapeTable(value) {
48
4
  return String(value ?? '—').replace(/\r?\n/g, '<br>').replace(/\|/g, '\\|');
49
5
  }
@@ -264,6 +220,83 @@ function collectOperations(spec) {
264
220
  });
265
221
  }
266
222
 
223
+ function formatGroupList(spec) {
224
+ const groups = collectOperations(spec);
225
+ const width = Math.max('GROUP'.length, ...groups.map(([name]) => name.length));
226
+ return [
227
+ `${'GROUP'.padEnd(width)} OPERATIONS`,
228
+ `${'-'.repeat(width)} ----------`,
229
+ ...groups.map(([name, operations]) => `${name.padEnd(width)} ${operations.length}`),
230
+ '',
231
+ ].join('\n');
232
+ }
233
+
234
+ function groupList(spec) {
235
+ return collectOperations(spec).map(([name, operations]) => ({
236
+ name,
237
+ operations: operations.length,
238
+ }));
239
+ }
240
+
241
+ function levenshteinDistance(left, right) {
242
+ const a = left.toLocaleLowerCase();
243
+ const b = right.toLocaleLowerCase();
244
+ const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
245
+ for (let row = 1; row <= a.length; row += 1) {
246
+ const current = [row];
247
+ for (let column = 1; column <= b.length; column += 1) {
248
+ current[column] = Math.min(
249
+ current[column - 1] + 1,
250
+ previous[column] + 1,
251
+ previous[column - 1] + (a[row - 1] === b[column - 1] ? 0 : 1),
252
+ );
253
+ }
254
+ previous.splice(0, previous.length, ...current);
255
+ }
256
+ return previous[b.length];
257
+ }
258
+
259
+ function closestGroup(name, groups) {
260
+ if (!groups.length) return undefined;
261
+ const candidates = groups
262
+ .map(([candidate]) => ({ candidate, distance: levenshteinDistance(name, candidate) }))
263
+ .sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate));
264
+ const best = candidates[0];
265
+ const threshold = Math.max(2, Math.floor(Math.max(name.length, best.candidate.length) / 3));
266
+ return best.distance <= threshold ? best.candidate : undefined;
267
+ }
268
+
269
+ function referencedSchemaNames(groups, spec) {
270
+ const names = new Set();
271
+ const visitedRefs = new Set();
272
+ const visitedObjects = new Set();
273
+
274
+ function visit(value) {
275
+ if (!value || typeof value !== 'object' || visitedObjects.has(value)) return;
276
+ visitedObjects.add(value);
277
+ if (typeof value.$ref === 'string' && value.$ref.startsWith('#/')) {
278
+ const schemaMatch = value.$ref.match(/^#\/(?:components\/schemas|definitions)\/(.+)$/);
279
+ if (schemaMatch) {
280
+ const name = decodeURIComponent(schemaMatch[1].replace(/~1/g, '/').replace(/~0/g, '~'));
281
+ names.add(name);
282
+ }
283
+ if (!visitedRefs.has(value.$ref)) {
284
+ visitedRefs.add(value.$ref);
285
+ visit(resolveLocalRef(spec, value.$ref));
286
+ }
287
+ }
288
+ for (const nested of Object.values(value)) visit(nested);
289
+ }
290
+
291
+ for (const [, operations] of groups) {
292
+ for (const { operation, pathItem } of operations) {
293
+ visit(operation);
294
+ visit(pathItem.parameters);
295
+ }
296
+ }
297
+ return names;
298
+ }
299
+
267
300
  function renderParameters(parameters, spec) {
268
301
  if (!parameters.length) return '';
269
302
  const lines = ['#### Parameters', '', '| Name | In | Required | Type | Description / constraints |', '|---|---|:---:|---|---|'];
@@ -418,27 +451,40 @@ function renderSchema(name, schema, spec, options) {
418
451
  return lines.join('\n');
419
452
  }
420
453
 
421
- function renderSchemas(spec, options) {
454
+ function renderSchemas(spec, options, includedNames) {
422
455
  const schemas = spec.components?.schemas || spec.definitions || {};
423
- if (!options.schemaCatalog || !Object.keys(schemas).length) return '';
424
- const lines = ['## Schema catalog', '', `${Object.keys(schemas).length} reusable schema(s).`, ''];
425
- for (const [name, schema] of Object.entries(schemas).sort(([a], [b]) => a.localeCompare(b))) {
456
+ const entries = Object.entries(schemas)
457
+ .filter(([name]) => !includedNames || includedNames.has(name))
458
+ .sort(([a], [b]) => a.localeCompare(b));
459
+ if (!options.schemaCatalog || !entries.length) return '';
460
+ const lines = ['## Schema catalog', '', `${entries.length} reusable schema(s).`, ''];
461
+ for (const [name, schema] of entries) {
426
462
  lines.push(renderSchema(name, schema, spec, options));
427
463
  }
428
464
  return lines.join('\n');
429
465
  }
430
466
 
431
467
  function generateMarkdown(spec, options = {}) {
432
- options = { schemaCatalog: true, examples: true, includeExtensions: false, ...options };
468
+ options = { schemaCatalog: true, examples: true, includeExtensions: false, group: undefined, ...options };
433
469
  if (!spec || typeof spec !== 'object') throw new Error('The OpenAPI document must be a JSON object');
434
470
  if (!spec.openapi && !spec.swagger) throw new Error('Missing required "openapi" or "swagger" version field');
435
471
  if (!spec.paths || typeof spec.paths !== 'object') throw new Error('Missing required "paths" object');
436
472
 
437
473
  const title = spec.info?.title || 'API Reference';
438
- const groups = collectOperations(spec);
474
+ const allGroups = collectOperations(spec);
475
+ let groups = allGroups;
476
+ if (options.group !== undefined) {
477
+ groups = allGroups.filter(([name]) => name === options.group);
478
+ if (!groups.length) {
479
+ const available = allGroups.map(([name]) => name).join(', ') || '(none)';
480
+ const suggestion = closestGroup(options.group, allGroups);
481
+ const hint = suggestion ? ` Did you mean ${JSON.stringify(suggestion)}?` : '';
482
+ throw new Error(`Unknown group ${JSON.stringify(options.group)}.${hint} Available groups: ${available}`);
483
+ }
484
+ }
439
485
  const operationCount = groups.reduce((count, [, operations]) => count + operations.length, 0);
440
486
  const lines = [
441
- `# ${title} API Reference`,
487
+ `# ${title}${options.group !== undefined ? ` — ${options.group}` : ''} API Reference`,
442
488
  '',
443
489
  '> Generated from the OpenAPI document. Optimized for human readers and AI agents: use operation IDs for tool names, honor required parameters, and validate request/response bodies against the linked schemas.',
444
490
  '',
@@ -477,40 +523,18 @@ function generateMarkdown(spec, options = {}) {
477
523
  for (const item of operations) lines.push(renderOperation(item, spec, options));
478
524
  }
479
525
 
480
- const schemas = renderSchemas(spec, options);
526
+ const includedSchemas = options.group !== undefined ? referencedSchemaNames(groups, spec) : undefined;
527
+ const schemas = renderSchemas(spec, options, includedSchemas);
481
528
  if (schemas) lines.push(schemas);
482
529
  lines.push('---', '', '_Generated by openapi-agent-reference. Do not edit manually; regenerate after the OpenAPI document changes._', '');
483
530
  return lines.join('\n').replace(/\n{4,}/g, '\n\n\n');
484
531
  }
485
532
 
486
- function main(argv = process.argv.slice(2)) {
487
- let options;
488
- try { options = parseArgs(argv); } catch (error) {
489
- console.error(`Error: ${error.message}\n\n${usage()}`);
490
- process.exitCode = 1;
491
- return;
492
- }
493
- if (options.help) {
494
- process.stdout.write(usage());
495
- return;
496
- }
497
- try {
498
- const inputPath = path.resolve(options.input);
499
- const outputPath = path.resolve(options.output);
500
- const source = fs.readFileSync(inputPath, 'utf8').replace(/^\uFEFF/, '');
501
- let spec;
502
- try { spec = JSON.parse(source); } catch (error) { throw new Error(`Invalid JSON in ${inputPath}: ${error.message}`); }
503
- const markdown = generateMarkdown(spec, options);
504
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
505
- fs.writeFileSync(outputPath, markdown, 'utf8');
506
- const operations = collectOperations(spec).reduce((count, [, items]) => count + items.length, 0);
507
- console.log(`Generated ${outputPath} (${operations} operations, ${Buffer.byteLength(markdown)} bytes)`);
508
- } catch (error) {
509
- console.error(`Error: ${error.message}`);
510
- process.exitCode = 1;
511
- }
512
- }
513
-
514
- if (require.main === module) main();
515
-
516
- module.exports = { generateMarkdown, parseArgs, sampleForSchema, collectOperations };
533
+ module.exports = {
534
+ collectOperations,
535
+ formatGroupList,
536
+ generateMarkdown,
537
+ groupList,
538
+ referencedSchemaNames,
539
+ sampleForSchema,
540
+ };