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.
@@ -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,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 = 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) {
586
623
  const lowerCode = codeSnapshot.toLowerCase();
587
624
  return allNodeIds.filter(id => {
588
- const shortName = id.includes('.') ? id.split('.').pop() : id;
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 = 3;
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
- await sleep(2000);
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: n.node_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: n.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 = 3;
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
- await sleep(2000);
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
  }