devsmind-mcp 1.1.1 → 1.2.1

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.
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const http = __importStar(require("http"));
40
40
  const https = __importStar(require("https"));
41
+ const crypto = __importStar(require("crypto"));
41
42
  const database_1 = require("../db/database");
42
43
  const indexer_1 = require("../db/indexer");
43
44
  const scanner_1 = require("../utils/scanner");
@@ -81,6 +82,137 @@ function makeHttpRequest(urlStr, method, headers, body) {
81
82
  function sleep(ms) {
82
83
  return new Promise((resolve) => setTimeout(resolve, ms));
83
84
  }
85
+ // ── Progress Display ─────────────────────────────────────────────────────
86
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
87
+ const BAR_WIDTH = 28;
88
+ const IS_TTY = !!process.stdout.isTTY;
89
+ function fmtMs(ms) {
90
+ if (ms < 1000)
91
+ return '<1s';
92
+ const s = Math.round(ms / 1000);
93
+ if (s < 60)
94
+ return `${s}s`;
95
+ const m = Math.floor(s / 60);
96
+ const rs = s % 60;
97
+ return `${m}m ${rs.toString().padStart(2, '0')}s`;
98
+ }
99
+ function makeBar(done, total) {
100
+ const pct = total > 0 ? done / total : 0;
101
+ const filled = Math.round(BAR_WIDTH * pct);
102
+ return `[${'█'.repeat(filled)}${'░'.repeat(BAR_WIDTH - filled)}]`;
103
+ }
104
+ function truncate(s, max) {
105
+ return s.length > max ? '…' + s.slice(-(max - 1)) : s;
106
+ }
107
+ class ProgressDisplay {
108
+ isTTY = IS_TTY;
109
+ lineCount = 0;
110
+ spinIdx = 0;
111
+ // phase state
112
+ phaseLabel = '';
113
+ phaseNum = 0;
114
+ totalPhases = 2;
115
+ total = 0;
116
+ done = 0;
117
+ phaseStart = 0;
118
+ itemStart = 0;
119
+ times = [];
120
+ currentItem = '';
121
+ extraLine = '';
122
+ startPhase(phaseNum, label, total, alreadyDone = 0) {
123
+ this.phaseNum = phaseNum;
124
+ this.phaseLabel = label;
125
+ this.total = total;
126
+ this.done = alreadyDone; // resume offset — show true overall progress
127
+ this.times = [];
128
+ this.phaseStart = Date.now();
129
+ this.currentItem = alreadyDone > 0 ? `Resuming from item ${alreadyDone + 1}…` : 'Starting…';
130
+ this.extraLine = '';
131
+ this.lineCount = 0;
132
+ if (!this.isTTY) {
133
+ console.log(`\n${'═'.repeat(52)}`);
134
+ console.log(` Phase ${phaseNum}/${this.totalPhases}: ${label}`);
135
+ console.log(`${'═'.repeat(52)}`);
136
+ if (alreadyDone > 0) {
137
+ console.log(` Resuming: ${alreadyDone}/${total} already done`);
138
+ }
139
+ console.log(` Remaining: ${total - alreadyDone} item(s) to process`);
140
+ console.log(`${'─'.repeat(52)}\n`);
141
+ }
142
+ else {
143
+ 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`);
145
+ this._render();
146
+ }
147
+ }
148
+ beginItem(name) {
149
+ this.currentItem = name;
150
+ this.itemStart = Date.now();
151
+ this.spinIdx = (this.spinIdx + 1) % SPINNER_FRAMES.length;
152
+ if (this.isTTY)
153
+ this._render();
154
+ else
155
+ process.stdout.write(` [${this.done + 1}/${this.total}] ${name} … `);
156
+ }
157
+ completeItem(extra = '') {
158
+ const t = Date.now() - this.itemStart;
159
+ this.times.push(t);
160
+ this.done++;
161
+ this.extraLine = extra;
162
+ if (this.isTTY)
163
+ this._render();
164
+ else
165
+ console.log(`done (${fmtMs(t)}) ${extra}`);
166
+ }
167
+ skipItem(reason) {
168
+ this.done++;
169
+ this.extraLine = reason;
170
+ if (this.isTTY)
171
+ this._render();
172
+ else
173
+ console.log(`skip — ${reason}`);
174
+ }
175
+ _render() {
176
+ // Clear previously rendered block
177
+ if (this.lineCount > 0) {
178
+ process.stdout.write(`\x1B[${this.lineCount}A\x1B[0J`);
179
+ }
180
+ const pct = this.total > 0 ? (this.done / this.total) * 100 : 0;
181
+ const bar = makeBar(this.done, this.total);
182
+ const elapsed = Date.now() - this.phaseStart;
183
+ const avg = this.times.length > 0
184
+ ? this.times.reduce((a, b) => a + b, 0) / this.times.length
185
+ : 0;
186
+ const remaining = this.total - this.done;
187
+ const eta = avg > 0 && remaining > 0 ? avg * remaining : 0;
188
+ const spin = SPINNER_FRAMES[this.spinIdx];
189
+ const itemShort = truncate(this.currentItem, 56);
190
+ const pctStr = `${Math.round(pct)}%`.padStart(4);
191
+ const doneStr = `${this.done}/${this.total}`;
192
+ const lines = [
193
+ ` ${spin} ${bar} ${doneStr.padEnd(9)} ${pctStr}`,
194
+ ` ⏱ 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
+ ``
198
+ ];
199
+ process.stdout.write(lines.join('\n'));
200
+ this.lineCount = lines.length;
201
+ }
202
+ 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;
207
+ }
208
+ const elapsed = Date.now() - this.phaseStart;
209
+ const avg = this.times.length > 0
210
+ ? this.times.reduce((a, b) => a + b, 0) / this.times.length
211
+ : 0;
212
+ const checkmark = '\x1B[32m✔\x1B[0m';
213
+ console.log(` ${checkmark} ${summary} \x1B[90m(total: ${fmtMs(elapsed)}, avg: ${avg > 0 ? fmtMs(avg) : '—'}/item)\x1B[0m`);
214
+ }
215
+ }
84
216
  // Build standard taxonomy prompt text
85
217
  const TAXONOMY_PROMPT = `
86
218
  Choose node types from this taxonomy:
@@ -97,23 +229,218 @@ Choose node types from this taxonomy:
97
229
  - CLI: cli_command | cli_option
98
230
  - UTILITY: util_function | helper | validator | formatter
99
231
  `;
232
+ // ── Vertex AI Authentication & Helper Functions ───────────────────────────
233
+ function base64UrlEncode(obj) {
234
+ return Buffer.from(JSON.stringify(obj))
235
+ .toString('base64')
236
+ .replace(/\+/g, '-')
237
+ .replace(/\//g, '_')
238
+ .replace(/=/g, '');
239
+ }
240
+ function getAccessTokenFromServiceAccount(sa) {
241
+ return new Promise((resolve, reject) => {
242
+ try {
243
+ const header = { alg: 'RS256', typ: 'JWT' };
244
+ const now = Math.floor(Date.now() / 1000);
245
+ const payload = {
246
+ iss: sa.client_email,
247
+ scope: 'https://www.googleapis.com/auth/cloud-platform',
248
+ aud: sa.token_uri || 'https://oauth2.googleapis.com/token',
249
+ exp: now + 3600,
250
+ iat: now
251
+ };
252
+ const dataToSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`;
253
+ const signer = crypto.createSign('RSA-SHA256');
254
+ signer.update(dataToSign);
255
+ const signature = signer.sign(sa.private_key, 'base64')
256
+ .replace(/\+/g, '-')
257
+ .replace(/\//g, '_')
258
+ .replace(/=/g, '');
259
+ const jwt = `${dataToSign}.${signature}`;
260
+ const tokenUri = sa.token_uri || 'https://oauth2.googleapis.com/token';
261
+ const body = `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`;
262
+ const url = new URL(tokenUri);
263
+ const req = https.request({
264
+ hostname: url.hostname,
265
+ port: url.port || 443,
266
+ path: url.pathname + url.search,
267
+ method: 'POST',
268
+ headers: {
269
+ 'Content-Type': 'application/x-www-form-urlencoded',
270
+ 'Content-Length': Buffer.byteLength(body)
271
+ }
272
+ }, (res) => {
273
+ let chunks = '';
274
+ res.on('data', (chunk) => {
275
+ chunks += chunk;
276
+ });
277
+ res.on('end', () => {
278
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
279
+ try {
280
+ const parsed = JSON.parse(chunks);
281
+ if (parsed.access_token) {
282
+ resolve(parsed.access_token);
283
+ }
284
+ else {
285
+ reject(new Error(`No access token in response: ${chunks}`));
286
+ }
287
+ }
288
+ catch (e) {
289
+ reject(e);
290
+ }
291
+ }
292
+ else {
293
+ reject(new Error(`Token request failed with status ${res.statusCode}: ${chunks}`));
294
+ }
295
+ });
296
+ });
297
+ req.on('error', (err) => {
298
+ reject(err);
299
+ });
300
+ req.write(body);
301
+ req.end();
302
+ }
303
+ catch (err) {
304
+ reject(err);
305
+ }
306
+ });
307
+ }
308
+ let cachedVertexToken = null;
309
+ let vertexTokenExpiry = 0; // Epoch ms
310
+ async function getVertexTokenCached(saData) {
311
+ const now = Date.now();
312
+ if (cachedVertexToken && vertexTokenExpiry > now + 300000) {
313
+ return cachedVertexToken;
314
+ }
315
+ const token = await getAccessTokenFromServiceAccount(saData);
316
+ cachedVertexToken = token;
317
+ vertexTokenExpiry = Date.now() + 3600 * 1000;
318
+ return token;
319
+ }
320
+ async function extractWithVertex(model, token, projectId, location, filePath, code) {
321
+ 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.
340
+ 4. Be highly precise and return an empty JSON object if no code constructs are found.`;
341
+ const payload = {
342
+ contents: [
343
+ {
344
+ role: 'user',
345
+ parts: [
346
+ {
347
+ text: `File path: ${filePath}\n\nCode:\n${code}`
348
+ }
349
+ ]
350
+ }
351
+ ],
352
+ systemInstruction: {
353
+ parts: [
354
+ {
355
+ text: systemPrompt
356
+ }
357
+ ]
358
+ },
359
+ generationConfig: {
360
+ responseMimeType: 'application/json'
361
+ }
362
+ };
363
+ const responseText = await makeHttpRequest(url, 'POST', {
364
+ 'Content-Type': 'application/json',
365
+ 'Authorization': `Bearer ${token}`
366
+ }, JSON.stringify(payload));
367
+ const parsed = JSON.parse(responseText);
368
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
369
+ if (!text) {
370
+ return {};
371
+ }
372
+ return JSON.parse(text);
373
+ }
374
+ async function resolveConnectionsWithVertex(model, token, projectId, location, sourceNodeId, code, candidateNodeIds) {
375
+ 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.
388
+ 4. If no connections are found, return an empty array.`;
389
+ const payload = {
390
+ contents: [
391
+ {
392
+ role: 'user',
393
+ parts: [
394
+ {
395
+ text: `Source Node ID: ${sourceNodeId}\n\nSource Code:\n${code}\n\nCandidate Target Node IDs in the Codebase:\n${JSON.stringify(candidateNodeIds, null, 2)}`
396
+ }
397
+ ]
398
+ }
399
+ ],
400
+ systemInstruction: {
401
+ parts: [
402
+ {
403
+ text: systemPrompt
404
+ }
405
+ ]
406
+ },
407
+ generationConfig: {
408
+ responseMimeType: 'application/json'
409
+ }
410
+ };
411
+ const responseText = await makeHttpRequest(url, 'POST', {
412
+ 'Content-Type': 'application/json',
413
+ 'Authorization': `Bearer ${token}`
414
+ }, JSON.stringify(payload));
415
+ const parsed = JSON.parse(responseText);
416
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
417
+ if (!text) {
418
+ return [];
419
+ }
420
+ const result = JSON.parse(text);
421
+ return result.connections || [];
422
+ }
100
423
  async function extractWithGemini(model, key, filePath, code) {
101
424
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
102
- 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) and caller-callee connections between them.
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.
103
426
  Return ONLY a valid JSON object matching the schema:
104
427
  {
105
428
  "nodes": [
106
- { "node_id": "fully_qualified_identifier (e.g. Class.method or function)", "name": "display_name", "type": "type_from_taxonomy", "signature": "param/return signature (optional)" }
107
- ],
108
- "connections": [
109
- { "source_node_id": "fully_qualified_caller", "target_node_id": "fully_qualified_callee" }
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
+ }
110
436
  ]
111
437
  }
112
438
  ${TAXONOMY_PROMPT}
113
439
  CRITICAL RULES:
114
440
  1. ONLY extract code structures defined in the file. Do NOT extract imports or third-party libraries as nodes.
115
- 2. DO NOT wrap JSON in markdown blocks (e.g. no \`\`\`json). Return raw JSON.
116
- 3. Be highly precise and return an empty JSON object if no code constructs are found.`;
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.
443
+ 4. Be highly precise and return an empty JSON object if no code constructs are found.`;
117
444
  const payload = {
118
445
  contents: [
119
446
  {
@@ -145,20 +472,24 @@ CRITICAL RULES:
145
472
  }
146
473
  async function extractWithOllama(url, model, filePath, code) {
147
474
  const endpoint = `${url.replace(/\/$/, '')}/api/chat`;
148
- const systemPrompt = `You are a codebase indexing assistant. Analyze this source code file and extract code structures (functions, classes, methods, endpoints) and caller-callee connections.
475
+ const systemPrompt = `You are a codebase indexing assistant. Analyze this source code file and extract code structures (functions, classes, methods, endpoints).
149
476
  Return ONLY a valid JSON object matching the schema:
150
477
  {
151
478
  "nodes": [
152
- { "node_id": "unique_string (e.g. Class.method or function)", "name": "display_name", "type": "type_from_taxonomy", "signature": "param/return signature (optional)" }
153
- ],
154
- "connections": [
155
- { "source_node_id": "caller", "target_node_id": "callee" }
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
+ }
156
486
  ]
157
487
  }
158
488
  ${TAXONOMY_PROMPT}
159
489
  CRITICAL RULES:
160
490
  1. ONLY extract constructs defined in this file. Do NOT extract third-party libraries or imports.
161
- 2. Return a clean, valid JSON object.`;
491
+ 2. For each node, extract its exact code snippet as "code_snapshot".
492
+ 3. Return a clean, valid JSON object.`;
162
493
  const userPrompt = `File path: ${filePath}\n\nCode:\n${code}`;
163
494
  const payload = {
164
495
  model,
@@ -172,10 +503,95 @@ CRITICAL RULES:
172
503
  const responseText = await makeHttpRequest(endpoint, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
173
504
  const parsed = JSON.parse(responseText);
174
505
  const text = parsed.message?.content;
506
+ return JSON.parse(text);
507
+ }
508
+ async function resolveConnectionsWithGemini(model, key, sourceNodeId, code, candidateNodeIds) {
509
+ 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.
522
+ 4. If no connections are found, return an empty array.`;
523
+ const payload = {
524
+ contents: [
525
+ {
526
+ parts: [
527
+ {
528
+ text: `Source Node ID: ${sourceNodeId}\n\nSource Code:\n${code}\n\nCandidate Target Node IDs in the Codebase:\n${JSON.stringify(candidateNodeIds, null, 2)}`
529
+ }
530
+ ]
531
+ }
532
+ ],
533
+ systemInstruction: {
534
+ parts: [
535
+ {
536
+ text: systemPrompt
537
+ }
538
+ ]
539
+ },
540
+ generationConfig: {
541
+ responseMimeType: 'application/json'
542
+ }
543
+ };
544
+ const responseText = await makeHttpRequest(url, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
545
+ const parsed = JSON.parse(responseText);
546
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
175
547
  if (!text) {
176
- return {};
548
+ return [];
177
549
  }
178
- return JSON.parse(text);
550
+ const result = JSON.parse(text);
551
+ return result.connections || [];
552
+ }
553
+ async function resolveConnectionsWithOllama(url, model, sourceNodeId, code, candidateNodeIds) {
554
+ 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.
565
+ 2. Return a clean, valid JSON object.`;
566
+ const userPrompt = `Source Node ID: ${sourceNodeId}\n\nSource Code:\n${code}\n\nCandidate Target Node IDs:\n${JSON.stringify(candidateNodeIds, null, 2)}`;
567
+ const payload = {
568
+ model,
569
+ messages: [
570
+ { role: 'system', content: systemPrompt },
571
+ { role: 'user', content: userPrompt }
572
+ ],
573
+ stream: false,
574
+ format: 'json'
575
+ };
576
+ const responseText = await makeHttpRequest(endpoint, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
577
+ const parsed = JSON.parse(responseText);
578
+ const text = parsed.message?.content;
579
+ if (!text) {
580
+ return [];
581
+ }
582
+ const result = JSON.parse(text);
583
+ return result.connections || [];
584
+ }
585
+ function filterCandidates(codeSnapshot, allNodeIds) {
586
+ const lowerCode = codeSnapshot.toLowerCase();
587
+ return allNodeIds.filter(id => {
588
+ const shortName = id.includes('.') ? id.split('.').pop() : id;
589
+ if (!shortName || shortName.trim().length === 0)
590
+ return false;
591
+ if (shortName.length < 3)
592
+ return false;
593
+ return lowerCode.includes(shortName.toLowerCase());
594
+ });
179
595
  }
180
596
  async function runBackgroundIndexing(opts) {
181
597
  const resolvedDevmind = path.resolve(opts.devmindPath);
@@ -183,6 +599,10 @@ async function runBackgroundIndexing(opts) {
183
599
  console.log(` Brain directory : ${resolvedDevmind}`);
184
600
  console.log(` Provider : ${opts.provider}`);
185
601
  let modelName = opts.model || '';
602
+ let vertexSaData = null;
603
+ let vertexToken = null;
604
+ let vertexProjectId = '';
605
+ let vertexLocation = 'us-central1';
186
606
  if (opts.provider === 'gemini') {
187
607
  modelName = modelName || 'gemini-2.0-flash';
188
608
  const apiKey = opts.key || process.env.GEMINI_API_KEY || '';
@@ -192,20 +612,61 @@ async function runBackgroundIndexing(opts) {
192
612
  }
193
613
  opts.key = apiKey;
194
614
  }
615
+ else if (opts.provider === 'vertex') {
616
+ modelName = modelName || 'gemini-1.5-flash';
617
+ const inputKey = opts.key || process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.VERTEX_API_KEY || process.env.GEMINI_API_KEY || '';
618
+ if (!inputKey) {
619
+ console.error('❌ Error: Vertex AI requires a Service Account JSON path or Bearer Token. Pass --key or set GOOGLE_APPLICATION_CREDENTIALS / VERTEX_API_KEY environment variable.');
620
+ process.exit(1);
621
+ }
622
+ try {
623
+ if (inputKey.trim().startsWith('{')) {
624
+ vertexSaData = JSON.parse(inputKey);
625
+ }
626
+ else if (fs.existsSync(inputKey)) {
627
+ vertexSaData = JSON.parse(fs.readFileSync(inputKey, 'utf-8'));
628
+ }
629
+ }
630
+ catch (e) {
631
+ // Treat as raw token
632
+ }
633
+ vertexProjectId = vertexSaData?.project_id || process.env.GCP_PROJECT_ID || process.env.VERTEX_PROJECT_ID || '';
634
+ vertexLocation = process.env.GCP_LOCATION || process.env.VERTEX_LOCATION || 'us-central1';
635
+ if (!vertexSaData && !inputKey.startsWith('ya29.')) {
636
+ console.error('❌ Error: Vertex key must be a valid Service Account JSON file path, inline JSON, or raw OAuth access token starting with "ya29."');
637
+ process.exit(1);
638
+ }
639
+ if (!vertexProjectId) {
640
+ console.error('❌ Error: Vertex Project ID could not be determined. Please set GCP_PROJECT_ID environment variable or specify it in your service account JSON.');
641
+ process.exit(1);
642
+ }
643
+ if (!vertexSaData) {
644
+ vertexToken = inputKey; // Raw Bearer token
645
+ }
646
+ }
195
647
  else {
196
648
  modelName = modelName || 'qwen2.5-coder';
197
649
  opts.url = opts.url || 'http://localhost:11434';
198
650
  }
651
+ const getVertexToken = async () => {
652
+ if (vertexToken)
653
+ return vertexToken;
654
+ if (vertexSaData) {
655
+ return await getVertexTokenCached(vertexSaData);
656
+ }
657
+ throw new Error('No Vertex credentials available');
658
+ };
199
659
  console.log(` Model : ${modelName}`);
200
- // 1. Scan for repos & files
660
+ // 1. Open DB
661
+ const dbFile = path.join(resolvedDevmind, 'brain.db');
662
+ const db = new database_1.DevMindDatabase(dbFile);
663
+ // 2. Scan for repos & files
201
664
  const { repos, total_files } = (0, scanner_1.scanRepoFiles)(resolvedDevmind);
202
665
  if (total_files === 0) {
203
666
  console.log('⚠️ No files found to index. Make sure config.json repositories are configured properly.');
667
+ db.close();
204
668
  return;
205
669
  }
206
- // 2. Open DB
207
- const dbFile = path.join(resolvedDevmind, 'brain.db');
208
- const db = new database_1.DevMindDatabase(dbFile);
209
670
  // 3. Read or create scratchpad
210
671
  let pad = (0, indexer_1.readScratchpad)(resolvedDevmind);
211
672
  if (!pad) {
@@ -216,137 +677,216 @@ async function runBackgroundIndexing(opts) {
216
677
  db.close();
217
678
  return;
218
679
  }
219
- const reposDone = new Set(pad.repos_done);
220
- // Flatten file list for tracking
221
- const allFiles = [];
222
- for (const repo of repos) {
223
- if (reposDone.has(repo.repo_name))
224
- continue;
225
- for (const f of repo.files) {
226
- allFiles.push({ repoName: repo.repo_name, absolutePath: f });
227
- }
228
- }
229
- let startIndex = 0;
230
- if (pad.last_file_indexed) {
231
- const idx = allFiles.findIndex(f => f.absolutePath === pad.last_file_indexed);
232
- if (idx !== -1) {
233
- startIndex = idx + 1;
234
- }
235
- }
236
- console.log(` Progress : ${pad.files_done}/${pad.files_total} files (${Math.round((pad.files_done / pad.files_total) * 100)}%)`);
237
- console.log(` Remaining Files : ${allFiles.length - startIndex} file(s)`);
238
- console.log('──────────────────────────────────────────────────\n');
239
- let fileIndex = startIndex;
240
- let successCount = 0;
241
- for (; fileIndex < allFiles.length; fileIndex++) {
242
- const fileObj = allFiles[fileIndex];
243
- const relPath = path.relative(process.cwd(), fileObj.absolutePath);
244
- console.log(`[${pad.files_done + 1}/${pad.files_total}] Indexing: ${relPath}...`);
245
- let code = '';
246
- try {
247
- code = fs.readFileSync(fileObj.absolutePath, 'utf-8');
680
+ // =========================================================================
681
+ // PHASE 1: NODE & CODE SNAPSHOT EXTRACTION
682
+ // =========================================================================
683
+ const progress = new ProgressDisplay();
684
+ if (pad.phase === 1) {
685
+ const reposDone = new Set(pad.repos_done);
686
+ const allFiles = [];
687
+ for (const repo of repos) {
688
+ if (reposDone.has(repo.repo_name))
689
+ continue;
690
+ for (const f of repo.files) {
691
+ allFiles.push({ repoName: repo.repo_name, absolutePath: f });
692
+ }
248
693
  }
249
- catch (err) {
250
- console.warn(`⚠️ Warning: Failed to read file ${fileObj.absolutePath}: ${err.message}`);
251
- continue;
694
+ let startIndex = 0;
695
+ if (pad.last_file_indexed) {
696
+ const idx = allFiles.findIndex(f => f.absolutePath === pad.last_file_indexed);
697
+ if (idx !== -1)
698
+ startIndex = idx + 1;
252
699
  }
253
- if (code.trim().length === 0) {
254
- // Empty file
255
- pad.files_done++;
256
- pad.last_file_indexed = fileObj.absolutePath;
257
- (0, indexer_1.updateScratchpad)(resolvedDevmind, {
258
- files_done: pad.files_done,
259
- last_file_indexed: pad.last_file_indexed
260
- });
261
- continue;
262
- }
263
- let result = {};
264
- let retries = 3;
265
- while (retries > 0) {
700
+ // Use pad.files_total as true total so resume shows e.g. 14/1068, not 1/1055
701
+ progress.startPhase(1, 'Node & Code Extraction', pad.files_total, pad.files_done);
702
+ let fileIndex = startIndex;
703
+ for (; fileIndex < allFiles.length; fileIndex++) {
704
+ const fileObj = allFiles[fileIndex];
705
+ const relPath = path.relative(process.cwd(), fileObj.absolutePath);
706
+ progress.beginItem(relPath);
707
+ let code = '';
266
708
  try {
267
- if (opts.provider === 'gemini') {
268
- result = await extractWithGemini(modelName, opts.key, fileObj.absolutePath, code);
709
+ code = fs.readFileSync(fileObj.absolutePath, 'utf-8');
710
+ }
711
+ catch (err) {
712
+ progress.skipItem(`read error: ${err.message}`);
713
+ continue;
714
+ }
715
+ if (code.trim().length === 0) {
716
+ pad.files_done++;
717
+ pad.last_file_indexed = fileObj.absolutePath;
718
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
719
+ progress.skipItem('empty file');
720
+ continue;
721
+ }
722
+ let result = {};
723
+ let retries = 3;
724
+ while (retries > 0) {
725
+ try {
726
+ if (opts.provider === 'gemini') {
727
+ result = await extractWithGemini(modelName, opts.key, fileObj.absolutePath, code);
728
+ }
729
+ else if (opts.provider === 'vertex') {
730
+ const token = await getVertexToken();
731
+ result = await extractWithVertex(modelName, token, vertexProjectId, vertexLocation, fileObj.absolutePath, code);
732
+ }
733
+ else {
734
+ result = await extractWithOllama(opts.url, modelName, fileObj.absolutePath, code);
735
+ }
736
+ break;
269
737
  }
270
- else {
271
- result = await extractWithOllama(opts.url, modelName, fileObj.absolutePath, code);
738
+ catch (err) {
739
+ retries--;
740
+ if (retries === 0) {
741
+ progress.finishPhase(`Paused — API error. Run again to resume.`);
742
+ console.error(`❌ ${err.message}`);
743
+ db.close();
744
+ process.exit(1);
745
+ }
746
+ await sleep(2000);
272
747
  }
273
- break;
274
748
  }
275
- catch (err) {
276
- retries--;
277
- console.error(` ⚠️ API Error: ${err.message}. Retries left: ${retries}`);
278
- if (retries === 0) {
279
- console.error('❌ Indexing paused. Run this command again to resume.');
280
- db.close();
281
- process.exit(1);
749
+ let newNodesCount = 0;
750
+ if (result.nodes && Array.isArray(result.nodes)) {
751
+ for (const n of result.nodes) {
752
+ if (n.node_id && n.name && n.type) {
753
+ db.upsertNode({
754
+ id: n.node_id,
755
+ name: n.name,
756
+ type: n.type,
757
+ file_path: fileObj.absolutePath,
758
+ signature: n.signature || null
759
+ });
760
+ newNodesCount++;
761
+ if (n.code_snapshot) {
762
+ db.updateHistory({
763
+ node_id: n.node_id,
764
+ code_snapshot: n.code_snapshot,
765
+ reasoning: {
766
+ what_changed: 'Initial code extraction during background indexing',
767
+ why: 'Initial index setup',
768
+ goal: 'Establish baseline codebase knowledge graph',
769
+ developer: 'devsmind background indexer',
770
+ model: modelName
771
+ }
772
+ });
773
+ }
774
+ }
282
775
  }
283
- await sleep(2000);
284
776
  }
777
+ pad.files_done++;
778
+ pad.nodes_created += newNodesCount;
779
+ pad.last_file_indexed = fileObj.absolutePath;
780
+ pad.current_repo = fileObj.repoName;
781
+ pad.updated_at = new Date().toISOString();
782
+ const currentRepoFiles = repos.find(r => r.repo_name === fileObj.repoName)?.files || [];
783
+ const isRepoDone = currentRepoFiles.length > 0 && currentRepoFiles[currentRepoFiles.length - 1] === fileObj.absolutePath;
784
+ if (isRepoDone && !pad.repos_done.includes(fileObj.repoName)) {
785
+ pad.repos_done.push(fileObj.repoName);
786
+ }
787
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
788
+ progress.completeItem(`${pad.nodes_created} node(s) found so far`);
789
+ if (opts.provider === 'gemini' || opts.provider === 'vertex')
790
+ await sleep(2000);
791
+ else
792
+ await sleep(200);
285
793
  }
286
- // 4. Save extracted nodes and connections directly to DB
287
- let newNodes = 0;
288
- let newConns = 0;
289
- if (result.nodes && Array.isArray(result.nodes)) {
290
- for (const n of result.nodes) {
291
- if (n.node_id && n.name && n.type) {
292
- db.upsertNode({
293
- id: n.node_id,
294
- name: n.name,
295
- type: n.type,
296
- file_path: fileObj.absolutePath,
297
- signature: n.signature || null
298
- });
299
- newNodes++;
794
+ // Transition to Phase 2
795
+ const activeNodes = db.listNodes();
796
+ pad.phase = 2;
797
+ pad.nodes_total = activeNodes.length;
798
+ pad.nodes_done = 0;
799
+ pad.updated_at = new Date().toISOString();
800
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
801
+ progress.finishPhase(`Phase 1 done — ${activeNodes.length} node(s) extracted from ${pad.files_done} file(s)`);
802
+ }
803
+ // =========================================================================
804
+ // PHASE 2: AI CONNECTION RESOLUTION / LINKING
805
+ // =========================================================================
806
+ if (pad.phase === 2) {
807
+ const activeNodes = db.listNodes();
808
+ const allNodeIds = activeNodes.map(n => n.id);
809
+ const resumeIndex = pad.nodes_done || 0;
810
+ // Use total node count and resume offset so bar shows true progress
811
+ progress.startPhase(2, 'AI Connection Resolution', activeNodes.length, resumeIndex);
812
+ let nodeIndex = resumeIndex;
813
+ for (; nodeIndex < activeNodes.length; nodeIndex++) {
814
+ const node = activeNodes[nodeIndex];
815
+ progress.beginItem(node.id);
816
+ const latestCode = db.getLatestCode(node.id);
817
+ if (!latestCode || !latestCode.code_snapshot || latestCode.code_snapshot.trim().length === 0) {
818
+ pad.nodes_done = nodeIndex + 1;
819
+ pad.updated_at = new Date().toISOString();
820
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
821
+ progress.skipItem('no code snapshot');
822
+ continue;
823
+ }
824
+ const candidates = filterCandidates(latestCode.code_snapshot, allNodeIds);
825
+ const filteredCandidates = candidates.filter(id => id !== node.id);
826
+ if (filteredCandidates.length === 0) {
827
+ pad.nodes_done = nodeIndex + 1;
828
+ pad.updated_at = new Date().toISOString();
829
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
830
+ progress.skipItem('no matching candidates');
831
+ continue;
832
+ }
833
+ let connections = [];
834
+ let retries = 3;
835
+ while (retries > 0) {
836
+ try {
837
+ if (opts.provider === 'gemini') {
838
+ connections = await resolveConnectionsWithGemini(modelName, opts.key, node.id, latestCode.code_snapshot, filteredCandidates);
839
+ }
840
+ else if (opts.provider === 'vertex') {
841
+ const token = await getVertexToken();
842
+ connections = await resolveConnectionsWithVertex(modelName, token, vertexProjectId, vertexLocation, node.id, latestCode.code_snapshot, filteredCandidates);
843
+ }
844
+ else {
845
+ connections = await resolveConnectionsWithOllama(opts.url, modelName, node.id, latestCode.code_snapshot, filteredCandidates);
846
+ }
847
+ break;
848
+ }
849
+ catch (err) {
850
+ retries--;
851
+ if (retries === 0) {
852
+ progress.finishPhase('Paused — API error. Run again to resume.');
853
+ console.error(`❌ ${err.message}`);
854
+ db.close();
855
+ process.exit(1);
856
+ }
857
+ await sleep(2000);
300
858
  }
301
859
  }
302
- }
303
- if (result.connections && Array.isArray(result.connections)) {
304
- for (const c of result.connections) {
305
- if (c.source_node_id && c.target_node_id) {
306
- db.addConnection(c.source_node_id, c.target_node_id);
307
- newConns++;
860
+ let addedCount = 0;
861
+ for (const targetId of connections) {
862
+ if (allNodeIds.includes(targetId)) {
863
+ db.addConnection(node.id, targetId);
864
+ addedCount++;
308
865
  }
309
866
  }
867
+ pad.nodes_done = nodeIndex + 1;
868
+ pad.connections_created += addedCount;
869
+ pad.updated_at = new Date().toISOString();
870
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
871
+ progress.completeItem(`${pad.connections_created} connection(s) created so far`);
872
+ if (opts.provider === 'gemini' || opts.provider === 'vertex')
873
+ await sleep(2000);
874
+ else
875
+ await sleep(200);
310
876
  }
311
- // Update progress
312
- pad.files_done++;
313
- pad.nodes_created += newNodes;
314
- pad.connections_created += newConns;
315
- pad.last_file_indexed = fileObj.absolutePath;
316
- pad.current_repo = fileObj.repoName;
317
- // Check if the repository is fully indexed
318
- const currentRepoFiles = repos.find(r => r.repo_name === fileObj.repoName)?.files || [];
319
- const isRepoDone = currentRepoFiles.length > 0 && currentRepoFiles[currentRepoFiles.length - 1] === fileObj.absolutePath;
320
- if (isRepoDone && !pad.repos_done.includes(fileObj.repoName)) {
321
- pad.repos_done.push(fileObj.repoName);
322
- }
323
- (0, indexer_1.updateScratchpad)(resolvedDevmind, {
324
- files_done: pad.files_done,
325
- last_file_indexed: pad.last_file_indexed,
326
- nodes_created: pad.nodes_created,
327
- connections_created: pad.connections_created,
328
- current_repo: pad.current_repo,
329
- repos_done: pad.repos_done
330
- });
331
- console.log(` Success: Created ${newNodes} node(s), ${newConns} connection(s).`);
332
- successCount++;
333
- // Respect Gemini AI Studio Free Tier limit (15 Requests/Min -> 1 request every 4 seconds)
334
- if (opts.provider === 'gemini') {
335
- await sleep(4000);
336
- }
337
- else {
338
- // Short breather for local CPU/GPU to not cook
339
- await sleep(200);
340
- }
877
+ progress.finishPhase(`Phase 2 done — ${pad.connections_created} connection(s) linked across ${pad.nodes_total} node(s)`);
341
878
  }
342
- // Mark complete
343
- (0, indexer_1.completeScratchpad)(resolvedDevmind);
879
+ // Mark indexing session as fully complete
880
+ pad.status = 'complete';
881
+ pad.updated_at = new Date().toISOString();
882
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
344
883
  db.vacuum();
345
884
  db.close();
346
- console.log('\n🎉 Indexing finished completely!');
347
- console.log(` Total Files Indexed : ${pad.files_done}`);
348
- console.log(` Total Nodes Created : ${pad.nodes_created}`);
349
- console.log(` Total Conns Created : ${pad.connections_created}`);
350
- console.log('──────────────────────────────────────────────────\n');
885
+ console.log('');
886
+ console.log('\x1B[1m\x1B[32m ✔ Indexing complete!\x1B[0m');
887
+ console.log(` ├─ Files indexed : \x1B[33m${pad.files_done}\x1B[0m`);
888
+ console.log(` ├─ Nodes created : \x1B[33m${pad.nodes_created}\x1B[0m`);
889
+ console.log(` └─ Connections : \x1B[33m${pad.connections_created}\x1B[0m`);
890
+ console.log('');
351
891
  }
352
892
  //# sourceMappingURL=runner.js.map