devsmind-mcp 1.2.1 → 1.2.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/LICENSE +21 -21
- package/dist/cli/runner.js +225 -148
- package/dist/cli/runner.js.map +1 -1
- package/dist/db/database.d.ts +7 -1
- package/dist/db/database.js +60 -19
- package/dist/db/database.js.map +1 -1
- package/dist/db/schema.js +33 -33
- package/dist/mcp/server.js +5 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/visualizer_2d.html +635 -635
- package/dist/mcp/visualizer_3d.html +628 -628
- package/dist/utils/json.d.ts +16 -0
- package/dist/utils/json.js +151 -0
- package/dist/utils/json.js.map +1 -0
- package/package.json +7 -3
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Abialidr
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abialidr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/cli/runner.js
CHANGED
|
@@ -42,6 +42,7 @@ const crypto = __importStar(require("crypto"));
|
|
|
42
42
|
const database_1 = require("../db/database");
|
|
43
43
|
const indexer_1 = require("../db/indexer");
|
|
44
44
|
const scanner_1 = require("../utils/scanner");
|
|
45
|
+
const json_1 = require("../utils/json");
|
|
45
46
|
function makeHttpRequest(urlStr, method, headers, body) {
|
|
46
47
|
return new Promise((resolve, reject) => {
|
|
47
48
|
const isHttps = urlStr.startsWith('https');
|
|
@@ -104,12 +105,11 @@ function makeBar(done, total) {
|
|
|
104
105
|
function truncate(s, max) {
|
|
105
106
|
return s.length > max ? '…' + s.slice(-(max - 1)) : s;
|
|
106
107
|
}
|
|
108
|
+
const MAX_LOG_LINES = 18;
|
|
107
109
|
class ProgressDisplay {
|
|
108
110
|
isTTY = IS_TTY;
|
|
109
|
-
lineCount = 0;
|
|
110
111
|
spinIdx = 0;
|
|
111
112
|
// phase state
|
|
112
|
-
phaseLabel = '';
|
|
113
113
|
phaseNum = 0;
|
|
114
114
|
totalPhases = 2;
|
|
115
115
|
total = 0;
|
|
@@ -118,36 +118,40 @@ class ProgressDisplay {
|
|
|
118
118
|
itemStart = 0;
|
|
119
119
|
times = [];
|
|
120
120
|
currentItem = '';
|
|
121
|
-
|
|
121
|
+
statusLine = '';
|
|
122
|
+
// scrolling log ring buffer
|
|
123
|
+
logLines = [];
|
|
124
|
+
// total lines currently drawn on screen (log + bar)
|
|
125
|
+
drawnLines = 0;
|
|
122
126
|
startPhase(phaseNum, label, total, alreadyDone = 0) {
|
|
123
127
|
this.phaseNum = phaseNum;
|
|
124
|
-
this.phaseLabel = label;
|
|
125
128
|
this.total = total;
|
|
126
|
-
this.done = alreadyDone;
|
|
129
|
+
this.done = alreadyDone;
|
|
127
130
|
this.times = [];
|
|
128
131
|
this.phaseStart = Date.now();
|
|
129
132
|
this.currentItem = alreadyDone > 0 ? `Resuming from item ${alreadyDone + 1}…` : 'Starting…';
|
|
130
|
-
this.
|
|
131
|
-
this.
|
|
133
|
+
this.statusLine = '';
|
|
134
|
+
this.logLines = [];
|
|
135
|
+
this.drawnLines = 0;
|
|
132
136
|
if (!this.isTTY) {
|
|
133
|
-
console.log(`\n${'═'.repeat(
|
|
137
|
+
console.log(`\n${'═'.repeat(60)}`);
|
|
134
138
|
console.log(` Phase ${phaseNum}/${this.totalPhases}: ${label}`);
|
|
135
|
-
console.log(`${'═'.repeat(
|
|
136
|
-
if (alreadyDone > 0)
|
|
139
|
+
console.log(`${'═'.repeat(60)}`);
|
|
140
|
+
if (alreadyDone > 0)
|
|
137
141
|
console.log(` Resuming: ${alreadyDone}/${total} already done`);
|
|
138
|
-
}
|
|
139
142
|
console.log(` Remaining: ${total - alreadyDone} item(s) to process`);
|
|
140
|
-
console.log(`${'─'.repeat(
|
|
143
|
+
console.log(`${'─'.repeat(60)}\n`);
|
|
141
144
|
}
|
|
142
145
|
else {
|
|
143
146
|
const resumeTag = alreadyDone > 0 ? ` \x1B[90m(resuming from ${alreadyDone}/${total})\x1B[0m` : '';
|
|
144
|
-
process.stdout.write(`\n \x1B[1m\x1B[36mPhase ${phaseNum}/${this.totalPhases}: ${label}\x1B[0m${resumeTag}\n
|
|
147
|
+
process.stdout.write(`\n \x1B[1m\x1B[36mPhase ${phaseNum}/${this.totalPhases}: ${label}\x1B[0m${resumeTag}\n`);
|
|
145
148
|
this._render();
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
beginItem(name) {
|
|
149
152
|
this.currentItem = name;
|
|
150
153
|
this.itemStart = Date.now();
|
|
154
|
+
this.statusLine = '';
|
|
151
155
|
this.spinIdx = (this.spinIdx + 1) % SPINNER_FRAMES.length;
|
|
152
156
|
if (this.isTTY)
|
|
153
157
|
this._render();
|
|
@@ -158,7 +162,7 @@ class ProgressDisplay {
|
|
|
158
162
|
const t = Date.now() - this.itemStart;
|
|
159
163
|
this.times.push(t);
|
|
160
164
|
this.done++;
|
|
161
|
-
this.
|
|
165
|
+
this.statusLine = extra;
|
|
162
166
|
if (this.isTTY)
|
|
163
167
|
this._render();
|
|
164
168
|
else
|
|
@@ -166,16 +170,37 @@ class ProgressDisplay {
|
|
|
166
170
|
}
|
|
167
171
|
skipItem(reason) {
|
|
168
172
|
this.done++;
|
|
169
|
-
this.
|
|
173
|
+
this.statusLine = `skip — ${reason}`;
|
|
174
|
+
if (this.isTTY)
|
|
175
|
+
this._render();
|
|
176
|
+
else
|
|
177
|
+
console.log(` skip — ${reason}`);
|
|
178
|
+
}
|
|
179
|
+
updateStatus(msg) {
|
|
180
|
+
this.statusLine = msg;
|
|
170
181
|
if (this.isTTY)
|
|
171
182
|
this._render();
|
|
172
183
|
else
|
|
173
|
-
console.log(`
|
|
184
|
+
console.log(` ... ${msg}`);
|
|
185
|
+
}
|
|
186
|
+
/** Push a log line into the scrolling log panel and re-render */
|
|
187
|
+
log(msg) {
|
|
188
|
+
const ts = new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
189
|
+
this.logLines.push(` \x1B[90m${ts}\x1B[0m ${msg}`);
|
|
190
|
+
if (this.logLines.length > MAX_LOG_LINES) {
|
|
191
|
+
this.logLines.shift();
|
|
192
|
+
}
|
|
193
|
+
if (this.isTTY) {
|
|
194
|
+
this._render();
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
console.log(` ${msg}`);
|
|
198
|
+
}
|
|
174
199
|
}
|
|
175
200
|
_render() {
|
|
176
|
-
// Clear previously
|
|
177
|
-
if (this.
|
|
178
|
-
process.stdout.write(`\x1B[${this.
|
|
201
|
+
// Clear all previously drawn lines
|
|
202
|
+
if (this.drawnLines > 0) {
|
|
203
|
+
process.stdout.write(`\x1B[${this.drawnLines}A\x1B[0J`);
|
|
179
204
|
}
|
|
180
205
|
const pct = this.total > 0 ? (this.done / this.total) * 100 : 0;
|
|
181
206
|
const bar = makeBar(this.done, this.total);
|
|
@@ -186,24 +211,33 @@ class ProgressDisplay {
|
|
|
186
211
|
const remaining = this.total - this.done;
|
|
187
212
|
const eta = avg > 0 && remaining > 0 ? avg * remaining : 0;
|
|
188
213
|
const spin = SPINNER_FRAMES[this.spinIdx];
|
|
189
|
-
const itemShort = truncate(this.currentItem,
|
|
214
|
+
const itemShort = truncate(this.currentItem, 64);
|
|
190
215
|
const pctStr = `${Math.round(pct)}%`.padStart(4);
|
|
191
216
|
const doneStr = `${this.done}/${this.total}`;
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
// ── Log panel (scrolling lines) ──────────────────────────────────
|
|
218
|
+
const logSection = this.logLines.length > 0
|
|
219
|
+
? [
|
|
220
|
+
` \x1B[90m${'─'.repeat(60)}\x1B[0m`,
|
|
221
|
+
...this.logLines,
|
|
222
|
+
` \x1B[90m${'─'.repeat(60)}\x1B[0m`,
|
|
223
|
+
]
|
|
224
|
+
: [];
|
|
225
|
+
// ── Progress bar (fixed) ─────────────────────────────────────────
|
|
226
|
+
const barSection = [
|
|
227
|
+
` ${spin} ${bar} ${doneStr.padEnd(11)} ${pctStr}`,
|
|
194
228
|
` ⏱ Elapsed : \x1B[33m${fmtMs(elapsed)}\x1B[0m ETA : \x1B[32m${eta > 0 ? '~' + fmtMs(eta) : remaining > 0 ? 'calculating…' : 'done!'}\x1B[0m`,
|
|
195
|
-
` ⚡ Avg/item: \x1B[35m${avg > 0 ? fmtMs(avg) : '—'}\x1B[0m
|
|
196
|
-
` ▶ \x1B[
|
|
197
|
-
|
|
229
|
+
` ⚡ Avg/item: \x1B[35m${avg > 0 ? fmtMs(avg) : '—'}\x1B[0m${this.statusLine ? ` \x1B[33m${truncate(this.statusLine, 40)}\x1B[0m` : ''}`,
|
|
230
|
+
` ▶ \x1B[96m${itemShort}\x1B[0m`,
|
|
231
|
+
``,
|
|
198
232
|
];
|
|
199
|
-
|
|
200
|
-
|
|
233
|
+
const all = [...logSection, ...barSection];
|
|
234
|
+
process.stdout.write(all.join('\n'));
|
|
235
|
+
this.drawnLines = all.length;
|
|
201
236
|
}
|
|
202
237
|
finishPhase(summary) {
|
|
203
|
-
if (this.isTTY && this.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
this.lineCount = 0;
|
|
238
|
+
if (this.isTTY && this.drawnLines > 0) {
|
|
239
|
+
process.stdout.write(`\x1B[${this.drawnLines}A\x1B[0J`);
|
|
240
|
+
this.drawnLines = 0;
|
|
207
241
|
}
|
|
208
242
|
const elapsed = Date.now() - this.phaseStart;
|
|
209
243
|
const avg = this.times.length > 0
|
|
@@ -214,20 +248,20 @@ class ProgressDisplay {
|
|
|
214
248
|
}
|
|
215
249
|
}
|
|
216
250
|
// Build standard taxonomy prompt text
|
|
217
|
-
const TAXONOMY_PROMPT = `
|
|
218
|
-
Choose node types from this taxonomy:
|
|
219
|
-
- UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator
|
|
220
|
-
- NESTJS: nest_module | nest_controller | nest_service | nest_provider | nest_guard | nest_interceptor | nest_pipe | nest_filter | nest_decorator | nest_middleware | nest_gateway | nest_resolver | nest_schema | nest_dto
|
|
221
|
-
- EXPRESS/FASTIFY: route_handler | middleware | router
|
|
222
|
-
- SPRING (Java): spring_controller | spring_service | spring_repository | spring_component | spring_bean | spring_config | spring_entity
|
|
223
|
-
- DJANGO/FASTAPI: django_view | django_model | django_serializer | django_form | django_signal | fastapi_router | fastapi_dependency
|
|
224
|
-
- GO: go_handler | go_middleware | go_struct | go_interface | go_func
|
|
225
|
-
- RUST: rust_struct | rust_impl | rust_trait | rust_enum | rust_fn | rust_macro
|
|
226
|
-
- REACT/NEXTJS: react_component | react_hook | react_context | react_hoc | react_page | next_page | next_layout | next_api_route | next_server_action
|
|
227
|
-
- ORM: prisma_model | typeorm_entity | mongoose_model | sqlalchemy_model
|
|
228
|
-
- REST/API/GRAPHQL: api_endpoint | rest_controller | graphql_resolver | graphql_query | graphql_mutation | graphql_schema
|
|
229
|
-
- CLI: cli_command | cli_option
|
|
230
|
-
- UTILITY: util_function | helper | validator | formatter
|
|
251
|
+
const TAXONOMY_PROMPT = `
|
|
252
|
+
Choose node types from this taxonomy:
|
|
253
|
+
- UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator
|
|
254
|
+
- NESTJS: nest_module | nest_controller | nest_service | nest_provider | nest_guard | nest_interceptor | nest_pipe | nest_filter | nest_decorator | nest_middleware | nest_gateway | nest_resolver | nest_schema | nest_dto
|
|
255
|
+
- EXPRESS/FASTIFY: route_handler | middleware | router
|
|
256
|
+
- SPRING (Java): spring_controller | spring_service | spring_repository | spring_component | spring_bean | spring_config | spring_entity
|
|
257
|
+
- DJANGO/FASTAPI: django_view | django_model | django_serializer | django_form | django_signal | fastapi_router | fastapi_dependency
|
|
258
|
+
- GO: go_handler | go_middleware | go_struct | go_interface | go_func
|
|
259
|
+
- RUST: rust_struct | rust_impl | rust_trait | rust_enum | rust_fn | rust_macro
|
|
260
|
+
- REACT/NEXTJS: react_component | react_hook | react_context | react_hoc | react_page | next_page | next_layout | next_api_route | next_server_action
|
|
261
|
+
- ORM: prisma_model | typeorm_entity | mongoose_model | sqlalchemy_model
|
|
262
|
+
- REST/API/GRAPHQL: api_endpoint | rest_controller | graphql_resolver | graphql_query | graphql_mutation | graphql_schema
|
|
263
|
+
- CLI: cli_command | cli_option
|
|
264
|
+
- UTILITY: util_function | helper | validator | formatter
|
|
231
265
|
`;
|
|
232
266
|
// ── Vertex AI Authentication & Helper Functions ───────────────────────────
|
|
233
267
|
function base64UrlEncode(obj) {
|
|
@@ -319,24 +353,24 @@ async function getVertexTokenCached(saData) {
|
|
|
319
353
|
}
|
|
320
354
|
async function extractWithVertex(model, token, projectId, location, filePath, code) {
|
|
321
355
|
const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:generateContent`;
|
|
322
|
-
const systemPrompt = `You are a codebase indexing assistant. Your job is to analyze the source code file provided and extract all code structures (functions, methods, classes, controllers, services, interfaces, schema models, types) defined in the file.
|
|
323
|
-
Return ONLY a valid JSON object matching the schema:
|
|
324
|
-
{
|
|
325
|
-
"nodes": [
|
|
326
|
-
{
|
|
327
|
-
"node_id": "fully_qualified_identifier (e.g. Class.method or function)",
|
|
328
|
-
"name": "display_name",
|
|
329
|
-
"type": "type_from_taxonomy",
|
|
330
|
-
"signature": "param/return signature (optional)",
|
|
331
|
-
"code_snapshot": "the exact full source code block of this entity"
|
|
332
|
-
}
|
|
333
|
-
]
|
|
334
|
-
}
|
|
335
|
-
${TAXONOMY_PROMPT}
|
|
336
|
-
CRITICAL RULES:
|
|
337
|
-
1. ONLY extract code structures defined in the file. Do NOT extract imports or third-party libraries as nodes.
|
|
338
|
-
2. For each node, extract its exact code snippet as "code_snapshot".
|
|
339
|
-
3. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
|
|
356
|
+
const systemPrompt = `You are a codebase indexing assistant. Your job is to analyze the source code file provided and extract all code structures (functions, methods, classes, controllers, services, interfaces, schema models, types) defined in the file.
|
|
357
|
+
Return ONLY a valid JSON object matching the schema:
|
|
358
|
+
{
|
|
359
|
+
"nodes": [
|
|
360
|
+
{
|
|
361
|
+
"node_id": "fully_qualified_identifier (e.g. Class.method or function)",
|
|
362
|
+
"name": "display_name",
|
|
363
|
+
"type": "type_from_taxonomy",
|
|
364
|
+
"signature": "param/return signature (optional)",
|
|
365
|
+
"code_snapshot": "the exact full source code block of this entity"
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
${TAXONOMY_PROMPT}
|
|
370
|
+
CRITICAL RULES:
|
|
371
|
+
1. ONLY extract code structures defined in the file. Do NOT extract imports or third-party libraries as nodes.
|
|
372
|
+
2. For each node, extract its exact code snippet as "code_snapshot".
|
|
373
|
+
3. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
|
|
340
374
|
4. Be highly precise and return an empty JSON object if no code constructs are found.`;
|
|
341
375
|
const payload = {
|
|
342
376
|
contents: [
|
|
@@ -364,27 +398,27 @@ CRITICAL RULES:
|
|
|
364
398
|
'Content-Type': 'application/json',
|
|
365
399
|
'Authorization': `Bearer ${token}`
|
|
366
400
|
}, JSON.stringify(payload));
|
|
367
|
-
const parsed =
|
|
401
|
+
const parsed = (0, json_1.safeJsonParse)(responseText, {});
|
|
368
402
|
const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
369
403
|
if (!text) {
|
|
370
404
|
return {};
|
|
371
405
|
}
|
|
372
|
-
return
|
|
406
|
+
return (0, json_1.safeJsonParse)(text, {});
|
|
373
407
|
}
|
|
374
408
|
async function resolveConnectionsWithVertex(model, token, projectId, location, sourceNodeId, code, candidateNodeIds) {
|
|
375
409
|
const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:generateContent`;
|
|
376
|
-
const systemPrompt = `You are a codebase indexing assistant. Your job is to analyze the source code of a specific code entity and identify which other known code entities from the provided candidate list it calls or references.
|
|
377
|
-
Return ONLY a valid JSON object matching the schema:
|
|
378
|
-
{
|
|
379
|
-
"connections": [
|
|
380
|
-
"target_node_id_1",
|
|
381
|
-
"target_node_id_2"
|
|
382
|
-
]
|
|
383
|
-
}
|
|
384
|
-
CRITICAL RULES:
|
|
385
|
-
1. ONLY return target node IDs that are present in the provided list of known candidates. Do NOT invent new node IDs.
|
|
386
|
-
2. DO NOT include connections to third-party libraries, language built-ins, or the source node itself.
|
|
387
|
-
3. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
|
|
410
|
+
const systemPrompt = `You are a codebase indexing assistant. Your job is to analyze the source code of a specific code entity and identify which other known code entities from the provided candidate list it calls or references.
|
|
411
|
+
Return ONLY a valid JSON object matching the schema:
|
|
412
|
+
{
|
|
413
|
+
"connections": [
|
|
414
|
+
"target_node_id_1",
|
|
415
|
+
"target_node_id_2"
|
|
416
|
+
]
|
|
417
|
+
}
|
|
418
|
+
CRITICAL RULES:
|
|
419
|
+
1. ONLY return target node IDs that are present in the provided list of known candidates. Do NOT invent new node IDs.
|
|
420
|
+
2. DO NOT include connections to third-party libraries, language built-ins, or the source node itself.
|
|
421
|
+
3. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
|
|
388
422
|
4. If no connections are found, return an empty array.`;
|
|
389
423
|
const payload = {
|
|
390
424
|
contents: [
|
|
@@ -412,34 +446,34 @@ CRITICAL RULES:
|
|
|
412
446
|
'Content-Type': 'application/json',
|
|
413
447
|
'Authorization': `Bearer ${token}`
|
|
414
448
|
}, JSON.stringify(payload));
|
|
415
|
-
const parsed =
|
|
449
|
+
const parsed = (0, json_1.safeJsonParse)(responseText, {});
|
|
416
450
|
const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
417
451
|
if (!text) {
|
|
418
452
|
return [];
|
|
419
453
|
}
|
|
420
|
-
const result =
|
|
454
|
+
const result = (0, json_1.safeJsonParse)(text, {});
|
|
421
455
|
return result.connections || [];
|
|
422
456
|
}
|
|
423
457
|
async function extractWithGemini(model, key, filePath, code) {
|
|
424
458
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
|
|
425
|
-
const systemPrompt = `You are a codebase indexing assistant. Your job is to analyze the source code file provided and extract all code structures (functions, methods, classes, controllers, services, interfaces, schema models, types) defined in the file.
|
|
426
|
-
Return ONLY a valid JSON object matching the schema:
|
|
427
|
-
{
|
|
428
|
-
"nodes": [
|
|
429
|
-
{
|
|
430
|
-
"node_id": "fully_qualified_identifier (e.g. Class.method or function)",
|
|
431
|
-
"name": "display_name",
|
|
432
|
-
"type": "type_from_taxonomy",
|
|
433
|
-
"signature": "param/return signature (optional)",
|
|
434
|
-
"code_snapshot": "the exact full source code block of this entity"
|
|
435
|
-
}
|
|
436
|
-
]
|
|
437
|
-
}
|
|
438
|
-
${TAXONOMY_PROMPT}
|
|
439
|
-
CRITICAL RULES:
|
|
440
|
-
1. ONLY extract code structures defined in the file. Do NOT extract imports or third-party libraries as nodes.
|
|
441
|
-
2. For each node, extract its exact code snippet as "code_snapshot".
|
|
442
|
-
3. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
|
|
459
|
+
const systemPrompt = `You are a codebase indexing assistant. Your job is to analyze the source code file provided and extract all code structures (functions, methods, classes, controllers, services, interfaces, schema models, types) defined in the file.
|
|
460
|
+
Return ONLY a valid JSON object matching the schema:
|
|
461
|
+
{
|
|
462
|
+
"nodes": [
|
|
463
|
+
{
|
|
464
|
+
"node_id": "fully_qualified_identifier (e.g. Class.method or function)",
|
|
465
|
+
"name": "display_name",
|
|
466
|
+
"type": "type_from_taxonomy",
|
|
467
|
+
"signature": "param/return signature (optional)",
|
|
468
|
+
"code_snapshot": "the exact full source code block of this entity"
|
|
469
|
+
}
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
${TAXONOMY_PROMPT}
|
|
473
|
+
CRITICAL RULES:
|
|
474
|
+
1. ONLY extract code structures defined in the file. Do NOT extract imports or third-party libraries as nodes.
|
|
475
|
+
2. For each node, extract its exact code snippet as "code_snapshot".
|
|
476
|
+
3. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
|
|
443
477
|
4. Be highly precise and return an empty JSON object if no code constructs are found.`;
|
|
444
478
|
const payload = {
|
|
445
479
|
contents: [
|
|
@@ -463,32 +497,32 @@ CRITICAL RULES:
|
|
|
463
497
|
}
|
|
464
498
|
};
|
|
465
499
|
const responseText = await makeHttpRequest(url, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
|
|
466
|
-
const parsed =
|
|
500
|
+
const parsed = (0, json_1.safeJsonParse)(responseText, {});
|
|
467
501
|
const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
468
502
|
if (!text) {
|
|
469
503
|
return {};
|
|
470
504
|
}
|
|
471
|
-
return
|
|
505
|
+
return (0, json_1.safeJsonParse)(text, {});
|
|
472
506
|
}
|
|
473
507
|
async function extractWithOllama(url, model, filePath, code) {
|
|
474
508
|
const endpoint = `${url.replace(/\/$/, '')}/api/chat`;
|
|
475
|
-
const systemPrompt = `You are a codebase indexing assistant. Analyze this source code file and extract code structures (functions, classes, methods, endpoints).
|
|
476
|
-
Return ONLY a valid JSON object matching the schema:
|
|
477
|
-
{
|
|
478
|
-
"nodes": [
|
|
479
|
-
{
|
|
480
|
-
"node_id": "unique_string (e.g. Class.method or function)",
|
|
481
|
-
"name": "display_name",
|
|
482
|
-
"type": "type_from_taxonomy",
|
|
483
|
-
"signature": "param/return signature (optional)",
|
|
484
|
-
"code_snapshot": "the exact full source code block of this entity"
|
|
485
|
-
}
|
|
486
|
-
]
|
|
487
|
-
}
|
|
488
|
-
${TAXONOMY_PROMPT}
|
|
489
|
-
CRITICAL RULES:
|
|
490
|
-
1. ONLY extract constructs defined in this file. Do NOT extract third-party libraries or imports.
|
|
491
|
-
2. For each node, extract its exact code snippet as "code_snapshot".
|
|
509
|
+
const systemPrompt = `You are a codebase indexing assistant. Analyze this source code file and extract code structures (functions, classes, methods, endpoints).
|
|
510
|
+
Return ONLY a valid JSON object matching the schema:
|
|
511
|
+
{
|
|
512
|
+
"nodes": [
|
|
513
|
+
{
|
|
514
|
+
"node_id": "unique_string (e.g. Class.method or function)",
|
|
515
|
+
"name": "display_name",
|
|
516
|
+
"type": "type_from_taxonomy",
|
|
517
|
+
"signature": "param/return signature (optional)",
|
|
518
|
+
"code_snapshot": "the exact full source code block of this entity"
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
}
|
|
522
|
+
${TAXONOMY_PROMPT}
|
|
523
|
+
CRITICAL RULES:
|
|
524
|
+
1. ONLY extract constructs defined in this file. Do NOT extract third-party libraries or imports.
|
|
525
|
+
2. For each node, extract its exact code snippet as "code_snapshot".
|
|
492
526
|
3. Return a clean, valid JSON object.`;
|
|
493
527
|
const userPrompt = `File path: ${filePath}\n\nCode:\n${code}`;
|
|
494
528
|
const payload = {
|
|
@@ -501,24 +535,27 @@ CRITICAL RULES:
|
|
|
501
535
|
format: 'json'
|
|
502
536
|
};
|
|
503
537
|
const responseText = await makeHttpRequest(endpoint, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
|
|
504
|
-
const parsed =
|
|
538
|
+
const parsed = (0, json_1.safeJsonParse)(responseText, {});
|
|
505
539
|
const text = parsed.message?.content;
|
|
506
|
-
|
|
540
|
+
if (!text) {
|
|
541
|
+
return {};
|
|
542
|
+
}
|
|
543
|
+
return (0, json_1.safeJsonParse)(text, {});
|
|
507
544
|
}
|
|
508
545
|
async function resolveConnectionsWithGemini(model, key, sourceNodeId, code, candidateNodeIds) {
|
|
509
546
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
|
|
510
|
-
const systemPrompt = `You are a codebase indexing assistant. Your job is to analyze the source code of a specific code entity and identify which other known code entities from the provided candidate list it calls or references.
|
|
511
|
-
Return ONLY a valid JSON object matching the schema:
|
|
512
|
-
{
|
|
513
|
-
"connections": [
|
|
514
|
-
"target_node_id_1",
|
|
515
|
-
"target_node_id_2"
|
|
516
|
-
]
|
|
517
|
-
}
|
|
518
|
-
CRITICAL RULES:
|
|
519
|
-
1. ONLY return target node IDs that are present in the provided list of known candidates. Do NOT invent new node IDs.
|
|
520
|
-
2. DO NOT include connections to third-party libraries, language built-ins, or the source node itself.
|
|
521
|
-
3. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
|
|
547
|
+
const systemPrompt = `You are a codebase indexing assistant. Your job is to analyze the source code of a specific code entity and identify which other known code entities from the provided candidate list it calls or references.
|
|
548
|
+
Return ONLY a valid JSON object matching the schema:
|
|
549
|
+
{
|
|
550
|
+
"connections": [
|
|
551
|
+
"target_node_id_1",
|
|
552
|
+
"target_node_id_2"
|
|
553
|
+
]
|
|
554
|
+
}
|
|
555
|
+
CRITICAL RULES:
|
|
556
|
+
1. ONLY return target node IDs that are present in the provided list of known candidates. Do NOT invent new node IDs.
|
|
557
|
+
2. DO NOT include connections to third-party libraries, language built-ins, or the source node itself.
|
|
558
|
+
3. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
|
|
522
559
|
4. If no connections are found, return an empty array.`;
|
|
523
560
|
const payload = {
|
|
524
561
|
contents: [
|
|
@@ -542,26 +579,26 @@ CRITICAL RULES:
|
|
|
542
579
|
}
|
|
543
580
|
};
|
|
544
581
|
const responseText = await makeHttpRequest(url, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
|
|
545
|
-
const parsed =
|
|
582
|
+
const parsed = (0, json_1.safeJsonParse)(responseText, {});
|
|
546
583
|
const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
547
584
|
if (!text) {
|
|
548
585
|
return [];
|
|
549
586
|
}
|
|
550
|
-
const result =
|
|
587
|
+
const result = (0, json_1.safeJsonParse)(text, {});
|
|
551
588
|
return result.connections || [];
|
|
552
589
|
}
|
|
553
590
|
async function resolveConnectionsWithOllama(url, model, sourceNodeId, code, candidateNodeIds) {
|
|
554
591
|
const endpoint = `${url.replace(/\/$/, '')}/api/chat`;
|
|
555
|
-
const systemPrompt = `You are a codebase indexing assistant. Analyze this source code of a code entity and identify which other known entities from the provided candidate list it calls or references.
|
|
556
|
-
Return ONLY a valid JSON object matching the schema:
|
|
557
|
-
{
|
|
558
|
-
"connections": [
|
|
559
|
-
"target_node_id_1",
|
|
560
|
-
"target_node_id_2"
|
|
561
|
-
]
|
|
562
|
-
}
|
|
563
|
-
CRITICAL RULES:
|
|
564
|
-
1. ONLY return target node IDs that are present in the provided list of known candidates. Do NOT invent new node IDs.
|
|
592
|
+
const systemPrompt = `You are a codebase indexing assistant. Analyze this source code of a code entity and identify which other known entities from the provided candidate list it calls or references.
|
|
593
|
+
Return ONLY a valid JSON object matching the schema:
|
|
594
|
+
{
|
|
595
|
+
"connections": [
|
|
596
|
+
"target_node_id_1",
|
|
597
|
+
"target_node_id_2"
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
CRITICAL RULES:
|
|
601
|
+
1. ONLY return target node IDs that are present in the provided list of known candidates. Do NOT invent new node IDs.
|
|
565
602
|
2. Return a clean, valid JSON object.`;
|
|
566
603
|
const userPrompt = `Source Node ID: ${sourceNodeId}\n\nSource Code:\n${code}\n\nCandidate Target Node IDs:\n${JSON.stringify(candidateNodeIds, null, 2)}`;
|
|
567
604
|
const payload = {
|
|
@@ -574,12 +611,12 @@ CRITICAL RULES:
|
|
|
574
611
|
format: 'json'
|
|
575
612
|
};
|
|
576
613
|
const responseText = await makeHttpRequest(endpoint, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
|
|
577
|
-
const parsed =
|
|
614
|
+
const parsed = (0, json_1.safeJsonParse)(responseText, {});
|
|
578
615
|
const text = parsed.message?.content;
|
|
579
616
|
if (!text) {
|
|
580
617
|
return [];
|
|
581
618
|
}
|
|
582
|
-
const result =
|
|
619
|
+
const result = (0, json_1.safeJsonParse)(text, {});
|
|
583
620
|
return result.connections || [];
|
|
584
621
|
}
|
|
585
622
|
function filterCandidates(codeSnapshot, allNodeIds) {
|
|
@@ -719,8 +756,11 @@ async function runBackgroundIndexing(opts) {
|
|
|
719
756
|
progress.skipItem('empty file');
|
|
720
757
|
continue;
|
|
721
758
|
}
|
|
759
|
+
const fileLines = code.split('\n').length;
|
|
760
|
+
progress.updateStatus(`Reading ${fileLines} lines — sending to AI…`);
|
|
722
761
|
let result = {};
|
|
723
|
-
let retries =
|
|
762
|
+
let retries = 5;
|
|
763
|
+
let backoffMs = 10000;
|
|
724
764
|
while (retries > 0) {
|
|
725
765
|
try {
|
|
726
766
|
if (opts.provider === 'gemini') {
|
|
@@ -743,13 +783,38 @@ async function runBackgroundIndexing(opts) {
|
|
|
743
783
|
db.close();
|
|
744
784
|
process.exit(1);
|
|
745
785
|
}
|
|
746
|
-
|
|
786
|
+
const errMsg = err.message;
|
|
787
|
+
if (errMsg.includes('429')) {
|
|
788
|
+
progress.updateStatus(`Rate limited (429). Retrying in ${backoffMs / 1000}s...`);
|
|
789
|
+
await sleep(backoffMs);
|
|
790
|
+
backoffMs *= 2;
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
progress.updateStatus(`API error. Retrying in 2s...`);
|
|
794
|
+
await sleep(2000);
|
|
795
|
+
}
|
|
747
796
|
}
|
|
748
797
|
}
|
|
749
798
|
let newNodesCount = 0;
|
|
799
|
+
const totalNodesFound = result.nodes?.length ?? 0;
|
|
800
|
+
if (totalNodesFound === 0) {
|
|
801
|
+
progress.log(`\x1B[90mNo nodes found in file\x1B[0m`);
|
|
802
|
+
}
|
|
750
803
|
if (result.nodes && Array.isArray(result.nodes)) {
|
|
751
804
|
for (const n of result.nodes) {
|
|
752
805
|
if (n.node_id && n.name && n.type) {
|
|
806
|
+
// Estimate which line the node starts on by finding its code in the file
|
|
807
|
+
let lineNum = '?';
|
|
808
|
+
if (n.code_snapshot) {
|
|
809
|
+
const snippet = n.code_snapshot.trimStart().substring(0, 60);
|
|
810
|
+
const pos = code.indexOf(snippet.substring(0, 40));
|
|
811
|
+
if (pos !== -1) {
|
|
812
|
+
lineNum = String(code.substring(0, pos).split('\n').length);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const pctDone = fileLines > 0 ? Math.round((parseInt(lineNum) / fileLines) * 100) : 0;
|
|
816
|
+
const lineTag = lineNum !== '?' ? `\x1B[90mL${lineNum}/${fileLines} (${pctDone}% through file)\x1B[0m` : `\x1B[90m(line unknown)\x1B[0m`;
|
|
817
|
+
progress.log(`\x1B[32m+\x1B[0m \x1B[1m${n.name}\x1B[0m \x1B[90m[${n.type}]\x1B[0m ${lineTag}`);
|
|
753
818
|
db.upsertNode({
|
|
754
819
|
id: n.node_id,
|
|
755
820
|
name: n.name,
|
|
@@ -770,6 +835,7 @@ async function runBackgroundIndexing(opts) {
|
|
|
770
835
|
model: modelName
|
|
771
836
|
}
|
|
772
837
|
});
|
|
838
|
+
progress.log(` \x1B[90m└ code snapshot saved (${n.code_snapshot.split('\n').length} lines)\x1B[0m`);
|
|
773
839
|
}
|
|
774
840
|
}
|
|
775
841
|
}
|
|
@@ -831,7 +897,8 @@ async function runBackgroundIndexing(opts) {
|
|
|
831
897
|
continue;
|
|
832
898
|
}
|
|
833
899
|
let connections = [];
|
|
834
|
-
let retries =
|
|
900
|
+
let retries = 5;
|
|
901
|
+
let backoffMs = 10000;
|
|
835
902
|
while (retries > 0) {
|
|
836
903
|
try {
|
|
837
904
|
if (opts.provider === 'gemini') {
|
|
@@ -854,12 +921,22 @@ async function runBackgroundIndexing(opts) {
|
|
|
854
921
|
db.close();
|
|
855
922
|
process.exit(1);
|
|
856
923
|
}
|
|
857
|
-
|
|
924
|
+
const errMsg = err.message;
|
|
925
|
+
if (errMsg.includes('429')) {
|
|
926
|
+
progress.updateStatus(`Rate limited (429). Retrying in ${backoffMs / 1000}s...`);
|
|
927
|
+
await sleep(backoffMs);
|
|
928
|
+
backoffMs *= 2;
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
progress.updateStatus(`API error. Retrying in 2s...`);
|
|
932
|
+
await sleep(2000);
|
|
933
|
+
}
|
|
858
934
|
}
|
|
859
935
|
}
|
|
860
936
|
let addedCount = 0;
|
|
861
937
|
for (const targetId of connections) {
|
|
862
938
|
if (allNodeIds.includes(targetId)) {
|
|
939
|
+
progress.log(`Linked: \x1B[36m${node.id}\x1B[0m → \x1B[36m${targetId}\x1B[0m`);
|
|
863
940
|
db.addConnection(node.id, targetId);
|
|
864
941
|
addedCount++;
|
|
865
942
|
}
|