devsmind-mcp 1.2.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/LICENSE +21 -21
- package/README.md +55 -47
- package/dist/cli/runner.js +232 -151
- package/dist/cli/runner.js.map +1 -1
- package/dist/db/database.d.ts +15 -1
- package/dist/db/database.js +420 -74
- package/dist/db/database.js.map +1 -1
- package/dist/db/schema.js +33 -33
- package/dist/mcp/server.js +21 -9
- 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/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,18 +611,19 @@ 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) {
|
|
586
623
|
const lowerCode = codeSnapshot.toLowerCase();
|
|
587
624
|
return allNodeIds.filter(id => {
|
|
588
|
-
const
|
|
625
|
+
const symbolName = id.includes('#') ? id.split('#').pop() : id;
|
|
626
|
+
const shortName = symbolName.includes('.') ? symbolName.split('.').pop() : symbolName;
|
|
589
627
|
if (!shortName || shortName.trim().length === 0)
|
|
590
628
|
return false;
|
|
591
629
|
if (shortName.length < 3)
|
|
@@ -719,8 +757,11 @@ async function runBackgroundIndexing(opts) {
|
|
|
719
757
|
progress.skipItem('empty file');
|
|
720
758
|
continue;
|
|
721
759
|
}
|
|
760
|
+
const fileLines = code.split('\n').length;
|
|
761
|
+
progress.updateStatus(`Reading ${fileLines} lines — sending to AI…`);
|
|
722
762
|
let result = {};
|
|
723
|
-
let retries =
|
|
763
|
+
let retries = 5;
|
|
764
|
+
let backoffMs = 10000;
|
|
724
765
|
while (retries > 0) {
|
|
725
766
|
try {
|
|
726
767
|
if (opts.provider === 'gemini') {
|
|
@@ -743,15 +784,43 @@ async function runBackgroundIndexing(opts) {
|
|
|
743
784
|
db.close();
|
|
744
785
|
process.exit(1);
|
|
745
786
|
}
|
|
746
|
-
|
|
787
|
+
const errMsg = err.message;
|
|
788
|
+
if (errMsg.includes('429')) {
|
|
789
|
+
progress.updateStatus(`Rate limited (429). Retrying in ${backoffMs / 1000}s...`);
|
|
790
|
+
await sleep(backoffMs);
|
|
791
|
+
backoffMs *= 2;
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
progress.updateStatus(`API error. Retrying in 2s...`);
|
|
795
|
+
await sleep(2000);
|
|
796
|
+
}
|
|
747
797
|
}
|
|
748
798
|
}
|
|
749
799
|
let newNodesCount = 0;
|
|
800
|
+
const totalNodesFound = result.nodes?.length ?? 0;
|
|
801
|
+
if (totalNodesFound === 0) {
|
|
802
|
+
progress.log(`\x1B[90mNo nodes found in file\x1B[0m`);
|
|
803
|
+
}
|
|
750
804
|
if (result.nodes && Array.isArray(result.nodes)) {
|
|
751
805
|
for (const n of result.nodes) {
|
|
752
806
|
if (n.node_id && n.name && n.type) {
|
|
807
|
+
// Estimate which line the node starts on by finding its code in the file
|
|
808
|
+
let lineNum = '?';
|
|
809
|
+
if (n.code_snapshot) {
|
|
810
|
+
const snippet = n.code_snapshot.trimStart().substring(0, 60);
|
|
811
|
+
const pos = code.indexOf(snippet.substring(0, 40));
|
|
812
|
+
if (pos !== -1) {
|
|
813
|
+
lineNum = String(code.substring(0, pos).split('\n').length);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const pctDone = fileLines > 0 ? Math.round((parseInt(lineNum) / fileLines) * 100) : 0;
|
|
817
|
+
const lineTag = lineNum !== '?' ? `\x1B[90mL${lineNum}/${fileLines} (${pctDone}% through file)\x1B[0m` : `\x1B[90m(line unknown)\x1B[0m`;
|
|
818
|
+
progress.log(`\x1B[32m+\x1B[0m \x1B[1m${n.name}\x1B[0m \x1B[90m[${n.type}]\x1B[0m ${lineTag}`);
|
|
819
|
+
const workspaceRoot = path.dirname(resolvedDevmind);
|
|
820
|
+
const relPath = path.relative(workspaceRoot, fileObj.absolutePath).replace(/\\/g, '/');
|
|
821
|
+
const qualifiedId = `${relPath}#${n.node_id}`;
|
|
753
822
|
db.upsertNode({
|
|
754
|
-
id:
|
|
823
|
+
id: qualifiedId,
|
|
755
824
|
name: n.name,
|
|
756
825
|
type: n.type,
|
|
757
826
|
file_path: fileObj.absolutePath,
|
|
@@ -760,7 +829,7 @@ async function runBackgroundIndexing(opts) {
|
|
|
760
829
|
newNodesCount++;
|
|
761
830
|
if (n.code_snapshot) {
|
|
762
831
|
db.updateHistory({
|
|
763
|
-
node_id:
|
|
832
|
+
node_id: qualifiedId,
|
|
764
833
|
code_snapshot: n.code_snapshot,
|
|
765
834
|
reasoning: {
|
|
766
835
|
what_changed: 'Initial code extraction during background indexing',
|
|
@@ -770,6 +839,7 @@ async function runBackgroundIndexing(opts) {
|
|
|
770
839
|
model: modelName
|
|
771
840
|
}
|
|
772
841
|
});
|
|
842
|
+
progress.log(` \x1B[90m└ code snapshot saved (${n.code_snapshot.split('\n').length} lines)\x1B[0m`);
|
|
773
843
|
}
|
|
774
844
|
}
|
|
775
845
|
}
|
|
@@ -831,7 +901,8 @@ async function runBackgroundIndexing(opts) {
|
|
|
831
901
|
continue;
|
|
832
902
|
}
|
|
833
903
|
let connections = [];
|
|
834
|
-
let retries =
|
|
904
|
+
let retries = 5;
|
|
905
|
+
let backoffMs = 10000;
|
|
835
906
|
while (retries > 0) {
|
|
836
907
|
try {
|
|
837
908
|
if (opts.provider === 'gemini') {
|
|
@@ -854,12 +925,22 @@ async function runBackgroundIndexing(opts) {
|
|
|
854
925
|
db.close();
|
|
855
926
|
process.exit(1);
|
|
856
927
|
}
|
|
857
|
-
|
|
928
|
+
const errMsg = err.message;
|
|
929
|
+
if (errMsg.includes('429')) {
|
|
930
|
+
progress.updateStatus(`Rate limited (429). Retrying in ${backoffMs / 1000}s...`);
|
|
931
|
+
await sleep(backoffMs);
|
|
932
|
+
backoffMs *= 2;
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
progress.updateStatus(`API error. Retrying in 2s...`);
|
|
936
|
+
await sleep(2000);
|
|
937
|
+
}
|
|
858
938
|
}
|
|
859
939
|
}
|
|
860
940
|
let addedCount = 0;
|
|
861
941
|
for (const targetId of connections) {
|
|
862
942
|
if (allNodeIds.includes(targetId)) {
|
|
943
|
+
progress.log(`Linked: \x1B[36m${node.id}\x1B[0m → \x1B[36m${targetId}\x1B[0m`);
|
|
863
944
|
db.addConnection(node.id, targetId);
|
|
864
945
|
addedCount++;
|
|
865
946
|
}
|