dinorex 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -15
- package/dist/agent.d.ts +8 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.groq.d.ts +14 -0
- package/dist/agent.groq.d.ts.map +1 -0
- package/dist/agent.groq.js +213 -0
- package/dist/agent.groq.js.map +1 -0
- package/{src → dist}/agent.js +75 -71
- package/dist/agent.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +179 -0
- package/dist/cli.js.map +1 -0
- package/dist/scanner.d.ts +20 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +108 -0
- package/dist/scanner.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +116 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +75 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +72 -0
- package/dist/store.js.map +1 -0
- package/package.json +22 -8
- package/src/agent.groq.js +0 -279
- package/src/cli.js +0 -198
- package/src/generators/postman.js +0 -84
- package/src/generators/swagger.js +0 -121
- package/src/scanner.js +0 -119
- package/src/server.js +0 -136
- package/src/store.js +0 -80
package/README.md
CHANGED
|
@@ -20,21 +20,6 @@ npx dinorex scan
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
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
23
|
# Or pass it directly every time
|
|
39
24
|
dinorex scan --api-key sk-ant-your-key-here
|
|
40
25
|
```
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApiSpec, DiffResult } from "./store.js";
|
|
2
|
+
import type { CollectedFiles } from "./scanner.js";
|
|
3
|
+
export declare function analyzeWithAI(collected: CollectedFiles, projectName?: string): Promise<ApiSpec>;
|
|
4
|
+
export declare function analyzeIncremental(existingSpec: ApiSpec, diff: DiffResult): Promise<{
|
|
5
|
+
spec: ApiSpec;
|
|
6
|
+
changed: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAsGnD,wBAAsB,aAAa,CACjC,SAAS,EAAE,cAAc,EACzB,WAAW,SAAQ,GAClB,OAAO,CAAC,OAAO,CAAC,CAmClB;AAED,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,OAAO,EACrB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAmC9C"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent.groq.ts — 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
|
+
import type { ApiSpec, DiffResult } from "./store.js";
|
|
8
|
+
import type { CollectedFiles } from "./scanner.js";
|
|
9
|
+
export declare function analyzeWithAI(collected: CollectedFiles, projectName?: string): Promise<ApiSpec>;
|
|
10
|
+
export declare function analyzeIncremental(existingSpec: ApiSpec, diff: DiffResult): Promise<{
|
|
11
|
+
spec: ApiSpec;
|
|
12
|
+
changed: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=agent.groq.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.groq.d.ts","sourceRoot":"","sources":["../src/agent.groq.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAuMnD,wBAAsB,aAAa,CACjC,SAAS,EAAE,cAAc,EACzB,WAAW,SAAQ,GAClB,OAAO,CAAC,OAAO,CAAC,CA6BlB;AAED,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,OAAO,EACrB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA0B9C"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent.groq.ts — 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
|
+
const GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";
|
|
8
|
+
const MODEL = "llama-3.3-70b-versatile";
|
|
9
|
+
const MAX_CHARS = 12000;
|
|
10
|
+
const SYSTEM_FULL = `You are an expert API analyst. Analyze source code (JavaScript OR TypeScript) and extract a complete API specification.
|
|
11
|
+
|
|
12
|
+
Supported frameworks — recognize ALL of these:
|
|
13
|
+
- Express / Fastify / Koa: router.get('/path', handler), app.post('/path', handler)
|
|
14
|
+
- NestJS decorators: @Controller('base'), @Get(':id'), @Post(), @Put(), @Patch(), @Delete(), @Body(), @Param(), @Query(), @UseGuards()
|
|
15
|
+
- TypeScript DTOs, interfaces, type aliases, class properties
|
|
16
|
+
- Mongoose / Sequelize / TypeORM / Prisma schemas
|
|
17
|
+
- Zod / Joi / Yup schemas: extract field names and types
|
|
18
|
+
- class-validator decorators: @IsString(), @IsEmail(), etc.
|
|
19
|
+
|
|
20
|
+
Rules:
|
|
21
|
+
- Return ONLY valid JSON. No markdown, no explanation, no code fences.
|
|
22
|
+
- Infer realistic example values from model/schema field names and types.
|
|
23
|
+
- Group endpoints into logical collections (e.g. "Users", "Auth", "Products").
|
|
24
|
+
- Detect auth guards: @UseGuards(), authMiddleware, isAuthenticated, verifyToken, requireAuth, JwtAuthGuard → requiresAuth: true.
|
|
25
|
+
- For NestJS: combine @Controller('users') prefix with method paths (@Get(':id') → /users/:id).
|
|
26
|
+
- TypeScript optional fields (field?: type) → required: false.
|
|
27
|
+
|
|
28
|
+
Return ONLY this JSON structure, nothing else:
|
|
29
|
+
{
|
|
30
|
+
"projectName": "string",
|
|
31
|
+
"baseUrl": "http://localhost:3000",
|
|
32
|
+
"version": "1.0.0",
|
|
33
|
+
"description": "string",
|
|
34
|
+
"collections": [
|
|
35
|
+
{
|
|
36
|
+
"name": "string",
|
|
37
|
+
"description": "string",
|
|
38
|
+
"endpoints": [
|
|
39
|
+
{
|
|
40
|
+
"id": "unique-kebab-slug",
|
|
41
|
+
"method": "GET|POST|PUT|PATCH|DELETE",
|
|
42
|
+
"path": "/api/resource/:id",
|
|
43
|
+
"summary": "Short title",
|
|
44
|
+
"description": "Longer description",
|
|
45
|
+
"requiresAuth": false,
|
|
46
|
+
"pathParams": [{ "name": "id", "type": "string", "description": "...", "example": "abc123" }],
|
|
47
|
+
"queryParams": [{ "name": "page", "type": "integer", "description": "...", "example": 1 }],
|
|
48
|
+
"requestBody": {
|
|
49
|
+
"contentType": "application/json",
|
|
50
|
+
"schema": {
|
|
51
|
+
"fieldName": { "type": "string", "example": "value", "required": true, "description": "..." }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"responses": {
|
|
55
|
+
"200": { "description": "Success", "example": {} },
|
|
56
|
+
"400": { "description": "Bad Request" },
|
|
57
|
+
"401": { "description": "Unauthorized" },
|
|
58
|
+
"404": { "description": "Not Found" },
|
|
59
|
+
"500": { "description": "Server Error" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}`;
|
|
66
|
+
const SYSTEM_INCREMENTAL = `You are an expert API analyst doing an INCREMENTAL update to an existing API spec.
|
|
67
|
+
|
|
68
|
+
You understand JavaScript AND TypeScript including Express, NestJS decorators, DTOs, Zod schemas, Mongoose/TypeORM/Prisma models.
|
|
69
|
+
|
|
70
|
+
You will receive:
|
|
71
|
+
1. The EXISTING spec (full JSON)
|
|
72
|
+
2. NEW or CHANGED source files to analyze
|
|
73
|
+
|
|
74
|
+
Your job:
|
|
75
|
+
- Extract endpoints from new/changed files
|
|
76
|
+
- If endpoint already exists (same method + path): update it if code changed, keep it if unchanged
|
|
77
|
+
- If it is NEW: add it to the correct collection (create collection if needed)
|
|
78
|
+
- Remove endpoints whose source files are listed under REMOVED FILES
|
|
79
|
+
- Keep all existing endpoints from unchanged files
|
|
80
|
+
|
|
81
|
+
Return the COMPLETE updated spec JSON. No markdown, no explanation, ONLY JSON.`;
|
|
82
|
+
async function callGroq(systemPrompt, userMessage) {
|
|
83
|
+
const apiKey = process.env.GROQ_API_KEY;
|
|
84
|
+
if (!apiKey) {
|
|
85
|
+
throw new Error("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");
|
|
86
|
+
}
|
|
87
|
+
const response = await fetch(GROQ_API_URL, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
Authorization: `Bearer ${apiKey}`,
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
model: MODEL,
|
|
95
|
+
temperature: 0.1,
|
|
96
|
+
max_tokens: 8000,
|
|
97
|
+
messages: [
|
|
98
|
+
{ role: "system", content: systemPrompt },
|
|
99
|
+
{ role: "user", content: userMessage },
|
|
100
|
+
],
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const err = await response.text();
|
|
105
|
+
throw new Error(`Groq API error ${response.status}: ${err}`);
|
|
106
|
+
}
|
|
107
|
+
const data = (await response.json());
|
|
108
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
109
|
+
}
|
|
110
|
+
function parseJSON(raw) {
|
|
111
|
+
const cleaned = raw
|
|
112
|
+
.trim()
|
|
113
|
+
.replace(/^```json\s*/i, "")
|
|
114
|
+
.replace(/^```\s*/i, "")
|
|
115
|
+
.replace(/```\s*$/i, "")
|
|
116
|
+
.trim();
|
|
117
|
+
const start = cleaned.indexOf("{");
|
|
118
|
+
const end = cleaned.lastIndexOf("}");
|
|
119
|
+
if (start === -1 || end === -1) {
|
|
120
|
+
throw new Error(`No JSON object found in response.\n\nSnippet: ${raw.slice(0, 300)}`);
|
|
121
|
+
}
|
|
122
|
+
const jsonStr = cleaned.slice(start, end + 1);
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(jsonStr);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
throw new Error(`Invalid JSON from Groq: ${err.message}\n\nSnippet: ${jsonStr.slice(0, 300)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function batchFiles(files) {
|
|
131
|
+
const batches = [];
|
|
132
|
+
let current = [];
|
|
133
|
+
let size = 0;
|
|
134
|
+
for (const f of files) {
|
|
135
|
+
const len = f.content.length + f.path.length + 20;
|
|
136
|
+
if (size + len > MAX_CHARS && current.length > 0) {
|
|
137
|
+
batches.push(current);
|
|
138
|
+
current = [];
|
|
139
|
+
size = 0;
|
|
140
|
+
}
|
|
141
|
+
const truncated = { ...f, content: f.content.slice(0, MAX_CHARS) };
|
|
142
|
+
current.push(truncated);
|
|
143
|
+
size += len;
|
|
144
|
+
}
|
|
145
|
+
if (current.length > 0)
|
|
146
|
+
batches.push(current);
|
|
147
|
+
return batches;
|
|
148
|
+
}
|
|
149
|
+
function mergeSpecs(specs) {
|
|
150
|
+
const base = specs[0];
|
|
151
|
+
const collectionsMap = {};
|
|
152
|
+
for (const spec of specs) {
|
|
153
|
+
for (const col of spec.collections) {
|
|
154
|
+
if (!collectionsMap[col.name]) {
|
|
155
|
+
collectionsMap[col.name] = { ...col, endpoints: [] };
|
|
156
|
+
}
|
|
157
|
+
for (const ep of col.endpoints) {
|
|
158
|
+
const key = `${ep.method}:${ep.path}`;
|
|
159
|
+
const exists = collectionsMap[col.name].endpoints.some((e) => `${e.method}:${e.path}` === key);
|
|
160
|
+
if (!exists)
|
|
161
|
+
collectionsMap[col.name].endpoints.push(ep);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { ...base, collections: Object.values(collectionsMap) };
|
|
166
|
+
}
|
|
167
|
+
function sleep(ms) {
|
|
168
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
169
|
+
}
|
|
170
|
+
export async function analyzeWithAI(collected, projectName = "API") {
|
|
171
|
+
const allFiles = [
|
|
172
|
+
...collected.routes.map((f) => ({ ...f, kind: "ROUTE" })),
|
|
173
|
+
...collected.controllers.map((f) => ({ ...f, kind: "CONTROLLER" })),
|
|
174
|
+
...collected.services.map((f) => ({ ...f, kind: "SERVICE" })),
|
|
175
|
+
...collected.models.map((f) => ({ ...f, kind: "MODEL" })),
|
|
176
|
+
];
|
|
177
|
+
const batches = batchFiles(allFiles);
|
|
178
|
+
console.log(`\n 📦 Sending ${batches.length} batch(es) to Groq (${allFiles.length} files total)...`);
|
|
179
|
+
const partialSpecs = [];
|
|
180
|
+
for (let i = 0; i < batches.length; i++) {
|
|
181
|
+
const batch = batches[i];
|
|
182
|
+
const context = batch
|
|
183
|
+
.map((f) => `### [${f.kind}] ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
|
|
184
|
+
.join("\n\n");
|
|
185
|
+
const userMessage = `Project name: "${projectName}" (batch ${i + 1} of ${batches.length})\n\n${context}\n\nExtract all API endpoints found in these files and return ONLY the JSON spec.`;
|
|
186
|
+
const raw = await callGroq(SYSTEM_FULL, userMessage);
|
|
187
|
+
const partial = parseJSON(raw);
|
|
188
|
+
partialSpecs.push(partial);
|
|
189
|
+
if (i < batches.length - 1)
|
|
190
|
+
await sleep(1000);
|
|
191
|
+
}
|
|
192
|
+
return batches.length === 1 ? partialSpecs[0] : mergeSpecs(partialSpecs);
|
|
193
|
+
}
|
|
194
|
+
export async function analyzeIncremental(existingSpec, diff) {
|
|
195
|
+
const { newFiles, changedFiles, removedFiles } = diff;
|
|
196
|
+
if (!newFiles.length && !changedFiles.length && !removedFiles.length) {
|
|
197
|
+
return { spec: existingSpec, changed: false };
|
|
198
|
+
}
|
|
199
|
+
const filesToAnalyze = [...newFiles, ...changedFiles];
|
|
200
|
+
const removedContext = removedFiles.length > 0
|
|
201
|
+
? `\n\nREMOVED FILES (delete their endpoints):\n${removedFiles.join("\n")}`
|
|
202
|
+
: "";
|
|
203
|
+
const specStr = JSON.stringify(existingSpec, null, 2);
|
|
204
|
+
const specTruncated = specStr.length > 6000 ? specStr.slice(0, 6000) + "\n... [truncated]" : specStr;
|
|
205
|
+
const changedContext = filesToAnalyze
|
|
206
|
+
.map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\``)
|
|
207
|
+
.join("\n\n");
|
|
208
|
+
const userMessage = `EXISTING SPEC:\n${specTruncated}\n\nNEW/CHANGED FILES:\n${changedContext}${removedContext}\n\nReturn the complete updated spec JSON only.`;
|
|
209
|
+
const raw = await callGroq(SYSTEM_INCREMENTAL, userMessage);
|
|
210
|
+
const updated = parseJSON(raw);
|
|
211
|
+
return { spec: updated, changed: true };
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=agent.groq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.groq.js","sourceRoot":"","sources":["../src/agent.groq.ts"],"names":[],"mappings":"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"}
|
package/{src → dist}/agent.js
RENAMED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
|
|
3
2
|
const client = new Anthropic();
|
|
4
|
-
|
|
5
3
|
function buildContext(files) {
|
|
6
|
-
|
|
4
|
+
return files.map((f) => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``).join("\n\n");
|
|
7
5
|
}
|
|
8
|
-
|
|
9
6
|
const SYSTEM_FULL = `You are an expert API analyst. Analyze source code (JavaScript OR TypeScript) and extract a complete API specification.
|
|
10
7
|
|
|
11
8
|
Supported frameworks and patterns — recognize ALL of these:
|
|
@@ -64,7 +61,6 @@ Return this exact structure:
|
|
|
64
61
|
}
|
|
65
62
|
]
|
|
66
63
|
}`;
|
|
67
|
-
|
|
68
64
|
const SYSTEM_INCREMENTAL = `You are an expert API analyst doing an INCREMENTAL update to an existing API spec.
|
|
69
65
|
|
|
70
66
|
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.
|
|
@@ -83,73 +79,81 @@ Your job:
|
|
|
83
79
|
- Keep all existing endpoints from files that weren't changed
|
|
84
80
|
|
|
85
81
|
Return the COMPLETE updated spec JSON (same structure as input). No markdown, no explanation, only JSON.`;
|
|
86
|
-
|
|
82
|
+
function parseJSON(raw) {
|
|
83
|
+
const cleaned = raw
|
|
84
|
+
.trim()
|
|
85
|
+
.replace(/^```json\s*/i, "")
|
|
86
|
+
.replace(/^```\s*/i, "")
|
|
87
|
+
.replace(/```\s*$/i, "")
|
|
88
|
+
.trim();
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(cleaned);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
throw new Error(`AI returned invalid JSON: ${err.message}\n\nSnippet: ${raw.slice(0, 300)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
87
96
|
export async function analyzeWithAI(collected, projectName = "API") {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
97
|
+
const context = [
|
|
98
|
+
collected.routes.length > 0
|
|
99
|
+
? "## ROUTES\n" + buildContext(collected.routes)
|
|
100
|
+
: null,
|
|
101
|
+
collected.controllers.length > 0
|
|
102
|
+
? "## CONTROLLERS\n" + buildContext(collected.controllers)
|
|
103
|
+
: null,
|
|
104
|
+
collected.services.length > 0
|
|
105
|
+
? "## SERVICES\n" + buildContext(collected.services)
|
|
106
|
+
: null,
|
|
107
|
+
collected.models.length > 0
|
|
108
|
+
? "## MODELS\n" + buildContext(collected.models)
|
|
109
|
+
: null,
|
|
110
|
+
]
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join("\n\n---\n\n");
|
|
113
|
+
const response = await client.messages.create({
|
|
114
|
+
model: "claude-opus-4-5",
|
|
115
|
+
max_tokens: 8000,
|
|
116
|
+
system: SYSTEM_FULL,
|
|
117
|
+
messages: [
|
|
118
|
+
{
|
|
119
|
+
role: "user",
|
|
120
|
+
content: `Project: "${projectName}"\n\n${context}\n\nExtract all API endpoints and return the JSON spec.`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
const block = response.content[0];
|
|
125
|
+
if (block.type !== "text") {
|
|
126
|
+
throw new Error("Unexpected response type from Anthropic API");
|
|
127
|
+
}
|
|
128
|
+
return parseJSON(block.text);
|
|
113
129
|
}
|
|
114
|
-
|
|
115
130
|
export async function analyzeIncremental(existingSpec, diff) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
131
|
+
const { newFiles, changedFiles, removedFiles } = diff;
|
|
132
|
+
if (!newFiles.length && !changedFiles.length && !removedFiles.length) {
|
|
133
|
+
return { spec: existingSpec, changed: false };
|
|
134
|
+
}
|
|
135
|
+
const changedContext = [...newFiles, ...changedFiles]
|
|
136
|
+
.map((f) => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
|
|
137
|
+
.join("\n\n");
|
|
138
|
+
const removedContext = removedFiles.length > 0
|
|
139
|
+
? `\n\nREMOVED FILES (delete their endpoints):\n${removedFiles.join("\n")}`
|
|
140
|
+
: "";
|
|
141
|
+
const response = await client.messages.create({
|
|
142
|
+
model: "claude-opus-4-5",
|
|
143
|
+
max_tokens: 8000,
|
|
144
|
+
system: SYSTEM_INCREMENTAL,
|
|
145
|
+
messages: [
|
|
146
|
+
{
|
|
147
|
+
role: "user",
|
|
148
|
+
content: `EXISTING SPEC:\n${JSON.stringify(existingSpec, null, 2)}\n\nNEW/CHANGED FILES:\n${changedContext}${removedContext}\n\nReturn the complete updated spec.`,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
const block = response.content[0];
|
|
153
|
+
if (block.type !== "text") {
|
|
154
|
+
throw new Error("Unexpected response type from Anthropic API");
|
|
155
|
+
}
|
|
156
|
+
const updated = parseJSON(block.text);
|
|
157
|
+
return { spec: updated, changed: true };
|
|
142
158
|
}
|
|
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
|
-
}
|
|
159
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkgPath = path.join(__dirname, "../package.json");
|
|
12
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
13
|
+
function banner() {
|
|
14
|
+
console.log();
|
|
15
|
+
console.log(chalk.green.bold(" 🦕 DINOREX") + chalk.dim(" v" + pkg.version));
|
|
16
|
+
console.log(chalk.dim(" AI-powered API documentation — one command, full docs."));
|
|
17
|
+
console.log();
|
|
18
|
+
}
|
|
19
|
+
function checkApiKey(options) {
|
|
20
|
+
const isAnthropic = options.provider === "anthropic" ||
|
|
21
|
+
process.env.DINOREX_PROVIDER === "anthropic";
|
|
22
|
+
if (isAnthropic) {
|
|
23
|
+
if (options.apiKey)
|
|
24
|
+
process.env.ANTHROPIC_API_KEY = options.apiKey;
|
|
25
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
26
|
+
console.log(chalk.red(" ✗ ANTHROPIC_API_KEY is not set.\n"));
|
|
27
|
+
console.log(chalk.dim(" Option 1 — env var: export ANTHROPIC_API_KEY=sk-ant-..."));
|
|
28
|
+
console.log(chalk.dim(" Option 2 — flag: dinorex scan --provider anthropic --api-key sk-ant-..."));
|
|
29
|
+
console.log();
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (options.apiKey)
|
|
35
|
+
process.env.GROQ_API_KEY = options.apiKey;
|
|
36
|
+
if (!process.env.GROQ_API_KEY) {
|
|
37
|
+
console.log(chalk.red(" ✗ GROQ_API_KEY is not set.\n"));
|
|
38
|
+
console.log(chalk.dim(" Get a free key at: https://console.groq.com"));
|
|
39
|
+
console.log(chalk.dim(" Then: export GROQ_API_KEY=gsk_your_key_here"));
|
|
40
|
+
console.log(chalk.dim(" Or: dinorex scan --api-key gsk_..."));
|
|
41
|
+
console.log(chalk.dim(" To use Anthropic instead: dinorex scan --provider anthropic --api-key sk-ant-..."));
|
|
42
|
+
console.log();
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ── dinorex scan [dir] ────────────────────────────────────────────────────
|
|
48
|
+
program
|
|
49
|
+
.name("dinorex")
|
|
50
|
+
.version(pkg.version)
|
|
51
|
+
.description("AI-powered API documentation generator");
|
|
52
|
+
program
|
|
53
|
+
.command("scan [directory]")
|
|
54
|
+
.alias("init")
|
|
55
|
+
.description("Scan a project and launch the interactive docs UI")
|
|
56
|
+
.option("-p, --port <port>", "Port for the docs server", "4321")
|
|
57
|
+
.option("--no-open", "Skip auto-opening browser")
|
|
58
|
+
.option("--api-key <key>", "API key (Groq or Anthropic)")
|
|
59
|
+
.option("--provider <name>", "AI provider: groq (default) or anthropic")
|
|
60
|
+
.action(async (directory = ".", options) => {
|
|
61
|
+
banner();
|
|
62
|
+
checkApiKey(options);
|
|
63
|
+
const targetDir = path.resolve(directory);
|
|
64
|
+
const port = parseInt(options.port, 10);
|
|
65
|
+
const isAnthropic = options.provider === "anthropic" || process.env.DINOREX_PROVIDER === "anthropic";
|
|
66
|
+
const { scanProject } = await import("./scanner.js");
|
|
67
|
+
const agentFile = isAnthropic ? "./agent.js" : "./agent.groq.js";
|
|
68
|
+
const { analyzeWithAI, analyzeIncremental } = await import(agentFile);
|
|
69
|
+
const { startServer } = await import("./server.js");
|
|
70
|
+
const { loadStore, saveStore, diffScan } = await import("./store.js");
|
|
71
|
+
console.log(chalk.dim(` 📂 ${targetDir}`));
|
|
72
|
+
const providerLabel = isAnthropic ? "anthropic" : "groq (free)";
|
|
73
|
+
console.log(chalk.dim(` 🤖 Provider: ${providerLabel}\n`));
|
|
74
|
+
// ── 1. Scan ──
|
|
75
|
+
const s1 = ora({ text: "Scanning project files…", color: "green" }).start();
|
|
76
|
+
let scanResult;
|
|
77
|
+
try {
|
|
78
|
+
scanResult = await scanProject(targetDir);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
s1.fail(chalk.red(e.message));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const { summary, collected } = scanResult;
|
|
85
|
+
const total = Object.values(summary).reduce((a, b) => a + b, 0);
|
|
86
|
+
if (!total) {
|
|
87
|
+
s1.fail(chalk.red("No API files found. Make sure you're in an Express/Nest/Fastify project."));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
s1.succeed(chalk.green("Discovered: ") +
|
|
91
|
+
chalk.white(`${summary.routes} routes ${summary.controllers} controllers ${summary.services} services ${summary.models} models`));
|
|
92
|
+
// ── 2. AI analysis (full or incremental) ──
|
|
93
|
+
const stored = loadStore(targetDir);
|
|
94
|
+
const allFiles = [
|
|
95
|
+
...collected.routes,
|
|
96
|
+
...collected.controllers,
|
|
97
|
+
...collected.services,
|
|
98
|
+
...collected.models,
|
|
99
|
+
];
|
|
100
|
+
let spec;
|
|
101
|
+
if (stored?.spec && stored?.hashes) {
|
|
102
|
+
const diff = diffScan(stored.hashes, allFiles);
|
|
103
|
+
const hasChanges = diff.newFiles.length || diff.changedFiles.length || diff.removedFiles.length;
|
|
104
|
+
if (hasChanges) {
|
|
105
|
+
const s2 = ora({
|
|
106
|
+
text: `Incremental update — ${diff.newFiles.length} new, ${diff.changedFiles.length} changed files…`,
|
|
107
|
+
color: "cyan",
|
|
108
|
+
}).start();
|
|
109
|
+
try {
|
|
110
|
+
const result = await analyzeIncremental(stored.spec, diff);
|
|
111
|
+
spec = result.spec;
|
|
112
|
+
const hashes = {};
|
|
113
|
+
for (const f of allFiles) {
|
|
114
|
+
hashes[f.path] = { hash: createHash("md5").update(f.content).digest("hex") };
|
|
115
|
+
}
|
|
116
|
+
saveStore(targetDir, { spec, hashes, lastScan: new Date().toISOString() });
|
|
117
|
+
s2.succeed(chalk.cyan("Incremental update complete."));
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
s2.fail(chalk.red(e.message));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
spec = stored.spec;
|
|
126
|
+
console.log(chalk.dim(" ✓ No changes since last scan — using cached spec."));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const s2 = ora({ text: "Running full AI analysis (~15s)…", color: "cyan" }).start();
|
|
131
|
+
try {
|
|
132
|
+
spec = await analyzeWithAI(collected, path.basename(targetDir));
|
|
133
|
+
const hashes = {};
|
|
134
|
+
for (const f of allFiles) {
|
|
135
|
+
hashes[f.path] = { hash: createHash("md5").update(f.content).digest("hex") };
|
|
136
|
+
}
|
|
137
|
+
saveStore(targetDir, { spec, hashes, lastScan: new Date().toISOString() });
|
|
138
|
+
s2.succeed(chalk.cyan("Analysis complete — ") +
|
|
139
|
+
chalk.white(`${spec.collections.reduce((a, c) => a + c.endpoints.length, 0)} endpoints across ${spec.collections.length} collections`));
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
s2.fail(chalk.red(e.message));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ── 3. Start server ──
|
|
147
|
+
const s3 = ora({ text: `Starting docs server on :${port}…`, color: "yellow" }).start();
|
|
148
|
+
let srv;
|
|
149
|
+
try {
|
|
150
|
+
srv = await startServer(targetDir, { port, _cachedSpec: spec });
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
s3.fail(chalk.red(e.message));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
s3.succeed(chalk.yellow("Docs server running!"));
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(chalk.green.bold(` ✓ Dinorex docs → ${srv.url}`));
|
|
159
|
+
console.log();
|
|
160
|
+
console.log(chalk.dim(" Tip: run dinorex scan again anytime to pick up new endpoints."));
|
|
161
|
+
console.log(chalk.dim(" Ctrl+C to stop.\n"));
|
|
162
|
+
if (options.open !== false) {
|
|
163
|
+
try {
|
|
164
|
+
const cmd = process.platform === "darwin"
|
|
165
|
+
? `open ${srv.url}`
|
|
166
|
+
: process.platform === "win32"
|
|
167
|
+
? `start ${srv.url}`
|
|
168
|
+
: `xdg-open ${srv.url}`;
|
|
169
|
+
execSync(cmd, { stdio: "ignore" });
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
}
|
|
173
|
+
process.on("SIGINT", () => {
|
|
174
|
+
console.log(chalk.dim("\n Dinorex stopped. 🦕\n"));
|
|
175
|
+
process.exit(0);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
program.parse();
|
|
179
|
+
//# sourceMappingURL=cli.js.map
|