dinorex 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 +126 -0
- package/package.json +40 -0
- package/src/agent.groq.js +279 -0
- package/src/agent.js +155 -0
- package/src/cli.js +198 -0
- package/src/generators/postman.js +84 -0
- package/src/generators/swagger.js +121 -0
- package/src/public/index.html +654 -0
- package/src/scanner.js +119 -0
- package/src/server.js +136 -0
- package/src/store.js +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# 🦕 Dinorex
|
|
2
|
+
|
|
3
|
+
> AI-powered API documentation generator. One command, full docs.
|
|
4
|
+
|
|
5
|
+
Dinorex scans your Node.js project — routes, controllers, services, and DB models — and uses AI to automatically generate an interactive docs UI, a Postman collection, and a Swagger/OpenAPI file.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g dinorex
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or use without installing (requires Node 18+):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx dinorex scan
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Setup: Your Anthropic API Key
|
|
24
|
+
|
|
25
|
+
Dinorex uses Claude AI to analyze your code. You need a free Anthropic API key.
|
|
26
|
+
|
|
27
|
+
1. Go to [https://console.anthropic.com](https://console.anthropic.com)
|
|
28
|
+
2. Create an account and generate an API key
|
|
29
|
+
3. Set it in your environment:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Mac/Linux — add to ~/.zshrc or ~/.bashrc
|
|
33
|
+
export ANTHROPIC_API_KEY=sk-ant-your-key-here
|
|
34
|
+
|
|
35
|
+
# Windows (PowerShell)
|
|
36
|
+
$env:ANTHROPIC_API_KEY="sk-ant-your-key-here"
|
|
37
|
+
|
|
38
|
+
# Or pass it directly every time
|
|
39
|
+
dinorex scan --api-key sk-ant-your-key-here
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Scan your project and open docs
|
|
47
|
+
```bash
|
|
48
|
+
cd your-project/
|
|
49
|
+
dinorex scan
|
|
50
|
+
# Opens http://localhost:4321 automatically
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Scan a specific folder
|
|
54
|
+
```bash
|
|
55
|
+
dinorex scan ./backend --port 5000
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### After adding new endpoints — smart rescan
|
|
59
|
+
```bash
|
|
60
|
+
dinorex scan
|
|
61
|
+
# Dinorex detects only new/changed files and updates the spec incrementally
|
|
62
|
+
# No need to re-analyze everything from scratch
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Force a full re-analysis
|
|
66
|
+
```bash
|
|
67
|
+
dinorex scan --full
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Generate files only (no UI server)
|
|
71
|
+
```bash
|
|
72
|
+
dinorex generate ./backend --out ./docs
|
|
73
|
+
# Outputs: *-postman.json, *-openapi.yaml, *-spec.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## What gets scanned
|
|
79
|
+
|
|
80
|
+
| Pattern | Examples |
|
|
81
|
+
|---|---|
|
|
82
|
+
| Routes | `routes/`, `*route*.js`, `*router*.js` |
|
|
83
|
+
| Controllers | `controllers/`, `*controller*.js` |
|
|
84
|
+
| Services | `services/`, `*service*.js` |
|
|
85
|
+
| Models/Schemas | `models/`, `schemas/`, `*model*.js`, `*schema*.js` |
|
|
86
|
+
|
|
87
|
+
Ignores: `node_modules`, `dist`, `build`, `.git`, test files.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Output
|
|
92
|
+
|
|
93
|
+
- **Interactive UI** at `http://localhost:4321` — browse, search, and test endpoints live
|
|
94
|
+
- **Postman Collection** — download and import directly into Postman
|
|
95
|
+
- **Swagger/OpenAPI YAML** — import into any OpenAPI-compatible tool
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Works with
|
|
100
|
+
|
|
101
|
+
- Express.js
|
|
102
|
+
- Fastify
|
|
103
|
+
- NestJS
|
|
104
|
+
- Koa
|
|
105
|
+
- Any Node.js framework that uses route files
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Cache & incremental updates
|
|
110
|
+
|
|
111
|
+
Dinorex stores a `.dinorex/spec.json` file in your project directory (auto-gitignored). On subsequent runs it only re-analyzes new or changed files — making rescans much faster.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Options
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
dinorex scan [directory]
|
|
119
|
+
-p, --port <port> Port to run docs server (default: 4321)
|
|
120
|
+
--no-open Don't auto-open the browser
|
|
121
|
+
--api-key <key> Anthropic API key
|
|
122
|
+
|
|
123
|
+
dinorex generate [directory]
|
|
124
|
+
--out <dir> Output directory (default: ./dinorex-output)
|
|
125
|
+
--api-key <key> Anthropic API key
|
|
126
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dinorex",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered API documentation generator — one command, full docs.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dinorex": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/server.js",
|
|
12
|
+
"dev": "nodemon src/server.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"api",
|
|
16
|
+
"swagger",
|
|
17
|
+
"postman",
|
|
18
|
+
"openapi",
|
|
19
|
+
"documentation",
|
|
20
|
+
"cli",
|
|
21
|
+
"ai"
|
|
22
|
+
],
|
|
23
|
+
"author": "Dinorex",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@anthropic-ai/sdk": "^0.20.0",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"commander": "^11.1.0",
|
|
29
|
+
"cors": "^2.8.5",
|
|
30
|
+
"express": "^4.18.2",
|
|
31
|
+
"glob": "^10.3.10",
|
|
32
|
+
"js-yaml": "^4.1.0",
|
|
33
|
+
"multer": "^1.4.5-lts.1",
|
|
34
|
+
"ora": "^7.0.1",
|
|
35
|
+
"chokidar": "^3.5.3"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent.groq.js — Free Groq/Llama3 version of the Dinorex AI agent.
|
|
3
|
+
*
|
|
4
|
+
* Get a free API key at: https://console.groq.com
|
|
5
|
+
* Set it: export GROQ_API_KEY=gsk_your_key_here
|
|
6
|
+
*
|
|
7
|
+
* To use this instead of the Anthropic agent, change the import in cli.js and server.js:
|
|
8
|
+
* import { analyzeWithAI, analyzeIncremental } from "./agent.groq.js";
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";
|
|
12
|
+
const MODEL = "llama-3.3-70b-versatile";
|
|
13
|
+
const MAX_CHARS = 12000; // safe limit per request (~3000 tokens of context)
|
|
14
|
+
|
|
15
|
+
function buildContext(files) {
|
|
16
|
+
return files.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``).join("\n\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SYSTEM_FULL = `You are an expert API analyst. Analyze source code (JavaScript OR TypeScript) and extract a complete API specification.
|
|
20
|
+
|
|
21
|
+
Supported frameworks — recognize ALL of these:
|
|
22
|
+
- Express / Fastify / Koa: router.get('/path', handler), app.post('/path', handler)
|
|
23
|
+
- NestJS decorators: @Controller('base'), @Get(':id'), @Post(), @Put(), @Patch(), @Delete(), @Body(), @Param(), @Query(), @UseGuards()
|
|
24
|
+
- TypeScript DTOs, interfaces, type aliases, class properties
|
|
25
|
+
- Mongoose / Sequelize / TypeORM / Prisma schemas
|
|
26
|
+
- Zod / Joi / Yup schemas: extract field names and types
|
|
27
|
+
- class-validator decorators: @IsString(), @IsEmail(), etc.
|
|
28
|
+
|
|
29
|
+
Rules:
|
|
30
|
+
- Return ONLY valid JSON. No markdown, no explanation, no code fences.
|
|
31
|
+
- Infer realistic example values from model/schema field names and types.
|
|
32
|
+
- Group endpoints into logical collections (e.g. "Users", "Auth", "Products").
|
|
33
|
+
- Detect auth guards: @UseGuards(), authMiddleware, isAuthenticated, verifyToken, requireAuth, JwtAuthGuard → requiresAuth: true.
|
|
34
|
+
- For NestJS: combine @Controller('users') prefix with method paths (@Get(':id') → /users/:id).
|
|
35
|
+
- TypeScript optional fields (field?: type) → required: false.
|
|
36
|
+
|
|
37
|
+
Return ONLY this JSON structure, nothing else:
|
|
38
|
+
{
|
|
39
|
+
"projectName": "string",
|
|
40
|
+
"baseUrl": "http://localhost:3000",
|
|
41
|
+
"version": "1.0.0",
|
|
42
|
+
"description": "string",
|
|
43
|
+
"collections": [
|
|
44
|
+
{
|
|
45
|
+
"name": "string",
|
|
46
|
+
"description": "string",
|
|
47
|
+
"endpoints": [
|
|
48
|
+
{
|
|
49
|
+
"id": "unique-kebab-slug",
|
|
50
|
+
"method": "GET|POST|PUT|PATCH|DELETE",
|
|
51
|
+
"path": "/api/resource/:id",
|
|
52
|
+
"summary": "Short title",
|
|
53
|
+
"description": "Longer description",
|
|
54
|
+
"requiresAuth": false,
|
|
55
|
+
"pathParams": [{ "name": "id", "type": "string", "description": "...", "example": "abc123" }],
|
|
56
|
+
"queryParams": [{ "name": "page", "type": "integer", "description": "...", "example": 1 }],
|
|
57
|
+
"requestBody": {
|
|
58
|
+
"contentType": "application/json",
|
|
59
|
+
"schema": {
|
|
60
|
+
"fieldName": { "type": "string", "example": "value", "required": true, "description": "..." }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"responses": {
|
|
64
|
+
"200": { "description": "Success", "example": {} },
|
|
65
|
+
"400": { "description": "Bad Request" },
|
|
66
|
+
"401": { "description": "Unauthorized" },
|
|
67
|
+
"404": { "description": "Not Found" },
|
|
68
|
+
"500": { "description": "Server Error" }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}`;
|
|
75
|
+
|
|
76
|
+
const SYSTEM_INCREMENTAL = `You are an expert API analyst doing an INCREMENTAL update to an existing API spec.
|
|
77
|
+
|
|
78
|
+
You understand JavaScript AND TypeScript including Express, NestJS decorators, DTOs, Zod schemas, Mongoose/TypeORM/Prisma models.
|
|
79
|
+
|
|
80
|
+
You will receive:
|
|
81
|
+
1. The EXISTING spec (full JSON)
|
|
82
|
+
2. NEW or CHANGED source files to analyze
|
|
83
|
+
|
|
84
|
+
Your job:
|
|
85
|
+
- Extract endpoints from new/changed files
|
|
86
|
+
- If endpoint already exists (same method + path): update it if code changed, keep it if unchanged
|
|
87
|
+
- If it is NEW: add it to the correct collection (create collection if needed)
|
|
88
|
+
- Remove endpoints whose source files are listed under REMOVED FILES
|
|
89
|
+
- Keep all existing endpoints from unchanged files
|
|
90
|
+
|
|
91
|
+
Return the COMPLETE updated spec JSON. No markdown, no explanation, ONLY JSON.`;
|
|
92
|
+
|
|
93
|
+
async function callGroq(systemPrompt, userMessage) {
|
|
94
|
+
const apiKey = process.env.GROQ_API_KEY;
|
|
95
|
+
if (!apiKey) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"GROQ_API_KEY is not set.\nGet a free key at https://console.groq.com\nThen run: export GROQ_API_KEY=gsk_your_key_here"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const response = await fetch(GROQ_API_URL, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
model: MODEL,
|
|
109
|
+
temperature: 0.1, // low temp = more deterministic JSON output
|
|
110
|
+
max_tokens: 8000,
|
|
111
|
+
messages: [
|
|
112
|
+
{ role: "system", content: systemPrompt },
|
|
113
|
+
{ role: "user", content: userMessage },
|
|
114
|
+
],
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const err = await response.text();
|
|
120
|
+
throw new Error(`Groq API error ${response.status}: ${err}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = await response.json();
|
|
124
|
+
const raw = data.choices?.[0]?.message?.content || "";
|
|
125
|
+
return raw;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseJSON(raw) {
|
|
129
|
+
// Strip any accidental markdown fences Llama might add
|
|
130
|
+
const cleaned = raw
|
|
131
|
+
.trim()
|
|
132
|
+
.replace(/^```json\s*/i, "")
|
|
133
|
+
.replace(/^```\s*/i, "")
|
|
134
|
+
.replace(/```\s*$/i, "")
|
|
135
|
+
.trim();
|
|
136
|
+
|
|
137
|
+
// Find the first { and last } to extract just the JSON object
|
|
138
|
+
const start = cleaned.indexOf("{");
|
|
139
|
+
const end = cleaned.lastIndexOf("}");
|
|
140
|
+
if (start === -1 || end === -1) {
|
|
141
|
+
throw new Error(`No JSON object found in response.\n\nSnippet: ${raw.slice(0, 300)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const jsonStr = cleaned.slice(start, end + 1);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(jsonStr);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw new Error(`Invalid JSON from Groq: ${err.message}\n\nSnippet: ${jsonStr.slice(0, 300)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Split files into batches that fit within MAX_CHARS each.
|
|
155
|
+
*/
|
|
156
|
+
function batchFiles(files) {
|
|
157
|
+
const batches = [];
|
|
158
|
+
let current = [];
|
|
159
|
+
let size = 0;
|
|
160
|
+
|
|
161
|
+
for (const f of files) {
|
|
162
|
+
const len = f.content.length + f.path.length + 20;
|
|
163
|
+
if (size + len > MAX_CHARS && current.length > 0) {
|
|
164
|
+
batches.push(current);
|
|
165
|
+
current = [];
|
|
166
|
+
size = 0;
|
|
167
|
+
}
|
|
168
|
+
// If a single file is too large, truncate it
|
|
169
|
+
const truncated = { ...f, content: f.content.slice(0, MAX_CHARS) };
|
|
170
|
+
current.push(truncated);
|
|
171
|
+
size += len;
|
|
172
|
+
}
|
|
173
|
+
if (current.length > 0) batches.push(current);
|
|
174
|
+
return batches;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Merge multiple partial specs into one, deduplicating endpoints by method+path.
|
|
179
|
+
*/
|
|
180
|
+
function mergeSpecs(specs) {
|
|
181
|
+
const base = specs[0];
|
|
182
|
+
const collectionsMap = {};
|
|
183
|
+
|
|
184
|
+
for (const spec of specs) {
|
|
185
|
+
for (const col of spec.collections) {
|
|
186
|
+
if (!collectionsMap[col.name]) {
|
|
187
|
+
collectionsMap[col.name] = { ...col, endpoints: [] };
|
|
188
|
+
}
|
|
189
|
+
for (const ep of col.endpoints) {
|
|
190
|
+
const key = `${ep.method}:${ep.path}`;
|
|
191
|
+
const existing = collectionsMap[col.name].endpoints.find(
|
|
192
|
+
e => `${e.method}:${e.path}` === key
|
|
193
|
+
);
|
|
194
|
+
if (!existing) collectionsMap[col.name].endpoints.push(ep);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...base,
|
|
201
|
+
collections: Object.values(collectionsMap),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function analyzeWithAI(collected, projectName = "API") {
|
|
206
|
+
// Priority order: routes+controllers first (most important), then services, then models
|
|
207
|
+
const allFiles = [
|
|
208
|
+
...collected.routes.map(f => ({ ...f, kind: "ROUTE" })),
|
|
209
|
+
...collected.controllers.map(f => ({ ...f, kind: "CONTROLLER" })),
|
|
210
|
+
...collected.services.map(f => ({ ...f, kind: "SERVICE" })),
|
|
211
|
+
...collected.models.map(f => ({ ...f, kind: "MODEL" })),
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const batches = batchFiles(allFiles);
|
|
215
|
+
console.log(`\n 📦 Sending ${batches.length} batch(es) to Groq (${allFiles.length} files total)...`);
|
|
216
|
+
|
|
217
|
+
const partialSpecs = [];
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < batches.length; i++) {
|
|
220
|
+
const batch = batches[i];
|
|
221
|
+
const context = batch
|
|
222
|
+
.map(f => `### [${f.kind}] ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
|
|
223
|
+
.join("\n\n");
|
|
224
|
+
|
|
225
|
+
const userMessage = `Project name: "${projectName}" (batch ${i + 1} of ${batches.length})
|
|
226
|
+
|
|
227
|
+
${context}
|
|
228
|
+
|
|
229
|
+
Extract all API endpoints found in these files and return ONLY the JSON spec.`;
|
|
230
|
+
|
|
231
|
+
const raw = await callGroq(SYSTEM_FULL, userMessage);
|
|
232
|
+
const partial = parseJSON(raw);
|
|
233
|
+
partialSpecs.push(partial);
|
|
234
|
+
|
|
235
|
+
// Small delay between batches to avoid rate limiting
|
|
236
|
+
if (i < batches.length - 1) await sleep(1000);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return batches.length === 1 ? partialSpecs[0] : mergeSpecs(partialSpecs);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sleep(ms) {
|
|
243
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function analyzeIncremental(existingSpec, diff) {
|
|
247
|
+
const { newFiles, changedFiles, removedFiles } = diff;
|
|
248
|
+
|
|
249
|
+
if (!newFiles.length && !changedFiles.length && !removedFiles.length) {
|
|
250
|
+
return { spec: existingSpec, changed: false };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const filesToAnalyze = [...newFiles, ...changedFiles];
|
|
254
|
+
const removedContext = removedFiles.length > 0
|
|
255
|
+
? `\n\nREMOVED FILES (delete their endpoints):\n${removedFiles.join("\n")}`
|
|
256
|
+
: "";
|
|
257
|
+
|
|
258
|
+
// Spec JSON itself can be large — truncate for incremental context
|
|
259
|
+
const specStr = JSON.stringify(existingSpec, null, 2);
|
|
260
|
+
const specTruncated = specStr.length > 6000
|
|
261
|
+
? specStr.slice(0, 6000) + "\n... [truncated]"
|
|
262
|
+
: specStr;
|
|
263
|
+
|
|
264
|
+
const changedContext = filesToAnalyze
|
|
265
|
+
.map(f => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\``)
|
|
266
|
+
.join("\n\n");
|
|
267
|
+
|
|
268
|
+
const userMessage = `EXISTING SPEC:
|
|
269
|
+
${specTruncated}
|
|
270
|
+
|
|
271
|
+
NEW/CHANGED FILES:
|
|
272
|
+
${changedContext}${removedContext}
|
|
273
|
+
|
|
274
|
+
Return the complete updated spec JSON only.`;
|
|
275
|
+
|
|
276
|
+
const raw = await callGroq(SYSTEM_INCREMENTAL, userMessage);
|
|
277
|
+
const updated = parseJSON(raw);
|
|
278
|
+
return { spec: updated, changed: true };
|
|
279
|
+
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
|
|
3
|
+
const client = new Anthropic();
|
|
4
|
+
|
|
5
|
+
function buildContext(files) {
|
|
6
|
+
return files.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``).join("\n\n");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SYSTEM_FULL = `You are an expert API analyst. Analyze source code (JavaScript OR TypeScript) and extract a complete API specification.
|
|
10
|
+
|
|
11
|
+
Supported frameworks and patterns — recognize ALL of these:
|
|
12
|
+
- Express / Fastify / Koa: router.get('/path', handler), app.post('/path', handler)
|
|
13
|
+
- NestJS decorators: @Controller('base'), @Get(':id'), @Post(), @Put(), @Patch(), @Delete(), @Body(), @Param(), @Query(), @UseGuards()
|
|
14
|
+
- TypeScript types & interfaces: extract field names/types from DTOs, interfaces, type aliases, class properties
|
|
15
|
+
- Mongoose/Sequelize/TypeORM/Prisma schemas: extract model fields and types
|
|
16
|
+
- tRPC routers: t.router({ ... }), publicProcedure, protectedProcedure — map to equivalent REST-style endpoints
|
|
17
|
+
- Zod/Joi/Yup schemas: extract field names and types as request/response body schemas
|
|
18
|
+
- Class-validator decorators: @IsString(), @IsEmail(), @IsNumber(), etc. → use as field type hints
|
|
19
|
+
|
|
20
|
+
Rules:
|
|
21
|
+
- Return ONLY valid JSON. No markdown, no explanation.
|
|
22
|
+
- Infer realistic example values from model/schema field names and types (TypeScript types count).
|
|
23
|
+
- Group endpoints into logical tags/collections (e.g. "Users", "Auth", "Products").
|
|
24
|
+
- Detect auth guards/middleware: @UseGuards(), @Roles(), authMiddleware, isAuthenticated, verifyToken, requireAuth, JwtAuthGuard, etc. → requiresAuth: true.
|
|
25
|
+
- For NestJS: combine @Controller('users') prefix with method decorator paths (e.g. @Get(':id') → /users/:id).
|
|
26
|
+
- Use DTO classes, interfaces, Zod schemas, or Mongoose models to build realistic request/response body examples.
|
|
27
|
+
- TypeScript optional fields (field?: type) → required: false. Non-optional → required: true.
|
|
28
|
+
|
|
29
|
+
Return this exact structure:
|
|
30
|
+
{
|
|
31
|
+
"projectName": "string",
|
|
32
|
+
"baseUrl": "http://localhost:3000",
|
|
33
|
+
"version": "1.0.0",
|
|
34
|
+
"description": "string",
|
|
35
|
+
"collections": [
|
|
36
|
+
{
|
|
37
|
+
"name": "string",
|
|
38
|
+
"description": "string",
|
|
39
|
+
"endpoints": [
|
|
40
|
+
{
|
|
41
|
+
"id": "unique-kebab-slug",
|
|
42
|
+
"method": "GET|POST|PUT|PATCH|DELETE",
|
|
43
|
+
"path": "/api/resource/:id",
|
|
44
|
+
"summary": "Short title",
|
|
45
|
+
"description": "Longer description",
|
|
46
|
+
"requiresAuth": false,
|
|
47
|
+
"pathParams": [{ "name": "id", "type": "string", "description": "...", "example": "abc123" }],
|
|
48
|
+
"queryParams": [{ "name": "page", "type": "integer", "description": "...", "example": 1 }],
|
|
49
|
+
"requestBody": {
|
|
50
|
+
"contentType": "application/json",
|
|
51
|
+
"schema": {
|
|
52
|
+
"fieldName": { "type": "string", "example": "value", "required": true, "description": "..." }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"responses": {
|
|
56
|
+
"200": { "description": "Success", "example": {} },
|
|
57
|
+
"400": { "description": "Bad Request" },
|
|
58
|
+
"401": { "description": "Unauthorized" },
|
|
59
|
+
"404": { "description": "Not Found" },
|
|
60
|
+
"500": { "description": "Server Error" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}`;
|
|
67
|
+
|
|
68
|
+
const SYSTEM_INCREMENTAL = `You are an expert API analyst doing an INCREMENTAL update to an existing API spec.
|
|
69
|
+
|
|
70
|
+
You understand JavaScript AND TypeScript, including: Express, Fastify, NestJS decorators (@Controller, @Get, @Post, etc.), DTOs, Zod/Joi schemas, Mongoose/TypeORM/Prisma models, and class-validator decorators.
|
|
71
|
+
|
|
72
|
+
You will receive:
|
|
73
|
+
1. The EXISTING spec (full JSON)
|
|
74
|
+
2. NEW or CHANGED source files to analyze
|
|
75
|
+
|
|
76
|
+
Your job:
|
|
77
|
+
- Extract endpoints from the new/changed files
|
|
78
|
+
- For each endpoint, check if it already exists in the spec (match by method + path)
|
|
79
|
+
- If it EXISTS and is unchanged: keep it as-is (don't re-add)
|
|
80
|
+
- If it EXISTS but the code changed: update it with improved info
|
|
81
|
+
- If it's NEW: add it to the correct collection (create the collection if needed)
|
|
82
|
+
- Remove endpoints whose source files were deleted (listed under REMOVED FILES)
|
|
83
|
+
- Keep all existing endpoints from files that weren't changed
|
|
84
|
+
|
|
85
|
+
Return the COMPLETE updated spec JSON (same structure as input). No markdown, no explanation, only JSON.`;
|
|
86
|
+
|
|
87
|
+
export async function analyzeWithAI(collected, projectName = "API") {
|
|
88
|
+
const allFiles = [
|
|
89
|
+
...collected.routes,
|
|
90
|
+
...collected.controllers,
|
|
91
|
+
...collected.services,
|
|
92
|
+
...collected.models,
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const context = [
|
|
96
|
+
collected.routes.length > 0 ? "## ROUTES\n" + buildContext(collected.routes) : null,
|
|
97
|
+
collected.controllers.length > 0 ? "## CONTROLLERS\n" + buildContext(collected.controllers) : null,
|
|
98
|
+
collected.services.length > 0 ? "## SERVICES\n" + buildContext(collected.services) : null,
|
|
99
|
+
collected.models.length > 0 ? "## MODELS\n" + buildContext(collected.models) : null,
|
|
100
|
+
].filter(Boolean).join("\n\n---\n\n");
|
|
101
|
+
|
|
102
|
+
const response = await client.messages.create({
|
|
103
|
+
model: "claude-opus-4-5",
|
|
104
|
+
max_tokens: 8000,
|
|
105
|
+
system: SYSTEM_FULL,
|
|
106
|
+
messages: [{
|
|
107
|
+
role: "user",
|
|
108
|
+
content: `Project: "${projectName}"\n\n${context}\n\nExtract all API endpoints and return the JSON spec.`
|
|
109
|
+
}],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return parseJSON(response.content[0].text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function analyzeIncremental(existingSpec, diff) {
|
|
116
|
+
const { newFiles, changedFiles, removedFiles } = diff;
|
|
117
|
+
|
|
118
|
+
if (newFiles.length === 0 && changedFiles.length === 0 && removedFiles.length === 0) {
|
|
119
|
+
return { spec: existingSpec, changed: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const changedContext = [...newFiles, ...changedFiles]
|
|
123
|
+
.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
|
|
124
|
+
.join("\n\n");
|
|
125
|
+
|
|
126
|
+
const removedContext = removedFiles.length > 0
|
|
127
|
+
? `\n\nREMOVED FILES (delete their endpoints):\n${removedFiles.join("\n")}`
|
|
128
|
+
: "";
|
|
129
|
+
|
|
130
|
+
const response = await client.messages.create({
|
|
131
|
+
model: "claude-opus-4-5",
|
|
132
|
+
max_tokens: 8000,
|
|
133
|
+
system: SYSTEM_INCREMENTAL,
|
|
134
|
+
messages: [{
|
|
135
|
+
role: "user",
|
|
136
|
+
content: `EXISTING SPEC:\n${JSON.stringify(existingSpec, null, 2)}\n\nNEW/CHANGED FILES:\n${changedContext}${removedContext}\n\nReturn the complete updated spec.`
|
|
137
|
+
}],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const updated = parseJSON(response.content[0].text);
|
|
141
|
+
return { spec: updated, changed: true };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseJSON(raw) {
|
|
145
|
+
const cleaned = raw.trim()
|
|
146
|
+
.replace(/^```json\s*/i, "")
|
|
147
|
+
.replace(/^```\s*/i, "")
|
|
148
|
+
.replace(/```\s*$/i, "")
|
|
149
|
+
.trim();
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(cleaned);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
throw new Error(`AI returned invalid JSON: ${err.message}\n\nSnippet: ${raw.slice(0, 300)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|