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 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.
@@ -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
- extraLine = '';
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; // resume offset — show true overall progress
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.extraLine = '';
131
- this.lineCount = 0;
133
+ this.statusLine = '';
134
+ this.logLines = [];
135
+ this.drawnLines = 0;
132
136
  if (!this.isTTY) {
133
- console.log(`\n${'═'.repeat(52)}`);
137
+ console.log(`\n${'═'.repeat(60)}`);
134
138
  console.log(` Phase ${phaseNum}/${this.totalPhases}: ${label}`);
135
- console.log(`${'═'.repeat(52)}`);
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(52)}\n`);
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\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.extraLine = extra;
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.extraLine = reason;
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(`skip ${reason}`);
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 rendered block
177
- if (this.lineCount > 0) {
178
- process.stdout.write(`\x1B[${this.lineCount}A\x1B[0J`);
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, 56);
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
- const lines = [
193
- ` ${spin} ${bar} ${doneStr.padEnd(9)} ${pctStr}`,
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 ${this.extraLine ? '\x1B[90m' + truncate(this.extraLine, 30) + '\x1B[0m' : ''}`,
196
- ` ▶ \x1B[90m${itemShort}\x1B[0m`,
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
- process.stdout.write(lines.join('\n'));
200
- this.lineCount = lines.length;
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.lineCount > 0) {
204
- // Clear live block
205
- process.stdout.write(`\x1B[${this.lineCount}A\x1B[0J`);
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 = JSON.parse(responseText);
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 JSON.parse(text);
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 = JSON.parse(responseText);
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 = JSON.parse(text);
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 = JSON.parse(responseText);
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 JSON.parse(text);
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 = JSON.parse(responseText);
538
+ const parsed = (0, json_1.safeJsonParse)(responseText, {});
505
539
  const text = parsed.message?.content;
506
- return JSON.parse(text);
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 = JSON.parse(responseText);
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 = JSON.parse(text);
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 = JSON.parse(responseText);
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 = JSON.parse(text);
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 = 3;
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
- await sleep(2000);
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 = 3;
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
- await sleep(2000);
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
  }