openapi-agent-reference 1.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/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # OpenAPI Agent Reference Generator
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.
4
+
5
+ It includes:
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.
16
+
17
+ ## Requirements
18
+
19
+ Node.js 18 or newer. No package installation is required.
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ node src/openapi-to-markdown.js openapi.json -o API_REFERENCE.md
25
+ ```
26
+
27
+ Or use the included npm script for the default paths:
28
+
29
+ ```bash
30
+ npm run generate
31
+ ```
32
+
33
+ Options:
34
+
35
+ ```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
41
+ ```
42
+
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.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "openapi-agent-reference",
3
+ "version": "1.0.0",
4
+ "description": "Generate agent-friendly Markdown API reference documentation from OpenAPI JSON.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "openapi-to-md": "src/openapi-to-markdown.js"
8
+ },
9
+ "scripts": {
10
+ "generate": "node src/openapi-to-markdown.js openapi.json -o API_REFERENCE.md",
11
+ "test": "node --test"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/Dikong1/extractOpenApi.git"
20
+ },
21
+ "homepage": "https://github.com/Dikong1/extractOpenApi#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/Dikong1/extractOpenApi/issues"
24
+ },
25
+ "files": [
26
+ "src/",
27
+ "README.md"
28
+ ]
29
+ }
@@ -0,0 +1,516 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
7
+
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
+ function escapeTable(value) {
48
+ return String(value ?? '—').replace(/\r?\n/g, '<br>').replace(/\|/g, '\\|');
49
+ }
50
+
51
+ function inlineCode(value) {
52
+ if (value === undefined || value === null || value === '') return '—';
53
+ const string = String(value);
54
+ const fence = string.includes('`') ? '``' : '`';
55
+ return `${fence}${string}${fence}`;
56
+ }
57
+
58
+ function anchor(value) {
59
+ return String(value)
60
+ .toLowerCase()
61
+ .trim()
62
+ .replace(/[^\p{L}\p{N}\s-]/gu, '')
63
+ .replace(/\s+/g, '-')
64
+ .replace(/-+/g, '-');
65
+ }
66
+
67
+ function localRefName(ref) {
68
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
69
+ const parts = ref.split('/');
70
+ return decodeURIComponent(parts[parts.length - 1].replace(/~1/g, '/').replace(/~0/g, '~'));
71
+ }
72
+
73
+ function resolveLocalRef(spec, ref) {
74
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) return undefined;
75
+ return ref.slice(2).split('/').reduce((current, part) => {
76
+ const key = decodeURIComponent(part.replace(/~1/g, '/').replace(/~0/g, '~'));
77
+ return current && current[key];
78
+ }, spec);
79
+ }
80
+
81
+ function schemaLink(ref) {
82
+ const name = localRefName(ref);
83
+ return name ? `[${inlineCode(name)}](#schema-${anchor(name)})` : inlineCode(ref);
84
+ }
85
+
86
+ function schemaType(schema) {
87
+ if (!schema) return 'any';
88
+ if (schema.$ref) return localRefName(schema.$ref) || schema.$ref;
89
+ if (schema.const !== undefined) return typeof schema.const;
90
+ if (schema.type) {
91
+ const type = Array.isArray(schema.type) ? schema.type.join(' or ') : schema.type;
92
+ if (type === 'array') return `array<${schemaType(schema.items)}>`;
93
+ return schema.format ? `${type} (${schema.format})` : type;
94
+ }
95
+ if (schema.oneOf) return schema.oneOf.map(schemaType).join(' or ');
96
+ if (schema.anyOf) return schema.anyOf.map(schemaType).join(' or ');
97
+ if (schema.allOf) return schema.allOf.map(schemaType).join(' & ');
98
+ if (schema.properties || schema.additionalProperties) return 'object';
99
+ if (schema.enum) return typeof schema.enum[0];
100
+ return 'any';
101
+ }
102
+
103
+ function schemaSummary(schema) {
104
+ if (!schema) return 'any';
105
+ if (schema.$ref) return schemaLink(schema.$ref);
106
+ let summary = inlineCode(schemaType(schema));
107
+ if (schema.enum) summary += ` enum: ${schema.enum.map(inlineCode).join(', ')}`;
108
+ if (schema.default !== undefined) summary += `; default: ${inlineCode(JSON.stringify(schema.default))}`;
109
+ if (schema.minimum !== undefined) summary += `; min: ${inlineCode(schema.minimum)}`;
110
+ if (schema.maximum !== undefined) summary += `; max: ${inlineCode(schema.maximum)}`;
111
+ if (schema.minLength !== undefined) summary += `; min length: ${inlineCode(schema.minLength)}`;
112
+ if (schema.maxLength !== undefined) summary += `; max length: ${inlineCode(schema.maxLength)}`;
113
+ if (schema.pattern) summary += `; pattern: ${inlineCode(schema.pattern)}`;
114
+ return summary;
115
+ }
116
+
117
+ function firstExample(container) {
118
+ if (!container) return undefined;
119
+ if (container.example !== undefined) return container.example;
120
+ if (container.examples) {
121
+ const example = Object.values(container.examples)[0];
122
+ if (example && typeof example === 'object' && 'value' in example) return example.value;
123
+ }
124
+ return undefined;
125
+ }
126
+
127
+ function sampleForSchema(schema, spec, state = { depth: 0, refs: new Set() }) {
128
+ if (!schema || state.depth > 6) return null;
129
+ if (schema.example !== undefined) return schema.example;
130
+ if (schema.examples?.length) return schema.examples[0];
131
+ if (schema.default !== undefined) return schema.default;
132
+ if (schema.const !== undefined) return schema.const;
133
+ if (schema.enum?.length) return schema.enum[0];
134
+ if (schema.$ref) {
135
+ if (state.refs.has(schema.$ref)) return `<recursive:${localRefName(schema.$ref) || schema.$ref}>`;
136
+ const resolved = resolveLocalRef(spec, schema.$ref);
137
+ if (!resolved) return `<${localRefName(schema.$ref) || schema.$ref}>`;
138
+ const refs = new Set(state.refs);
139
+ refs.add(schema.$ref);
140
+ return sampleForSchema(resolved, spec, { depth: state.depth + 1, refs });
141
+ }
142
+ if (schema.allOf) {
143
+ return schema.allOf.reduce((result, part) => {
144
+ const value = sampleForSchema(part, spec, { ...state, depth: state.depth + 1 });
145
+ return value && typeof value === 'object' && !Array.isArray(value) ? { ...result, ...value } : result;
146
+ }, {});
147
+ }
148
+ if (schema.oneOf?.length) return sampleForSchema(schema.oneOf[0], spec, { ...state, depth: state.depth + 1 });
149
+ if (schema.anyOf?.length) {
150
+ const preferred = schema.anyOf.find((item) => item.type !== 'null') || schema.anyOf[0];
151
+ return sampleForSchema(preferred, spec, { ...state, depth: state.depth + 1 });
152
+ }
153
+ const type = Array.isArray(schema.type) ? schema.type.find((item) => item !== 'null') : schema.type;
154
+ if (type === 'object' || schema.properties || schema.additionalProperties) {
155
+ const result = {};
156
+ for (const [name, property] of Object.entries(schema.properties || {})) {
157
+ if (property.readOnly) continue;
158
+ result[name] = sampleForSchema(property, spec, { ...state, depth: state.depth + 1 });
159
+ }
160
+ if (!Object.keys(result).length && schema.additionalProperties && typeof schema.additionalProperties === 'object') {
161
+ result.key = sampleForSchema(schema.additionalProperties, spec, { ...state, depth: state.depth + 1 });
162
+ }
163
+ return result;
164
+ }
165
+ if (type === 'array') return [sampleForSchema(schema.items || {}, spec, { ...state, depth: state.depth + 1 })];
166
+ if (type === 'integer' || type === 'number') return schema.minimum ?? (schema.format === 'float' ? 1.5 : 1);
167
+ if (type === 'boolean') return true;
168
+ if (type === 'null') return null;
169
+ if (schema.format === 'date-time') return '2026-01-15T12:00:00Z';
170
+ if (schema.format === 'date') return '2026-01-15';
171
+ if (schema.format === 'time') return '12:00:00Z';
172
+ if (schema.format === 'email') return 'user@example.com';
173
+ if (schema.format === 'uuid') return '123e4567-e89b-12d3-a456-426614174000';
174
+ if (schema.format === 'uri' || schema.format === 'url') return 'https://example.com';
175
+ if (schema.format === 'binary') return '<binary>';
176
+ return 'string';
177
+ }
178
+
179
+ function jsonBlock(value) {
180
+ let rendered;
181
+ try { rendered = JSON.stringify(value, null, 2); } catch { rendered = 'null'; }
182
+ return `\n\`\`\`json\n${rendered}\n\`\`\`\n`;
183
+ }
184
+
185
+ function mergeParameters(pathParameters = [], operationParameters = []) {
186
+ const merged = new Map();
187
+ for (const parameter of [...pathParameters, ...operationParameters]) {
188
+ const key = parameter.$ref || `${parameter.in}:${parameter.name}`;
189
+ merged.set(key, parameter);
190
+ }
191
+ return [...merged.values()];
192
+ }
193
+
194
+ function resolvedObject(spec, object) {
195
+ return object?.$ref ? { ...resolveLocalRef(spec, object.$ref), ...object, $ref: object.$ref } : object;
196
+ }
197
+
198
+ function securityText(requirements, spec) {
199
+ if (requirements === undefined) requirements = spec.security;
200
+ if (requirements === undefined) return 'Not specified';
201
+ if (requirements.length === 0) return 'None';
202
+ return requirements.map((requirement) => {
203
+ const entries = Object.entries(requirement);
204
+ if (!entries.length) return 'Anonymous';
205
+ return entries.map(([name, scopes]) => scopes?.length ? `${name} (${scopes.join(', ')})` : name).join(' AND ');
206
+ }).join(' OR ');
207
+ }
208
+
209
+ function renderSecurity(spec) {
210
+ const schemes = spec.components?.securitySchemes || spec.securityDefinitions || {};
211
+ if (!Object.keys(schemes).length && !spec.security) return '';
212
+ const lines = ['## Authentication', ''];
213
+ if (spec.security !== undefined) lines.push(`Global requirement: **${securityText(spec.security, spec)}**`, '');
214
+ for (const [name, scheme] of Object.entries(schemes)) {
215
+ lines.push(`### ${name}`, '', `- Type: ${inlineCode(scheme.type)}`);
216
+ if (scheme.description) lines.push(`- Description: ${scheme.description}`);
217
+ if (scheme.in) lines.push(`- Send in: ${inlineCode(scheme.in)}`);
218
+ if (scheme.name) lines.push(`- Parameter name: ${inlineCode(scheme.name)}`);
219
+ if (scheme.scheme) lines.push(`- HTTP scheme: ${inlineCode(scheme.scheme)}`);
220
+ if (scheme.bearerFormat) lines.push(`- Bearer format: ${inlineCode(scheme.bearerFormat)}`);
221
+ if (scheme.openIdConnectUrl) lines.push(`- OpenID Connect URL: ${scheme.openIdConnectUrl}`);
222
+ if (scheme.flow) {
223
+ lines.push(`- OAuth flow: ${inlineCode(scheme.flow)}`);
224
+ if (scheme.authorizationUrl) lines.push(`- Authorization URL: ${scheme.authorizationUrl}`);
225
+ if (scheme.tokenUrl) lines.push(`- Token URL: ${scheme.tokenUrl}`);
226
+ }
227
+ for (const [flowName, flow] of Object.entries(scheme.flows || {})) {
228
+ lines.push(`- OAuth flow ${inlineCode(flowName)}: token ${flow.tokenUrl || '—'}${flow.authorizationUrl ? `; authorize ${flow.authorizationUrl}` : ''}`);
229
+ const scopes = Object.entries(flow.scopes || {});
230
+ if (scopes.length) lines.push(` - Scopes: ${scopes.map(([scope, description]) => `${inlineCode(scope)} — ${description}`).join('; ')}`);
231
+ }
232
+ lines.push('');
233
+ }
234
+ return lines.join('\n');
235
+ }
236
+
237
+ function serverUrls(spec) {
238
+ if (spec.servers?.length) return spec.servers.map((server) => ({ url: server.url, description: server.description }));
239
+ if (spec.swagger) {
240
+ const schemes = spec.schemes?.length ? spec.schemes : ['https'];
241
+ return schemes.map((scheme) => ({ url: `${scheme}://${spec.host || '<host>'}${spec.basePath || ''}` }));
242
+ }
243
+ return [];
244
+ }
245
+
246
+ function collectOperations(spec) {
247
+ const groups = new Map();
248
+ for (const [route, rawPathItem] of Object.entries(spec.paths || {})) {
249
+ const pathItem = resolvedObject(spec, rawPathItem) || rawPathItem;
250
+ for (const [method, rawOperation] of Object.entries(pathItem || {})) {
251
+ if (!HTTP_METHODS.has(method.toLowerCase())) continue;
252
+ const operation = resolvedObject(spec, rawOperation);
253
+ const group = operation.tags?.[0] || 'Untagged';
254
+ if (!groups.has(group)) groups.set(group, []);
255
+ groups.get(group).push({ route, method: method.toUpperCase(), operation, pathItem });
256
+ }
257
+ }
258
+ const declaredOrder = (spec.tags || []).map((tag) => tag.name);
259
+ return [...groups.entries()].sort(([a], [b]) => {
260
+ const ai = declaredOrder.indexOf(a);
261
+ const bi = declaredOrder.indexOf(b);
262
+ if (ai >= 0 || bi >= 0) return (ai < 0 ? Infinity : ai) - (bi < 0 ? Infinity : bi);
263
+ return a.localeCompare(b);
264
+ });
265
+ }
266
+
267
+ function renderParameters(parameters, spec) {
268
+ if (!parameters.length) return '';
269
+ const lines = ['#### Parameters', '', '| Name | In | Required | Type | Description / constraints |', '|---|---|:---:|---|---|'];
270
+ for (const rawParameter of parameters) {
271
+ const parameter = resolvedObject(spec, rawParameter) || rawParameter;
272
+ const schema = parameter.schema || (parameter.type ? parameter : {});
273
+ const details = [parameter.description, parameter.deprecated ? '**Deprecated.**' : '', parameter.allowEmptyValue ? 'Empty value allowed.' : ''].filter(Boolean).join(' ');
274
+ lines.push(`| ${inlineCode(parameter.name || localRefName(rawParameter.$ref))} | ${inlineCode(parameter.in)} | ${parameter.required ? 'yes' : 'no'} | ${escapeTable(schemaSummary(schema))} | ${escapeTable(details || '—')} |`);
275
+ }
276
+ lines.push('');
277
+ return lines.join('\n');
278
+ }
279
+
280
+ function renderContent(content, spec, options, headingLevel = 5) {
281
+ const lines = [];
282
+ for (const [mediaType, media] of Object.entries(content || {})) {
283
+ lines.push(`${'#'.repeat(headingLevel)} ${mediaType}`, '');
284
+ if (media.schema) lines.push(`Schema: ${schemaSummary(media.schema)}`, '');
285
+ const example = firstExample(media);
286
+ if (options.examples && (example !== undefined || media.schema)) {
287
+ lines.push('Example:', jsonBlock(example !== undefined ? example : sampleForSchema(media.schema, spec)));
288
+ }
289
+ if (media.encoding && Object.keys(media.encoding).length) {
290
+ lines.push('Encoding:', '', '| Property | Content type | Style | Explode |', '|---|---|---|---|');
291
+ for (const [property, encoding] of Object.entries(media.encoding)) {
292
+ lines.push(`| ${inlineCode(property)} | ${inlineCode(encoding.contentType)} | ${inlineCode(encoding.style)} | ${encoding.explode ?? '—'} |`);
293
+ }
294
+ lines.push('');
295
+ }
296
+ }
297
+ return lines.join('\n');
298
+ }
299
+
300
+ function swaggerRequestContent(parameters) {
301
+ const body = parameters.find((parameter) => parameter.in === 'body');
302
+ if (body) return { 'application/json': { schema: body.schema, example: body.example } };
303
+ const form = parameters.filter((parameter) => parameter.in === 'formData');
304
+ if (form.length) {
305
+ return { 'application/x-www-form-urlencoded': { schema: { type: 'object', properties: Object.fromEntries(form.map((p) => [p.name, p])), required: form.filter((p) => p.required).map((p) => p.name) } } };
306
+ }
307
+ return {};
308
+ }
309
+
310
+ function renderRequest(operation, parameters, spec, options) {
311
+ const rawBody = operation.requestBody ? resolvedObject(spec, operation.requestBody) : null;
312
+ const content = rawBody?.content || swaggerRequestContent(parameters);
313
+ if (!Object.keys(content).length) return '';
314
+ const lines = ['#### Request body', ''];
315
+ if (rawBody?.description) lines.push(rawBody.description, '');
316
+ if (rawBody) lines.push(`Required: **${rawBody.required ? 'yes' : 'no'}**`, '');
317
+ lines.push(renderContent(content, spec, options));
318
+ return lines.join('\n');
319
+ }
320
+
321
+ function renderHeaders(headers, spec) {
322
+ if (!headers || !Object.keys(headers).length) return '';
323
+ const lines = ['Headers:', '', '| Name | Type | Description |', '|---|---|---|'];
324
+ for (const [name, rawHeader] of Object.entries(headers)) {
325
+ const header = resolvedObject(spec, rawHeader) || rawHeader;
326
+ lines.push(`| ${inlineCode(name)} | ${escapeTable(schemaSummary(header.schema || header))} | ${escapeTable(header.description || '—')} |`);
327
+ }
328
+ lines.push('');
329
+ return lines.join('\n');
330
+ }
331
+
332
+ function renderResponses(operation, spec, options) {
333
+ const lines = ['#### Responses', ''];
334
+ const responses = operation.responses || {};
335
+ if (!Object.keys(responses).length) return [...lines, '_No responses documented._', ''].join('\n');
336
+ for (const [status, rawResponse] of Object.entries(responses)) {
337
+ const response = resolvedObject(spec, rawResponse) || rawResponse;
338
+ lines.push(`##### ${status} — ${response.description || 'Response'}`, '');
339
+ const headers = renderHeaders(response.headers, spec);
340
+ if (headers) lines.push(headers);
341
+ let content = response.content;
342
+ if (!content && response.schema) content = { 'application/json': { schema: response.schema, examples: response.examples } };
343
+ if (content) lines.push(renderContent(content, spec, options, 6));
344
+ if (response.links && Object.keys(response.links).length) {
345
+ lines.push('Links:', '');
346
+ for (const [name, link] of Object.entries(response.links)) lines.push(`- **${name}:** ${link.operationId || link.operationRef || ''} ${link.description || ''}`.trim());
347
+ lines.push('');
348
+ }
349
+ }
350
+ return lines.join('\n');
351
+ }
352
+
353
+ function renderExtensions(operation) {
354
+ const extensions = Object.entries(operation).filter(([key]) => key.startsWith('x-'));
355
+ if (!extensions.length) return '';
356
+ return ['#### Extensions', '', ...extensions.map(([key, value]) => `- ${inlineCode(key)}: ${inlineCode(JSON.stringify(value))}`), ''].join('\n');
357
+ }
358
+
359
+ function renderOperation(item, spec, options) {
360
+ const { route, method, operation, pathItem } = item;
361
+ const title = operation.summary || operation.operationId || `${method} ${route}`;
362
+ const parameters = mergeParameters(pathItem.parameters, operation.parameters).map((parameter) => resolvedObject(spec, parameter) || parameter);
363
+ const nonBodyParameters = parameters.filter((parameter) => !['body', 'formData'].includes(parameter.in));
364
+ const lines = [
365
+ `### ${method} ${route}`,
366
+ '',
367
+ `**${title}**`,
368
+ '',
369
+ ];
370
+ if (operation.description && operation.description !== operation.summary) lines.push(operation.description, '');
371
+ const metadata = [];
372
+ if (operation.operationId) metadata.push(`- Operation ID: ${inlineCode(operation.operationId)}`);
373
+ metadata.push(`- Authentication: **${securityText(operation.security, spec)}**`);
374
+ if (operation.deprecated) metadata.push('- Status: **Deprecated**');
375
+ if (operation.externalDocs?.url) metadata.push(`- External docs: [${operation.externalDocs.description || operation.externalDocs.url}](${operation.externalDocs.url})`);
376
+ if (operation.tags?.length > 1) metadata.push(`- Additional tags: ${operation.tags.slice(1).map(inlineCode).join(', ')}`);
377
+ lines.push(...metadata, '');
378
+ const parameterSection = renderParameters(nonBodyParameters, spec);
379
+ if (parameterSection) lines.push(parameterSection);
380
+ const requestSection = renderRequest(operation, parameters, spec, options);
381
+ if (requestSection) lines.push(requestSection);
382
+ lines.push(renderResponses(operation, spec, options));
383
+ if (options.includeExtensions) {
384
+ const extensions = renderExtensions(operation);
385
+ if (extensions) lines.push(extensions);
386
+ }
387
+ return lines.join('\n');
388
+ }
389
+
390
+ function flattenSchemaProperties(schema, spec) {
391
+ const properties = { ...(schema.properties || {}) };
392
+ const required = new Set(schema.required || []);
393
+ for (const part of schema.allOf || []) {
394
+ const resolved = part.$ref ? resolveLocalRef(spec, part.$ref) : part;
395
+ if (!resolved) continue;
396
+ Object.assign(properties, resolved.properties || {});
397
+ for (const name of resolved.required || []) required.add(name);
398
+ }
399
+ return { properties, required };
400
+ }
401
+
402
+ function renderSchema(name, schema, spec, options) {
403
+ const lines = [`<a id="schema-${anchor(name)}"></a>`, '', `### ${name}`, ''];
404
+ if (schema.description) lines.push(schema.description, '');
405
+ lines.push(`Type: ${schemaSummary(schema)}`, '');
406
+ if (schema.deprecated) lines.push('**Deprecated.**', '');
407
+ const { properties, required } = flattenSchemaProperties(schema, spec);
408
+ if (Object.keys(properties).length) {
409
+ lines.push('| Property | Required | Type | Description / constraints |', '|---|:---:|---|---|');
410
+ for (const [propertyName, property] of Object.entries(properties)) {
411
+ const details = [property.description, property.readOnly ? 'Read-only.' : '', property.writeOnly ? 'Write-only.' : '', property.deprecated ? 'Deprecated.' : ''].filter(Boolean).join(' ');
412
+ lines.push(`| ${inlineCode(propertyName)} | ${required.has(propertyName) ? 'yes' : 'no'} | ${escapeTable(schemaSummary(property))} | ${escapeTable(details || '—')} |`);
413
+ }
414
+ lines.push('');
415
+ }
416
+ if (schema.discriminator) lines.push(`Discriminator: ${inlineCode(schema.discriminator.propertyName)}`, '');
417
+ if (options.examples) lines.push('Example:', jsonBlock(sampleForSchema(schema, spec)));
418
+ return lines.join('\n');
419
+ }
420
+
421
+ function renderSchemas(spec, options) {
422
+ 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))) {
426
+ lines.push(renderSchema(name, schema, spec, options));
427
+ }
428
+ return lines.join('\n');
429
+ }
430
+
431
+ function generateMarkdown(spec, options = {}) {
432
+ options = { schemaCatalog: true, examples: true, includeExtensions: false, ...options };
433
+ if (!spec || typeof spec !== 'object') throw new Error('The OpenAPI document must be a JSON object');
434
+ if (!spec.openapi && !spec.swagger) throw new Error('Missing required "openapi" or "swagger" version field');
435
+ if (!spec.paths || typeof spec.paths !== 'object') throw new Error('Missing required "paths" object');
436
+
437
+ const title = spec.info?.title || 'API Reference';
438
+ const groups = collectOperations(spec);
439
+ const operationCount = groups.reduce((count, [, operations]) => count + operations.length, 0);
440
+ const lines = [
441
+ `# ${title} API Reference`,
442
+ '',
443
+ '> 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
+ '',
445
+ `- API version: ${inlineCode(spec.info?.version || 'unspecified')}`,
446
+ `- OpenAPI version: ${inlineCode(spec.openapi || spec.swagger)}`,
447
+ `- Operations: **${operationCount}** across **${groups.length}** group(s)`,
448
+ ];
449
+ if (spec.info?.description) lines.push('', spec.info.description);
450
+ if (spec.info?.termsOfService) lines.push(`- Terms of service: ${spec.info.termsOfService}`);
451
+ if (spec.info?.contact?.url || spec.info?.contact?.email) lines.push(`- Contact: ${spec.info.contact.url || spec.info.contact.email}`);
452
+ if (spec.externalDocs?.url) lines.push(`- External documentation: [${spec.externalDocs.description || spec.externalDocs.url}](${spec.externalDocs.url})`);
453
+
454
+ const servers = serverUrls(spec);
455
+ lines.push('', '## Base URLs', '');
456
+ if (servers.length) {
457
+ for (const server of servers) lines.push(`- ${inlineCode(server.url)}${server.description ? ` — ${server.description}` : ''}`);
458
+ } else lines.push('_No server URL is declared. Supply the deployment base URL at runtime._');
459
+ lines.push('');
460
+
461
+ const security = renderSecurity(spec);
462
+ if (security) lines.push(security);
463
+
464
+ lines.push('## Endpoint index', '');
465
+ for (const [group, operations] of groups) {
466
+ lines.push(`- [${group}](#group-${anchor(group)}) (${operations.length})`);
467
+ for (const item of operations) lines.push(` - [${item.method} ${item.route}](#${anchor(`${item.method} ${item.route}`)})`);
468
+ }
469
+ lines.push('');
470
+
471
+ const declaredTags = new Map((spec.tags || []).map((tag) => [tag.name, tag]));
472
+ for (const [group, operations] of groups) {
473
+ lines.push(`<a id="group-${anchor(group)}"></a>`, '', `## ${group}`, '');
474
+ const tag = declaredTags.get(group);
475
+ if (tag?.description) lines.push(tag.description, '');
476
+ if (tag?.externalDocs?.url) lines.push(`[Group documentation](${tag.externalDocs.url})`, '');
477
+ for (const item of operations) lines.push(renderOperation(item, spec, options));
478
+ }
479
+
480
+ const schemas = renderSchemas(spec, options);
481
+ if (schemas) lines.push(schemas);
482
+ lines.push('---', '', '_Generated by openapi-agent-reference. Do not edit manually; regenerate after the OpenAPI document changes._', '');
483
+ return lines.join('\n').replace(/\n{4,}/g, '\n\n\n');
484
+ }
485
+
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 };