te.js 1.3.1 → 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/.cursor/plans/ai_native_framework_features_5bb1a20a.plan.md +234 -0
- package/.cursor/plans/auto_error_fix_agent_e68979c5.plan.md +356 -0
- package/.cursor/plans/tejas_framework_test_suite_5e3c6fad.plan.md +168 -0
- package/.prettierignore +31 -0
- package/README.md +156 -14
- package/auto-docs/analysis/handler-analyzer.js +58 -0
- package/auto-docs/analysis/source-resolver.js +101 -0
- package/auto-docs/constants.js +37 -0
- package/auto-docs/index.js +146 -0
- package/auto-docs/llm/index.js +6 -0
- package/auto-docs/llm/parse.js +88 -0
- package/auto-docs/llm/prompts.js +222 -0
- package/auto-docs/llm/provider.js +187 -0
- package/auto-docs/openapi/endpoint-processor.js +277 -0
- package/auto-docs/openapi/generator.js +107 -0
- package/auto-docs/openapi/level3.js +131 -0
- package/auto-docs/openapi/spec-builders.js +244 -0
- package/auto-docs/ui/docs-ui.js +186 -0
- package/auto-docs/utils/logger.js +17 -0
- package/auto-docs/utils/strip-usage.js +10 -0
- package/cli/docs-command.js +315 -0
- package/cli/fly-command.js +71 -0
- package/cli/index.js +57 -0
- package/database/index.js +163 -5
- package/database/mongodb.js +146 -0
- package/database/redis.js +201 -0
- package/docs/README.md +36 -0
- package/docs/ammo.md +362 -0
- package/docs/api-reference.md +489 -0
- package/docs/auto-docs.md +215 -0
- package/docs/cli.md +152 -0
- package/docs/configuration.md +233 -0
- package/docs/database.md +391 -0
- package/docs/error-handling.md +417 -0
- package/docs/file-uploads.md +334 -0
- package/docs/getting-started.md +181 -0
- package/docs/middleware.md +356 -0
- package/docs/rate-limiting.md +394 -0
- package/docs/routing.md +302 -0
- package/example/API_OVERVIEW.md +77 -0
- package/example/README.md +155 -0
- package/example/index.js +27 -2
- package/example/openapi.json +390 -0
- package/example/package.json +5 -2
- package/example/services/cache.service.js +25 -0
- package/example/services/user.service.js +42 -0
- package/example/start-redis.js +2 -0
- package/example/targets/cache.target.js +35 -0
- package/example/targets/index.target.js +11 -2
- package/example/targets/users.target.js +60 -0
- package/example/tejas.config.json +13 -1
- package/package.json +20 -5
- package/rate-limit/algorithms/fixed-window.js +141 -0
- package/rate-limit/algorithms/sliding-window.js +147 -0
- package/rate-limit/algorithms/token-bucket.js +115 -0
- package/rate-limit/base.js +165 -0
- package/rate-limit/index.js +147 -0
- package/rate-limit/storage/base.js +104 -0
- package/rate-limit/storage/memory.js +102 -0
- package/rate-limit/storage/redis.js +88 -0
- package/server/ammo/body-parser.js +152 -25
- package/server/ammo/enhancer.js +6 -2
- package/server/ammo.js +356 -327
- package/server/endpoint.js +21 -0
- package/server/handler.js +113 -87
- package/server/target.js +50 -9
- package/server/targets/registry.js +160 -57
- package/te.js +363 -137
- package/tests/auto-docs/handler-analyzer.test.js +44 -0
- package/tests/auto-docs/openapi-generator.test.js +103 -0
- package/tests/auto-docs/parse.test.js +63 -0
- package/tests/auto-docs/source-resolver.test.js +58 -0
- package/tests/helpers/index.js +37 -0
- package/tests/helpers/mock-http.js +342 -0
- package/tests/helpers/test-utils.js +446 -0
- package/tests/setup.test.js +148 -0
- package/utils/configuration.js +13 -10
- package/vitest.config.js +54 -0
- package/database/mongo.js +0 -67
- package/example/targets/user/user.target.js +0 -17
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-docs orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Module layout:
|
|
5
|
+
* - index.js (this file) — generateDocs(registry, options): orchestration, config, spec write, level-3 dispatch
|
|
6
|
+
* - openapi/generator.js — build OpenAPI spec from registry (levels 1–2: handler analysis, LLM enhancement)
|
|
7
|
+
* - openapi/level3.js — level-3 pipeline: reorder tags by importance, generate and write overview page
|
|
8
|
+
* - analysis/handler-analyzer.js — detect HTTP methods from handler source
|
|
9
|
+
* - analysis/source-resolver.js — resolve target file and dependencies (for level 2 context)
|
|
10
|
+
* - llm/ — LLM provider (enhanceEndpointDocs, summarizeTargetGroup, reorderTagsByImportance, generateOverviewPage)
|
|
11
|
+
* - ui/docs-ui.js — build Scalar docs HTML, registerDocRoutes
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { writeFile } from 'node:fs/promises';
|
|
15
|
+
import TejLogger from 'tej-logger';
|
|
16
|
+
import { createProvider } from './llm/index.js';
|
|
17
|
+
import { generateOpenAPISpec } from './openapi/generator.js';
|
|
18
|
+
import { runLevel3 } from './openapi/level3.js';
|
|
19
|
+
import targetRegistry from '../server/targets/registry.js';
|
|
20
|
+
|
|
21
|
+
const logger = new TejLogger('Tejas.AutoDocs');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate llm config, warn if no API key, create and return LLM provider.
|
|
25
|
+
* @param {object} llmConfig - options.llm
|
|
26
|
+
* @returns {object} LLM provider instance
|
|
27
|
+
*/
|
|
28
|
+
function validateAndCreateLlm(llmConfig) {
|
|
29
|
+
if (!llmConfig || typeof llmConfig !== 'object') {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Documentation generation requires an LLM. Provide options.llm with { baseURL?, apiKey?, model? }.',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const hasApiKey = llmConfig.apiKey || process.env.OPENAI_API_KEY;
|
|
35
|
+
if (!hasApiKey) {
|
|
36
|
+
logger.warn(
|
|
37
|
+
'No API key set. Provide options.llm.apiKey or OPENAI_API_KEY. Local providers (e.g. Ollama) may work without a key.',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return createProvider(llmConfig);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Log start summary when verbose (endpoints count, model, title, output file, building message).
|
|
45
|
+
* @param {object} options - { info?, outputPath? }
|
|
46
|
+
* @param {number} targetCount
|
|
47
|
+
* @param {object} log - logger
|
|
48
|
+
* @param {boolean} verbose
|
|
49
|
+
*/
|
|
50
|
+
function logStartSummary(options, targetCount, log, verbose) {
|
|
51
|
+
if (!verbose) return;
|
|
52
|
+
const { info, outputPath } = options;
|
|
53
|
+
log.info('OpenAPI documentation generation started.');
|
|
54
|
+
log.info(` Endpoints in registry: ${targetCount}`);
|
|
55
|
+
log.info(` LLM model: ${options.llm?.model ?? 'default'}`);
|
|
56
|
+
if (info?.title) log.info(` API title: ${info.title}`);
|
|
57
|
+
if (outputPath) log.info(` Output file: ${outputPath}`);
|
|
58
|
+
if (targetCount === 0) {
|
|
59
|
+
log.warn('No endpoints in registry; OpenAPI spec will be minimal.');
|
|
60
|
+
} else {
|
|
61
|
+
log.info('Building OpenAPI spec (analyzing handlers, LLM enhancement)...');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Log result summary when verbose (paths count, tags).
|
|
67
|
+
* @param {object} spec - OpenAPI spec
|
|
68
|
+
* @param {object} log - logger
|
|
69
|
+
* @param {boolean} verbose
|
|
70
|
+
*/
|
|
71
|
+
function logResultSummary(spec, log, verbose) {
|
|
72
|
+
if (!verbose) return;
|
|
73
|
+
const pathCount = spec.paths ? Object.keys(spec.paths).length : 0;
|
|
74
|
+
const tagList = Array.isArray(spec.tags) ? spec.tags.map((t) => t.name).join(', ') : '';
|
|
75
|
+
log.info(` Paths: ${pathCount}`);
|
|
76
|
+
log.info(` Tags (groups): ${spec.tags?.length ?? 0} [ ${tagList || '—'}]`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Write spec JSON to outputPath if path is set. Logs when verbose.
|
|
81
|
+
* @param {object} spec - OpenAPI spec
|
|
82
|
+
* @param {string|undefined} outputPath
|
|
83
|
+
* @param {object} log - logger
|
|
84
|
+
* @param {boolean} verbose
|
|
85
|
+
*/
|
|
86
|
+
async function writeSpecIfNeeded(spec, outputPath, log, verbose) {
|
|
87
|
+
if (!outputPath || typeof outputPath !== 'string') return;
|
|
88
|
+
if (verbose) log.info(`Writing spec to ${outputPath}...`);
|
|
89
|
+
await writeFile(outputPath, JSON.stringify(spec, null, 2), 'utf8');
|
|
90
|
+
if (verbose) log.info(`OpenAPI spec written to ${outputPath}.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate OpenAPI 3.0 spec from the target registry using an LLM.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} [registry] - Target registry with .targets (default: app registry)
|
|
97
|
+
* @param {object} [options] - llm (required), info, servers, outputPath, level, dirTargets, overviewPath, verbose
|
|
98
|
+
* @returns {Promise<object>} OpenAPI 3.0 spec object
|
|
99
|
+
*/
|
|
100
|
+
export async function generateDocs(registry = targetRegistry, options = {}) {
|
|
101
|
+
const {
|
|
102
|
+
llm: llmConfig,
|
|
103
|
+
info,
|
|
104
|
+
servers,
|
|
105
|
+
outputPath,
|
|
106
|
+
level,
|
|
107
|
+
dirTargets,
|
|
108
|
+
overviewPath: overviewPathOption,
|
|
109
|
+
verbose = false,
|
|
110
|
+
} = options;
|
|
111
|
+
const targets = registry?.targets ?? [];
|
|
112
|
+
|
|
113
|
+
const llm = validateAndCreateLlm(llmConfig);
|
|
114
|
+
logStartSummary({ ...options, llm: llmConfig }, targets.length, logger, verbose);
|
|
115
|
+
|
|
116
|
+
const spec = await generateOpenAPISpec(registry, {
|
|
117
|
+
llm,
|
|
118
|
+
info,
|
|
119
|
+
servers,
|
|
120
|
+
level,
|
|
121
|
+
dirTargets,
|
|
122
|
+
verbose,
|
|
123
|
+
logger,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
logResultSummary(spec, logger, verbose);
|
|
127
|
+
await writeSpecIfNeeded(spec, outputPath, logger, verbose);
|
|
128
|
+
|
|
129
|
+
if (level === 3 && llm) {
|
|
130
|
+
await runLevel3(spec, {
|
|
131
|
+
outputPath,
|
|
132
|
+
overviewPath: overviewPathOption,
|
|
133
|
+
info,
|
|
134
|
+
verbose,
|
|
135
|
+
logger,
|
|
136
|
+
}, llm);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (verbose) logger.info('OpenAPI documentation generation completed.');
|
|
140
|
+
return spec;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { generateOpenAPISpec } from './openapi/generator.js';
|
|
144
|
+
export { createProvider } from './llm/index.js';
|
|
145
|
+
export { buildDocsPage } from './ui/docs-ui.js';
|
|
146
|
+
export { analyzeHandler, detectMethods } from './analysis/handler-analyzer.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse JSON from LLM response text (handles markdown code blocks).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract the first JSON object from a string.
|
|
7
|
+
* @param {string} str - Raw LLM response
|
|
8
|
+
* @returns {object|null}
|
|
9
|
+
*/
|
|
10
|
+
export function extractJSON(str) {
|
|
11
|
+
if (!str || typeof str !== 'string') return null;
|
|
12
|
+
const trimmed = str.trim();
|
|
13
|
+
const open = trimmed.indexOf('{');
|
|
14
|
+
if (open === -1) return null;
|
|
15
|
+
let depth = 0;
|
|
16
|
+
let end = -1;
|
|
17
|
+
for (let i = open; i < trimmed.length; i++) {
|
|
18
|
+
if (trimmed[i] === '{') depth++;
|
|
19
|
+
else if (trimmed[i] === '}') {
|
|
20
|
+
depth--;
|
|
21
|
+
if (depth === 0) {
|
|
22
|
+
end = i + 1;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (end === -1) return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(trimmed.slice(open, end));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract the first JSON array from a string.
|
|
37
|
+
* @param {string} str - Raw LLM response
|
|
38
|
+
* @returns {Array|null}
|
|
39
|
+
*/
|
|
40
|
+
export function extractJSONArray(str) {
|
|
41
|
+
if (!str || typeof str !== 'string') return null;
|
|
42
|
+
const trimmed = str.trim();
|
|
43
|
+
const open = trimmed.indexOf('[');
|
|
44
|
+
if (open === -1) return null;
|
|
45
|
+
let depth = 0;
|
|
46
|
+
let end = -1;
|
|
47
|
+
for (let i = open; i < trimmed.length; i++) {
|
|
48
|
+
if (trimmed[i] === '[') depth++;
|
|
49
|
+
else if (trimmed[i] === ']') {
|
|
50
|
+
depth--;
|
|
51
|
+
if (depth === 0) {
|
|
52
|
+
end = i + 1;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (end === -1) return null;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(trimmed.slice(open, end));
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Reconcile LLM-ordered tag names with actual tag objects. Returns tags in desired order;
|
|
67
|
+
* any tag not in orderedTagNames is appended at the end.
|
|
68
|
+
* @param {string[]} orderedTagNames - Tag names in desired order (from LLM)
|
|
69
|
+
* @param {Array<{ name: string, description?: string }>} tags - Current spec.tags
|
|
70
|
+
* @returns {Array<{ name: string, description?: string }>} Tags reordered
|
|
71
|
+
*/
|
|
72
|
+
export function reconcileOrderedTags(orderedTagNames, tags) {
|
|
73
|
+
if (!Array.isArray(tags) || !tags.length) return [];
|
|
74
|
+
if (!Array.isArray(orderedTagNames) || !orderedTagNames.length) return [...tags];
|
|
75
|
+
const byName = new Map(tags.map((t) => [t.name, t]));
|
|
76
|
+
const ordered = [];
|
|
77
|
+
for (const name of orderedTagNames) {
|
|
78
|
+
const tag = byName.get(name);
|
|
79
|
+
if (tag) {
|
|
80
|
+
ordered.push(tag);
|
|
81
|
+
byName.delete(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const [, tag] of byName) {
|
|
85
|
+
ordered.push(tag);
|
|
86
|
+
}
|
|
87
|
+
return ordered;
|
|
88
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt builders for auto-documentation LLM calls.
|
|
3
|
+
* Each function returns the prompt string; provider calls analyze(prompt).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
PROMPT_HANDLER_SLICE,
|
|
8
|
+
PROMPT_HANDLER_SLICE_WITH_DEPS,
|
|
9
|
+
PROMPT_DEPENDENCY_SLICE,
|
|
10
|
+
PROMPT_GROUP_CODE_LIMIT,
|
|
11
|
+
PROMPT_GROUP_CODE_LIMIT_WITH_DEPS,
|
|
12
|
+
PROMPT_GROUP_SNIPPET_CHARS,
|
|
13
|
+
} from '../constants.js';
|
|
14
|
+
|
|
15
|
+
/** Shared rule block for enhance prompts: required vs optional and format. */
|
|
16
|
+
const ENHANCE_RULES = `CRITICAL - Required vs optional: Infer from the code which parameters are required and which are optional. Look for: validation that throws or returns error when missing; checks like !payload.field or !ammo.payload.x; required in schema or JSDoc; or optional/undefined/default handling. For every body property and every query parameter you list, you MUST set "required": true or "required": false explicitly. Do not omit "required".
|
|
17
|
+
|
|
18
|
+
Respond with ONLY a single JSON object (no markdown, no explanation). Use this shape:`;
|
|
19
|
+
|
|
20
|
+
/** JSON shape example for single-endpoint enhance. */
|
|
21
|
+
const ENHANCE_RESPONSE_SHAPE = `{
|
|
22
|
+
"summary": "Short one-line description",
|
|
23
|
+
"description": "Optional longer description",
|
|
24
|
+
"request": {
|
|
25
|
+
"body": {
|
|
26
|
+
"fieldName": { "type": "string", "description": "...", "required": true }
|
|
27
|
+
},
|
|
28
|
+
"query": {
|
|
29
|
+
"paramName": { "type": "string", "description": "...", "required": false }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"response": { "200": { "description": "Success" }, "201": { "description": "Created" } }
|
|
33
|
+
}
|
|
34
|
+
- For every field in request.body and request.query set "required": true or "required": false based on code context.
|
|
35
|
+
- Include "format" when relevant (e.g. "email", "date-time", "binary").
|
|
36
|
+
- Omit "request" or "response" if not applicable. Keep summary under 80 characters.`;
|
|
37
|
+
|
|
38
|
+
/** JSON shape example for per-method enhance. */
|
|
39
|
+
const PER_METHOD_RESPONSE_SHAPE = `{
|
|
40
|
+
"get": { "summary": "...", "description": "...", "response": { "200": { "description": "..." } } },
|
|
41
|
+
"put": { "summary": "...", "description": "...", "request": { "body": { "name": { "type": "string", "required": true }, "email": { "type": "string", "format": "email", "required": true } } }, "response": { "200": { "description": "..." } } },
|
|
42
|
+
"delete": { "summary": "...", "description": "...", "response": { "204": { "description": "..." } } }
|
|
43
|
+
}`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build handler snippet and related-files section for endpoint prompts. Single place for limits and formatting.
|
|
47
|
+
* @param {object} endpointInfo - { handlerSource?, dependencySources? }
|
|
48
|
+
* @returns {{ handlerSnippet: string, relatedSection: string }}
|
|
49
|
+
*/
|
|
50
|
+
export function buildEndpointPromptContext(endpointInfo) {
|
|
51
|
+
const { handlerSource = '', dependencySources = '' } = endpointInfo;
|
|
52
|
+
const handlerLimit = dependencySources ? PROMPT_HANDLER_SLICE_WITH_DEPS : PROMPT_HANDLER_SLICE;
|
|
53
|
+
const handlerSnippet = (handlerSource || '').slice(0, handlerLimit);
|
|
54
|
+
const relatedSection = dependencySources
|
|
55
|
+
? `
|
|
56
|
+
|
|
57
|
+
Related source files (target and dependencies):
|
|
58
|
+
\`\`\`javascript
|
|
59
|
+
${dependencySources.slice(0, PROMPT_DEPENDENCY_SLICE)}
|
|
60
|
+
\`\`\`
|
|
61
|
+
`
|
|
62
|
+
: '';
|
|
63
|
+
return { handlerSnippet, relatedSection };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build prompt for summarizing a target group (tag name + description).
|
|
68
|
+
* @param {string} groupId
|
|
69
|
+
* @param {Array<object>} endpointsInfo - { path, methods, summary?, description?, handlerSource?, dependencySources? }
|
|
70
|
+
* @param {string} [dependencySources]
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function buildSummarizeGroupPrompt(groupId, endpointsInfo, dependencySources = '') {
|
|
74
|
+
if (!endpointsInfo?.length) return '';
|
|
75
|
+
const list = endpointsInfo
|
|
76
|
+
.map((e) => `- ${e.path} [${(e.methods || []).join(', ')}]: ${e.summary || e.description || '—'}`)
|
|
77
|
+
.join('\n');
|
|
78
|
+
const codeSnippets = endpointsInfo
|
|
79
|
+
.filter((e) => e.handlerSource)
|
|
80
|
+
.map((e) => `Path ${e.path}:\n${(e.handlerSource || '').slice(0, PROMPT_GROUP_SNIPPET_CHARS)}`)
|
|
81
|
+
.join('\n\n');
|
|
82
|
+
const codeLimit = dependencySources ? PROMPT_GROUP_CODE_LIMIT_WITH_DEPS : PROMPT_GROUP_CODE_LIMIT;
|
|
83
|
+
const { relatedSection } = buildEndpointPromptContext({ dependencySources });
|
|
84
|
+
return `You are an API documentation assistant. A single source file (group) exposes these endpoints:
|
|
85
|
+
|
|
86
|
+
Group id: ${groupId}
|
|
87
|
+
|
|
88
|
+
Endpoints:
|
|
89
|
+
${list}
|
|
90
|
+
|
|
91
|
+
Handler code (excerpts):
|
|
92
|
+
\`\`\`javascript
|
|
93
|
+
${codeSnippets.slice(0, codeLimit)}
|
|
94
|
+
\`\`\`
|
|
95
|
+
${relatedSection}
|
|
96
|
+
|
|
97
|
+
Write a SHORT paragraph (2–4 sentences) describing what this group of endpoints does as a whole. Focus on the domain and purpose, not the HTTP details. Respond with ONLY a single JSON object:
|
|
98
|
+
{ "name": "Human-readable group name", "description": "Your paragraph here." }
|
|
99
|
+
Use "name" as a short label (e.g. "Users", "Health & routes"); keep description under 300 characters.`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build prompt for enhancing a single endpoint (OpenAPI-style metadata).
|
|
104
|
+
* @param {object} endpointInfo - { path, methods, metadata?, handlerSource?, dependencySources? }
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
export function buildEnhanceEndpointPrompt(endpointInfo) {
|
|
108
|
+
const { path, methods = [], metadata = {} } = endpointInfo;
|
|
109
|
+
const { handlerSnippet, relatedSection } = buildEndpointPromptContext(endpointInfo);
|
|
110
|
+
const existing = Object.keys(metadata).length ? `Existing metadata: ${JSON.stringify(metadata)}\n` : '';
|
|
111
|
+
return `You are an API documentation assistant. Given an HTTP endpoint and its source code, suggest OpenAPI-style metadata.
|
|
112
|
+
|
|
113
|
+
Endpoint path: ${path}
|
|
114
|
+
HTTP methods: ${methods.join(', ')}
|
|
115
|
+
${existing}Handler source (for context):
|
|
116
|
+
\`\`\`javascript
|
|
117
|
+
${handlerSnippet}
|
|
118
|
+
\`\`\`
|
|
119
|
+
${relatedSection}
|
|
120
|
+
|
|
121
|
+
${ENHANCE_RULES}
|
|
122
|
+
${ENHANCE_RESPONSE_SHAPE}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build prompt for method-specific endpoint metadata (per-method summary, request, response).
|
|
127
|
+
* @param {object} endpointInfo - { path, methods, metadata?, handlerSource?, dependencySources? }
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
export function buildEnhanceEndpointPerMethodPrompt(endpointInfo) {
|
|
131
|
+
const { path, methods = [], metadata = {} } = endpointInfo;
|
|
132
|
+
const { handlerSnippet, relatedSection } = buildEndpointPromptContext(endpointInfo);
|
|
133
|
+
const methodsLower = methods.map((m) => m.toLowerCase());
|
|
134
|
+
const existing = Object.keys(metadata).length ? `Existing metadata: ${JSON.stringify(metadata)}\n` : '';
|
|
135
|
+
return `You are an API documentation assistant. This endpoint supports multiple HTTP methods. Provide METHOD-SPECIFIC metadata so each method is documented accurately.
|
|
136
|
+
|
|
137
|
+
Endpoint path: ${path}
|
|
138
|
+
HTTP methods: ${methods.join(', ')}
|
|
139
|
+
|
|
140
|
+
${existing}Handler source (for context):
|
|
141
|
+
\`\`\`javascript
|
|
142
|
+
${handlerSnippet}
|
|
143
|
+
\`\`\`
|
|
144
|
+
${relatedSection}
|
|
145
|
+
|
|
146
|
+
RULES:
|
|
147
|
+
- Return ONE JSON object keyed by lowercase method name: "get", "put", "post", "delete", "patch", "head", "options".
|
|
148
|
+
- Include a key for EACH of: ${methodsLower.map((m) => `"${m}"`).join(', ')}.
|
|
149
|
+
- For each method provide: "summary" (one line, method-specific, e.g. "Get user by id" for GET, "Update user" for PUT), optional "description", optional "request" (only for methods that accept a body: put, post, patch; omit for get, delete, head, options), and "response" (ONLY the status codes that THIS method returns — e.g. GET returns 200, DELETE returns 204; do not list 204 under PUT or 200 under DELETE unless the handler returns it for that branch).
|
|
150
|
+
- For request.body, set "required": true or "required": false on each field based on code. Include "format" when relevant (e.g. "email").
|
|
151
|
+
- Each method's response object must only list the HTTP status codes that that specific method returns.
|
|
152
|
+
|
|
153
|
+
Respond with ONLY a single JSON object (no markdown, no explanation). Shape:
|
|
154
|
+
${PER_METHOD_RESPONSE_SHAPE}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build prompt for reordering tag groups by importance.
|
|
159
|
+
* @param {object} spec - OpenAPI 3 spec with spec.tags
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
export function buildReorderTagsPrompt(spec) {
|
|
163
|
+
const tags = spec?.tags ?? [];
|
|
164
|
+
if (!tags.length) return '';
|
|
165
|
+
const list = tags
|
|
166
|
+
.map((t) => `- "${t.name}"${t.description ? `: ${t.description.slice(0, 200)}` : ''}`)
|
|
167
|
+
.join('\n');
|
|
168
|
+
return `You are an API documentation assistant. This API has the following tag groups (categories):
|
|
169
|
+
|
|
170
|
+
${list}
|
|
171
|
+
|
|
172
|
+
Reorder these groups by importance for a reader exploring the API. Put the most important or central groups first (e.g. main resources, auth), and utility or secondary groups last (e.g. health, admin).
|
|
173
|
+
|
|
174
|
+
Respond with ONLY a JSON array of the tag names in the desired order. Example: ["Users", "Auth", "Health & routes"]
|
|
175
|
+
Do not include any other text or markdown.`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build prompt for generating the API overview Markdown page.
|
|
180
|
+
* @param {object} spec - OpenAPI 3 spec with info, tags, paths
|
|
181
|
+
* @param {object} [options] - { title?, version?, description? }
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
export function buildOverviewPrompt(spec, options = {}) {
|
|
185
|
+
const info = spec?.info ?? {};
|
|
186
|
+
const title = options.title ?? info.title ?? 'API';
|
|
187
|
+
const version = options.version ?? info.version ?? '1.0.0';
|
|
188
|
+
const description = options.description ?? info.description ?? '';
|
|
189
|
+
const tags = spec?.tags ?? [];
|
|
190
|
+
const paths = spec?.paths ?? {};
|
|
191
|
+
const tagList = tags.map((t) => `- **${t.name}**: ${t.description || '—'}`).join('\n');
|
|
192
|
+
const pathList = Object.entries(paths)
|
|
193
|
+
.slice(0, 50)
|
|
194
|
+
.map(([p, ops]) => {
|
|
195
|
+
const methods = Object.keys(ops)
|
|
196
|
+
.filter((k) => ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'].includes(k))
|
|
197
|
+
.join(', ')
|
|
198
|
+
.toUpperCase();
|
|
199
|
+
return `- ${p} [${methods}]`;
|
|
200
|
+
})
|
|
201
|
+
.join('\n');
|
|
202
|
+
|
|
203
|
+
return `You are an API documentation assistant. Generate a single comprehensive Markdown document for this API.
|
|
204
|
+
|
|
205
|
+
API title: ${title}
|
|
206
|
+
Version: ${version}
|
|
207
|
+
${description ? `Description: ${description.slice(0, 500)}\n` : ''}
|
|
208
|
+
|
|
209
|
+
Tag groups (API areas):
|
|
210
|
+
${tagList}
|
|
211
|
+
|
|
212
|
+
Sample of endpoints (path and methods):
|
|
213
|
+
${pathList}
|
|
214
|
+
|
|
215
|
+
Write a Markdown document that includes:
|
|
216
|
+
1. A short **project summary** (what this API does).
|
|
217
|
+
2. **APIs available**: a high-level list of the tag groups and what they cover.
|
|
218
|
+
3. **Key endpoints** or "Getting started": suggest a few important paths (e.g. health check, main resources).
|
|
219
|
+
4. Any other brief sections that fit (e.g. Authentication, Rate limits) only if clearly inferable from the spec; otherwise omit.
|
|
220
|
+
|
|
221
|
+
Use clear headings (##, ###). Keep the document concise (under 2 pages). Output ONLY the Markdown, no surrounding explanation or code fence.`;
|
|
222
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM provider for auto-documentation: single OpenAI-compatible implementation.
|
|
3
|
+
* Works with OpenAI, OpenRouter, Ollama (OpenAI-compatible endpoint), Azure, etc.
|
|
4
|
+
* Uses fetch() only — no provider-specific npm dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { extractJSON, extractJSONArray, reconcileOrderedTags } from './parse.js';
|
|
8
|
+
import {
|
|
9
|
+
buildSummarizeGroupPrompt,
|
|
10
|
+
buildEnhanceEndpointPrompt,
|
|
11
|
+
buildEnhanceEndpointPerMethodPrompt,
|
|
12
|
+
buildReorderTagsPrompt,
|
|
13
|
+
buildOverviewPrompt,
|
|
14
|
+
} from './prompts.js';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
|
|
17
|
+
const DEFAULT_MODEL = 'gpt-4o-mini';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* OpenAI-compatible LLM provider. POSTs to {baseURL}/chat/completions.
|
|
21
|
+
*/
|
|
22
|
+
class LLMProvider {
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
25
|
+
this.model = options.model ?? DEFAULT_MODEL;
|
|
26
|
+
this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
|
|
27
|
+
this.options = options;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Send a prompt to the LLM and return the raw text response.
|
|
32
|
+
* @param {string} prompt
|
|
33
|
+
* @returns {Promise<string>}
|
|
34
|
+
*/
|
|
35
|
+
async analyze(prompt) {
|
|
36
|
+
const url = `${this.baseURL}/chat/completions`;
|
|
37
|
+
const headers = {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
40
|
+
};
|
|
41
|
+
const body = {
|
|
42
|
+
model: this.model,
|
|
43
|
+
messages: [{ role: 'user', content: prompt }],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers,
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
const text = await res.text();
|
|
54
|
+
throw new Error(`LLM request failed (${res.status}): ${text.slice(0, 300)}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
const content = data.choices?.[0]?.message?.content ?? '';
|
|
59
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content);
|
|
60
|
+
const rawUsage = data.usage;
|
|
61
|
+
const usage = {
|
|
62
|
+
prompt_tokens: rawUsage?.prompt_tokens ?? 0,
|
|
63
|
+
completion_tokens: rawUsage?.completion_tokens ?? 0,
|
|
64
|
+
total_tokens: rawUsage?.total_tokens ?? (rawUsage?.prompt_tokens ?? 0) + (rawUsage?.completion_tokens ?? 0),
|
|
65
|
+
};
|
|
66
|
+
return { content: text, usage };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Summarize what a target file (group) does from its endpoints and handler context.
|
|
71
|
+
* @param {string} groupId - Group id (e.g. target file path without .target.js)
|
|
72
|
+
* @param {Array<object>} endpointsInfo - List of { path, methods, summary?, description?, handlerSource?, dependencySources? }
|
|
73
|
+
* @param {string} [dependencySources] - Optional full context (target + dependencies) for Level 3
|
|
74
|
+
* @returns {Promise<{ name: string, description: string }>} Tag name and description for OpenAPI
|
|
75
|
+
*/
|
|
76
|
+
async summarizeTargetGroup(groupId, endpointsInfo, dependencySources = '') {
|
|
77
|
+
if (!endpointsInfo?.length) {
|
|
78
|
+
return { name: groupId, description: '', _usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } };
|
|
79
|
+
}
|
|
80
|
+
const prompt = buildSummarizeGroupPrompt(groupId, endpointsInfo, dependencySources);
|
|
81
|
+
const { content: raw, usage } = await this.analyze(prompt);
|
|
82
|
+
const json = extractJSON(raw);
|
|
83
|
+
if (!json)
|
|
84
|
+
return {
|
|
85
|
+
name: groupId.split('/').pop() || groupId,
|
|
86
|
+
description: '',
|
|
87
|
+
_usage: usage,
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
name: json.name || groupId.split('/').pop() || groupId,
|
|
91
|
+
description: (json.description || '').trim(),
|
|
92
|
+
_usage: usage,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build enhanced OpenAPI-style metadata for an endpoint from handler info.
|
|
98
|
+
* @param {object} endpointInfo - { path, methods, metadata?, handlerSource?, dependencySources? }
|
|
99
|
+
* @returns {Promise<object>} Enhanced metadata (summary, description, request?, response?)
|
|
100
|
+
*/
|
|
101
|
+
async enhanceEndpointDocs(endpointInfo) {
|
|
102
|
+
const { path } = endpointInfo;
|
|
103
|
+
const prompt = buildEnhanceEndpointPrompt(endpointInfo);
|
|
104
|
+
const { content: raw, usage } = await this.analyze(prompt);
|
|
105
|
+
const json = extractJSON(raw);
|
|
106
|
+
if (!json) return { summary: path, description: '', _usage: usage };
|
|
107
|
+
return {
|
|
108
|
+
summary: json.summary || path,
|
|
109
|
+
description: json.description || '',
|
|
110
|
+
...(json.request && { request: json.request }),
|
|
111
|
+
...(json.response && { response: json.response }),
|
|
112
|
+
_usage: usage,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build method-specific OpenAPI metadata so each HTTP method gets its own summary, description, request, and response.
|
|
118
|
+
* Returns an object keyed by lowercase method (get, put, post, delete, patch, head, options). If the LLM
|
|
119
|
+
* returns a flat shape or omits methods, the caller should fall back to shared metadata.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} endpointInfo - { path, methods, metadata?, handlerSource?, dependencySources? }
|
|
122
|
+
* @returns {Promise<object>} Method-keyed metadata, e.g. { get: { summary, description?, response }, put: { summary, request?, response }, ... }
|
|
123
|
+
*/
|
|
124
|
+
async enhanceEndpointDocsPerMethod(endpointInfo) {
|
|
125
|
+
const prompt = buildEnhanceEndpointPerMethodPrompt(endpointInfo);
|
|
126
|
+
const { content: raw, usage } = await this.analyze(prompt);
|
|
127
|
+
const json = extractJSON(raw);
|
|
128
|
+
if (!json || typeof json !== 'object') return { _usage: usage, _fallback: true };
|
|
129
|
+
return { ...json, _usage: usage };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Return tag names ordered by importance (most important first) for use in OpenAPI spec.tags.
|
|
134
|
+
* @param {object} spec - OpenAPI 3 spec with spec.tags (array of { name, description? })
|
|
135
|
+
* @returns {Promise<{ orderedTagNames: string[], _usage?: object }>}
|
|
136
|
+
*/
|
|
137
|
+
async reorderTagsByImportance(spec) {
|
|
138
|
+
const tags = spec?.tags ?? [];
|
|
139
|
+
if (!tags.length) return { orderedTagNames: [], _usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } };
|
|
140
|
+
const prompt = buildReorderTagsPrompt(spec);
|
|
141
|
+
const { content: raw, usage } = await this.analyze(prompt);
|
|
142
|
+
const parsed = extractJSON(raw) ?? extractJSONArray(raw);
|
|
143
|
+
const orderedTagNames = Array.isArray(parsed)
|
|
144
|
+
? parsed.filter((n) => typeof n === 'string').map((n) => String(n).trim()).filter(Boolean)
|
|
145
|
+
: tags.map((t) => t.name);
|
|
146
|
+
const orderedTags = reconcileOrderedTags(orderedTagNames, tags);
|
|
147
|
+
return {
|
|
148
|
+
orderedTagNames: orderedTags.map((t) => t.name),
|
|
149
|
+
_usage: usage,
|
|
150
|
+
_orderedTags: orderedTags,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate a comprehensive project/API overview page in Markdown.
|
|
156
|
+
* @param {object} spec - OpenAPI 3 spec (after reorder) with info, tags, paths
|
|
157
|
+
* @param {object} [options] - { title?, version?, description? } (defaults from spec.info)
|
|
158
|
+
* @returns {Promise<{ markdown: string, _usage?: object }>}
|
|
159
|
+
*/
|
|
160
|
+
async generateOverviewPage(spec, options = {}) {
|
|
161
|
+
const prompt = buildOverviewPrompt(spec, options);
|
|
162
|
+
const { content: raw, usage } = await this.analyze(prompt);
|
|
163
|
+
const markdown = typeof raw === 'string' ? raw.trim() : '';
|
|
164
|
+
return { markdown, _usage: usage };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create an LLM provider from config.
|
|
170
|
+
* Single OpenAI-compatible setup: works with OpenAI, OpenRouter, Ollama (compat), Azure, etc.
|
|
171
|
+
*
|
|
172
|
+
* @param {object} config - { baseURL?, apiKey?, model? }
|
|
173
|
+
* - baseURL: e.g. 'https://api.openai.com/v1' | 'https://openrouter.ai/api/v1' | 'http://localhost:11434/v1'
|
|
174
|
+
* - apiKey: optional for local (e.g. Ollama); use OPENAI_API_KEY or OPENROUTER_API_KEY
|
|
175
|
+
* - model: e.g. 'gpt-4o-mini' | 'openai/gpt-4o-mini' (OpenRouter)
|
|
176
|
+
* @returns {LLMProvider}
|
|
177
|
+
*/
|
|
178
|
+
function createProvider(config) {
|
|
179
|
+
if (!config || typeof config !== 'object') {
|
|
180
|
+
return new LLMProvider({});
|
|
181
|
+
}
|
|
182
|
+
return new LLMProvider(config);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { LLMProvider, createProvider };
|
|
186
|
+
export { extractJSON, extractJSONArray } from './parse.js';
|
|
187
|
+
export default LLMProvider;
|