devsmind-mcp 1.2.0 → 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.
@@ -38,9 +38,11 @@ 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");
45
+ const json_1 = require("../utils/json");
44
46
  function makeHttpRequest(urlStr, method, headers, body) {
45
47
  return new Promise((resolve, reject) => {
46
48
  const isHttps = urlStr.startsWith('https');
@@ -81,39 +83,398 @@ function makeHttpRequest(urlStr, method, headers, body) {
81
83
  function sleep(ms) {
82
84
  return new Promise((resolve) => setTimeout(resolve, ms));
83
85
  }
86
+ // ── Progress Display ─────────────────────────────────────────────────────
87
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
88
+ const BAR_WIDTH = 28;
89
+ const IS_TTY = !!process.stdout.isTTY;
90
+ function fmtMs(ms) {
91
+ if (ms < 1000)
92
+ return '<1s';
93
+ const s = Math.round(ms / 1000);
94
+ if (s < 60)
95
+ return `${s}s`;
96
+ const m = Math.floor(s / 60);
97
+ const rs = s % 60;
98
+ return `${m}m ${rs.toString().padStart(2, '0')}s`;
99
+ }
100
+ function makeBar(done, total) {
101
+ const pct = total > 0 ? done / total : 0;
102
+ const filled = Math.round(BAR_WIDTH * pct);
103
+ return `[${'█'.repeat(filled)}${'░'.repeat(BAR_WIDTH - filled)}]`;
104
+ }
105
+ function truncate(s, max) {
106
+ return s.length > max ? '…' + s.slice(-(max - 1)) : s;
107
+ }
108
+ const MAX_LOG_LINES = 18;
109
+ class ProgressDisplay {
110
+ isTTY = IS_TTY;
111
+ spinIdx = 0;
112
+ // phase state
113
+ phaseNum = 0;
114
+ totalPhases = 2;
115
+ total = 0;
116
+ done = 0;
117
+ phaseStart = 0;
118
+ itemStart = 0;
119
+ times = [];
120
+ currentItem = '';
121
+ statusLine = '';
122
+ // scrolling log ring buffer
123
+ logLines = [];
124
+ // total lines currently drawn on screen (log + bar)
125
+ drawnLines = 0;
126
+ startPhase(phaseNum, label, total, alreadyDone = 0) {
127
+ this.phaseNum = phaseNum;
128
+ this.total = total;
129
+ this.done = alreadyDone;
130
+ this.times = [];
131
+ this.phaseStart = Date.now();
132
+ this.currentItem = alreadyDone > 0 ? `Resuming from item ${alreadyDone + 1}…` : 'Starting…';
133
+ this.statusLine = '';
134
+ this.logLines = [];
135
+ this.drawnLines = 0;
136
+ if (!this.isTTY) {
137
+ console.log(`\n${'═'.repeat(60)}`);
138
+ console.log(` Phase ${phaseNum}/${this.totalPhases}: ${label}`);
139
+ console.log(`${'═'.repeat(60)}`);
140
+ if (alreadyDone > 0)
141
+ console.log(` Resuming: ${alreadyDone}/${total} already done`);
142
+ console.log(` Remaining: ${total - alreadyDone} item(s) to process`);
143
+ console.log(`${'─'.repeat(60)}\n`);
144
+ }
145
+ else {
146
+ const resumeTag = alreadyDone > 0 ? ` \x1B[90m(resuming from ${alreadyDone}/${total})\x1B[0m` : '';
147
+ process.stdout.write(`\n \x1B[1m\x1B[36mPhase ${phaseNum}/${this.totalPhases}: ${label}\x1B[0m${resumeTag}\n`);
148
+ this._render();
149
+ }
150
+ }
151
+ beginItem(name) {
152
+ this.currentItem = name;
153
+ this.itemStart = Date.now();
154
+ this.statusLine = '';
155
+ this.spinIdx = (this.spinIdx + 1) % SPINNER_FRAMES.length;
156
+ if (this.isTTY)
157
+ this._render();
158
+ else
159
+ process.stdout.write(` [${this.done + 1}/${this.total}] ${name} … `);
160
+ }
161
+ completeItem(extra = '') {
162
+ const t = Date.now() - this.itemStart;
163
+ this.times.push(t);
164
+ this.done++;
165
+ this.statusLine = extra;
166
+ if (this.isTTY)
167
+ this._render();
168
+ else
169
+ console.log(`done (${fmtMs(t)}) ${extra}`);
170
+ }
171
+ skipItem(reason) {
172
+ this.done++;
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;
181
+ if (this.isTTY)
182
+ this._render();
183
+ else
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
+ }
199
+ }
200
+ _render() {
201
+ // Clear all previously drawn lines
202
+ if (this.drawnLines > 0) {
203
+ process.stdout.write(`\x1B[${this.drawnLines}A\x1B[0J`);
204
+ }
205
+ const pct = this.total > 0 ? (this.done / this.total) * 100 : 0;
206
+ const bar = makeBar(this.done, this.total);
207
+ const elapsed = Date.now() - this.phaseStart;
208
+ const avg = this.times.length > 0
209
+ ? this.times.reduce((a, b) => a + b, 0) / this.times.length
210
+ : 0;
211
+ const remaining = this.total - this.done;
212
+ const eta = avg > 0 && remaining > 0 ? avg * remaining : 0;
213
+ const spin = SPINNER_FRAMES[this.spinIdx];
214
+ const itemShort = truncate(this.currentItem, 64);
215
+ const pctStr = `${Math.round(pct)}%`.padStart(4);
216
+ const doneStr = `${this.done}/${this.total}`;
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}`,
228
+ ` ⏱ Elapsed : \x1B[33m${fmtMs(elapsed)}\x1B[0m ETA : \x1B[32m${eta > 0 ? '~' + fmtMs(eta) : remaining > 0 ? 'calculating…' : 'done!'}\x1B[0m`,
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
+ ``,
232
+ ];
233
+ const all = [...logSection, ...barSection];
234
+ process.stdout.write(all.join('\n'));
235
+ this.drawnLines = all.length;
236
+ }
237
+ finishPhase(summary) {
238
+ if (this.isTTY && this.drawnLines > 0) {
239
+ process.stdout.write(`\x1B[${this.drawnLines}A\x1B[0J`);
240
+ this.drawnLines = 0;
241
+ }
242
+ const elapsed = Date.now() - this.phaseStart;
243
+ const avg = this.times.length > 0
244
+ ? this.times.reduce((a, b) => a + b, 0) / this.times.length
245
+ : 0;
246
+ const checkmark = '\x1B[32m✔\x1B[0m';
247
+ console.log(` ${checkmark} ${summary} \x1B[90m(total: ${fmtMs(elapsed)}, avg: ${avg > 0 ? fmtMs(avg) : '—'}/item)\x1B[0m`);
248
+ }
249
+ }
84
250
  // Build standard taxonomy prompt text
85
- const TAXONOMY_PROMPT = `
86
- Choose node types from this taxonomy:
87
- - UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator
88
- - 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
89
- - EXPRESS/FASTIFY: route_handler | middleware | router
90
- - SPRING (Java): spring_controller | spring_service | spring_repository | spring_component | spring_bean | spring_config | spring_entity
91
- - DJANGO/FASTAPI: django_view | django_model | django_serializer | django_form | django_signal | fastapi_router | fastapi_dependency
92
- - GO: go_handler | go_middleware | go_struct | go_interface | go_func
93
- - RUST: rust_struct | rust_impl | rust_trait | rust_enum | rust_fn | rust_macro
94
- - REACT/NEXTJS: react_component | react_hook | react_context | react_hoc | react_page | next_page | next_layout | next_api_route | next_server_action
95
- - ORM: prisma_model | typeorm_entity | mongoose_model | sqlalchemy_model
96
- - REST/API/GRAPHQL: api_endpoint | rest_controller | graphql_resolver | graphql_query | graphql_mutation | graphql_schema
97
- - CLI: cli_command | cli_option
98
- - 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
99
265
  `;
266
+ // ── Vertex AI Authentication & Helper Functions ───────────────────────────
267
+ function base64UrlEncode(obj) {
268
+ return Buffer.from(JSON.stringify(obj))
269
+ .toString('base64')
270
+ .replace(/\+/g, '-')
271
+ .replace(/\//g, '_')
272
+ .replace(/=/g, '');
273
+ }
274
+ function getAccessTokenFromServiceAccount(sa) {
275
+ return new Promise((resolve, reject) => {
276
+ try {
277
+ const header = { alg: 'RS256', typ: 'JWT' };
278
+ const now = Math.floor(Date.now() / 1000);
279
+ const payload = {
280
+ iss: sa.client_email,
281
+ scope: 'https://www.googleapis.com/auth/cloud-platform',
282
+ aud: sa.token_uri || 'https://oauth2.googleapis.com/token',
283
+ exp: now + 3600,
284
+ iat: now
285
+ };
286
+ const dataToSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`;
287
+ const signer = crypto.createSign('RSA-SHA256');
288
+ signer.update(dataToSign);
289
+ const signature = signer.sign(sa.private_key, 'base64')
290
+ .replace(/\+/g, '-')
291
+ .replace(/\//g, '_')
292
+ .replace(/=/g, '');
293
+ const jwt = `${dataToSign}.${signature}`;
294
+ const tokenUri = sa.token_uri || 'https://oauth2.googleapis.com/token';
295
+ const body = `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`;
296
+ const url = new URL(tokenUri);
297
+ const req = https.request({
298
+ hostname: url.hostname,
299
+ port: url.port || 443,
300
+ path: url.pathname + url.search,
301
+ method: 'POST',
302
+ headers: {
303
+ 'Content-Type': 'application/x-www-form-urlencoded',
304
+ 'Content-Length': Buffer.byteLength(body)
305
+ }
306
+ }, (res) => {
307
+ let chunks = '';
308
+ res.on('data', (chunk) => {
309
+ chunks += chunk;
310
+ });
311
+ res.on('end', () => {
312
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
313
+ try {
314
+ const parsed = JSON.parse(chunks);
315
+ if (parsed.access_token) {
316
+ resolve(parsed.access_token);
317
+ }
318
+ else {
319
+ reject(new Error(`No access token in response: ${chunks}`));
320
+ }
321
+ }
322
+ catch (e) {
323
+ reject(e);
324
+ }
325
+ }
326
+ else {
327
+ reject(new Error(`Token request failed with status ${res.statusCode}: ${chunks}`));
328
+ }
329
+ });
330
+ });
331
+ req.on('error', (err) => {
332
+ reject(err);
333
+ });
334
+ req.write(body);
335
+ req.end();
336
+ }
337
+ catch (err) {
338
+ reject(err);
339
+ }
340
+ });
341
+ }
342
+ let cachedVertexToken = null;
343
+ let vertexTokenExpiry = 0; // Epoch ms
344
+ async function getVertexTokenCached(saData) {
345
+ const now = Date.now();
346
+ if (cachedVertexToken && vertexTokenExpiry > now + 300000) {
347
+ return cachedVertexToken;
348
+ }
349
+ const token = await getAccessTokenFromServiceAccount(saData);
350
+ cachedVertexToken = token;
351
+ vertexTokenExpiry = Date.now() + 3600 * 1000;
352
+ return token;
353
+ }
354
+ async function extractWithVertex(model, token, projectId, location, filePath, code) {
355
+ const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:generateContent`;
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.
374
+ 4. Be highly precise and return an empty JSON object if no code constructs are found.`;
375
+ const payload = {
376
+ contents: [
377
+ {
378
+ role: 'user',
379
+ parts: [
380
+ {
381
+ text: `File path: ${filePath}\n\nCode:\n${code}`
382
+ }
383
+ ]
384
+ }
385
+ ],
386
+ systemInstruction: {
387
+ parts: [
388
+ {
389
+ text: systemPrompt
390
+ }
391
+ ]
392
+ },
393
+ generationConfig: {
394
+ responseMimeType: 'application/json'
395
+ }
396
+ };
397
+ const responseText = await makeHttpRequest(url, 'POST', {
398
+ 'Content-Type': 'application/json',
399
+ 'Authorization': `Bearer ${token}`
400
+ }, JSON.stringify(payload));
401
+ const parsed = (0, json_1.safeJsonParse)(responseText, {});
402
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
403
+ if (!text) {
404
+ return {};
405
+ }
406
+ return (0, json_1.safeJsonParse)(text, {});
407
+ }
408
+ async function resolveConnectionsWithVertex(model, token, projectId, location, sourceNodeId, code, candidateNodeIds) {
409
+ const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:generateContent`;
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.
422
+ 4. If no connections are found, return an empty array.`;
423
+ const payload = {
424
+ contents: [
425
+ {
426
+ role: 'user',
427
+ parts: [
428
+ {
429
+ text: `Source Node ID: ${sourceNodeId}\n\nSource Code:\n${code}\n\nCandidate Target Node IDs in the Codebase:\n${JSON.stringify(candidateNodeIds, null, 2)}`
430
+ }
431
+ ]
432
+ }
433
+ ],
434
+ systemInstruction: {
435
+ parts: [
436
+ {
437
+ text: systemPrompt
438
+ }
439
+ ]
440
+ },
441
+ generationConfig: {
442
+ responseMimeType: 'application/json'
443
+ }
444
+ };
445
+ const responseText = await makeHttpRequest(url, 'POST', {
446
+ 'Content-Type': 'application/json',
447
+ 'Authorization': `Bearer ${token}`
448
+ }, JSON.stringify(payload));
449
+ const parsed = (0, json_1.safeJsonParse)(responseText, {});
450
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
451
+ if (!text) {
452
+ return [];
453
+ }
454
+ const result = (0, json_1.safeJsonParse)(text, {});
455
+ return result.connections || [];
456
+ }
100
457
  async function extractWithGemini(model, key, filePath, code) {
101
458
  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.
103
- Return ONLY a valid JSON object matching the schema:
104
- {
105
- "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" }
110
- ]
111
- }
112
- ${TAXONOMY_PROMPT}
113
- CRITICAL RULES:
114
- 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.`;
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.
477
+ 4. Be highly precise and return an empty JSON object if no code constructs are found.`;
117
478
  const payload = {
118
479
  contents: [
119
480
  {
@@ -136,29 +497,33 @@ CRITICAL RULES:
136
497
  }
137
498
  };
138
499
  const responseText = await makeHttpRequest(url, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
139
- const parsed = JSON.parse(responseText);
500
+ const parsed = (0, json_1.safeJsonParse)(responseText, {});
140
501
  const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
141
502
  if (!text) {
142
503
  return {};
143
504
  }
144
- return JSON.parse(text);
505
+ return (0, json_1.safeJsonParse)(text, {});
145
506
  }
146
507
  async function extractWithOllama(url, model, filePath, code) {
147
508
  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.
149
- Return ONLY a valid JSON object matching the schema:
150
- {
151
- "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" }
156
- ]
157
- }
158
- ${TAXONOMY_PROMPT}
159
- CRITICAL RULES:
160
- 1. ONLY extract constructs defined in this file. Do NOT extract third-party libraries or imports.
161
- 2. Return a clean, valid JSON object.`;
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".
526
+ 3. Return a clean, valid JSON object.`;
162
527
  const userPrompt = `File path: ${filePath}\n\nCode:\n${code}`;
163
528
  const payload = {
164
529
  model,
@@ -170,12 +535,100 @@ CRITICAL RULES:
170
535
  format: 'json'
171
536
  };
172
537
  const responseText = await makeHttpRequest(endpoint, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
173
- const parsed = JSON.parse(responseText);
538
+ const parsed = (0, json_1.safeJsonParse)(responseText, {});
174
539
  const text = parsed.message?.content;
175
540
  if (!text) {
176
541
  return {};
177
542
  }
178
- return JSON.parse(text);
543
+ return (0, json_1.safeJsonParse)(text, {});
544
+ }
545
+ async function resolveConnectionsWithGemini(model, key, sourceNodeId, code, candidateNodeIds) {
546
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
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.
559
+ 4. If no connections are found, return an empty array.`;
560
+ const payload = {
561
+ contents: [
562
+ {
563
+ parts: [
564
+ {
565
+ text: `Source Node ID: ${sourceNodeId}\n\nSource Code:\n${code}\n\nCandidate Target Node IDs in the Codebase:\n${JSON.stringify(candidateNodeIds, null, 2)}`
566
+ }
567
+ ]
568
+ }
569
+ ],
570
+ systemInstruction: {
571
+ parts: [
572
+ {
573
+ text: systemPrompt
574
+ }
575
+ ]
576
+ },
577
+ generationConfig: {
578
+ responseMimeType: 'application/json'
579
+ }
580
+ };
581
+ const responseText = await makeHttpRequest(url, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
582
+ const parsed = (0, json_1.safeJsonParse)(responseText, {});
583
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
584
+ if (!text) {
585
+ return [];
586
+ }
587
+ const result = (0, json_1.safeJsonParse)(text, {});
588
+ return result.connections || [];
589
+ }
590
+ async function resolveConnectionsWithOllama(url, model, sourceNodeId, code, candidateNodeIds) {
591
+ const endpoint = `${url.replace(/\/$/, '')}/api/chat`;
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.
602
+ 2. Return a clean, valid JSON object.`;
603
+ const userPrompt = `Source Node ID: ${sourceNodeId}\n\nSource Code:\n${code}\n\nCandidate Target Node IDs:\n${JSON.stringify(candidateNodeIds, null, 2)}`;
604
+ const payload = {
605
+ model,
606
+ messages: [
607
+ { role: 'system', content: systemPrompt },
608
+ { role: 'user', content: userPrompt }
609
+ ],
610
+ stream: false,
611
+ format: 'json'
612
+ };
613
+ const responseText = await makeHttpRequest(endpoint, 'POST', { 'Content-Type': 'application/json' }, JSON.stringify(payload));
614
+ const parsed = (0, json_1.safeJsonParse)(responseText, {});
615
+ const text = parsed.message?.content;
616
+ if (!text) {
617
+ return [];
618
+ }
619
+ const result = (0, json_1.safeJsonParse)(text, {});
620
+ return result.connections || [];
621
+ }
622
+ function filterCandidates(codeSnapshot, allNodeIds) {
623
+ const lowerCode = codeSnapshot.toLowerCase();
624
+ return allNodeIds.filter(id => {
625
+ const shortName = id.includes('.') ? id.split('.').pop() : id;
626
+ if (!shortName || shortName.trim().length === 0)
627
+ return false;
628
+ if (shortName.length < 3)
629
+ return false;
630
+ return lowerCode.includes(shortName.toLowerCase());
631
+ });
179
632
  }
180
633
  async function runBackgroundIndexing(opts) {
181
634
  const resolvedDevmind = path.resolve(opts.devmindPath);
@@ -183,6 +636,10 @@ async function runBackgroundIndexing(opts) {
183
636
  console.log(` Brain directory : ${resolvedDevmind}`);
184
637
  console.log(` Provider : ${opts.provider}`);
185
638
  let modelName = opts.model || '';
639
+ let vertexSaData = null;
640
+ let vertexToken = null;
641
+ let vertexProjectId = '';
642
+ let vertexLocation = 'us-central1';
186
643
  if (opts.provider === 'gemini') {
187
644
  modelName = modelName || 'gemini-2.0-flash';
188
645
  const apiKey = opts.key || process.env.GEMINI_API_KEY || '';
@@ -192,20 +649,61 @@ async function runBackgroundIndexing(opts) {
192
649
  }
193
650
  opts.key = apiKey;
194
651
  }
652
+ else if (opts.provider === 'vertex') {
653
+ modelName = modelName || 'gemini-1.5-flash';
654
+ const inputKey = opts.key || process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.VERTEX_API_KEY || process.env.GEMINI_API_KEY || '';
655
+ if (!inputKey) {
656
+ 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.');
657
+ process.exit(1);
658
+ }
659
+ try {
660
+ if (inputKey.trim().startsWith('{')) {
661
+ vertexSaData = JSON.parse(inputKey);
662
+ }
663
+ else if (fs.existsSync(inputKey)) {
664
+ vertexSaData = JSON.parse(fs.readFileSync(inputKey, 'utf-8'));
665
+ }
666
+ }
667
+ catch (e) {
668
+ // Treat as raw token
669
+ }
670
+ vertexProjectId = vertexSaData?.project_id || process.env.GCP_PROJECT_ID || process.env.VERTEX_PROJECT_ID || '';
671
+ vertexLocation = process.env.GCP_LOCATION || process.env.VERTEX_LOCATION || 'us-central1';
672
+ if (!vertexSaData && !inputKey.startsWith('ya29.')) {
673
+ console.error('❌ Error: Vertex key must be a valid Service Account JSON file path, inline JSON, or raw OAuth access token starting with "ya29."');
674
+ process.exit(1);
675
+ }
676
+ if (!vertexProjectId) {
677
+ 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.');
678
+ process.exit(1);
679
+ }
680
+ if (!vertexSaData) {
681
+ vertexToken = inputKey; // Raw Bearer token
682
+ }
683
+ }
195
684
  else {
196
685
  modelName = modelName || 'qwen2.5-coder';
197
686
  opts.url = opts.url || 'http://localhost:11434';
198
687
  }
688
+ const getVertexToken = async () => {
689
+ if (vertexToken)
690
+ return vertexToken;
691
+ if (vertexSaData) {
692
+ return await getVertexTokenCached(vertexSaData);
693
+ }
694
+ throw new Error('No Vertex credentials available');
695
+ };
199
696
  console.log(` Model : ${modelName}`);
200
- // 1. Scan for repos & files
697
+ // 1. Open DB
698
+ const dbFile = path.join(resolvedDevmind, 'brain.db');
699
+ const db = new database_1.DevMindDatabase(dbFile);
700
+ // 2. Scan for repos & files
201
701
  const { repos, total_files } = (0, scanner_1.scanRepoFiles)(resolvedDevmind);
202
702
  if (total_files === 0) {
203
703
  console.log('⚠️ No files found to index. Make sure config.json repositories are configured properly.');
704
+ db.close();
204
705
  return;
205
706
  }
206
- // 2. Open DB
207
- const dbFile = path.join(resolvedDevmind, 'brain.db');
208
- const db = new database_1.DevMindDatabase(dbFile);
209
707
  // 3. Read or create scratchpad
210
708
  let pad = (0, indexer_1.readScratchpad)(resolvedDevmind);
211
709
  if (!pad) {
@@ -216,137 +714,256 @@ async function runBackgroundIndexing(opts) {
216
714
  db.close();
217
715
  return;
218
716
  }
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;
717
+ // =========================================================================
718
+ // PHASE 1: NODE & CODE SNAPSHOT EXTRACTION
719
+ // =========================================================================
720
+ const progress = new ProgressDisplay();
721
+ if (pad.phase === 1) {
722
+ const reposDone = new Set(pad.repos_done);
723
+ const allFiles = [];
724
+ for (const repo of repos) {
725
+ if (reposDone.has(repo.repo_name))
726
+ continue;
727
+ for (const f of repo.files) {
728
+ allFiles.push({ repoName: repo.repo_name, absolutePath: f });
729
+ }
234
730
  }
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');
731
+ let startIndex = 0;
732
+ if (pad.last_file_indexed) {
733
+ const idx = allFiles.findIndex(f => f.absolutePath === pad.last_file_indexed);
734
+ if (idx !== -1)
735
+ startIndex = idx + 1;
248
736
  }
249
- catch (err) {
250
- console.warn(`⚠️ Warning: Failed to read file ${fileObj.absolutePath}: ${err.message}`);
251
- continue;
252
- }
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) {
737
+ // Use pad.files_total as true total so resume shows e.g. 14/1068, not 1/1055
738
+ progress.startPhase(1, 'Node & Code Extraction', pad.files_total, pad.files_done);
739
+ let fileIndex = startIndex;
740
+ for (; fileIndex < allFiles.length; fileIndex++) {
741
+ const fileObj = allFiles[fileIndex];
742
+ const relPath = path.relative(process.cwd(), fileObj.absolutePath);
743
+ progress.beginItem(relPath);
744
+ let code = '';
266
745
  try {
267
- if (opts.provider === 'gemini') {
268
- result = await extractWithGemini(modelName, opts.key, fileObj.absolutePath, code);
746
+ code = fs.readFileSync(fileObj.absolutePath, 'utf-8');
747
+ }
748
+ catch (err) {
749
+ progress.skipItem(`read error: ${err.message}`);
750
+ continue;
751
+ }
752
+ if (code.trim().length === 0) {
753
+ pad.files_done++;
754
+ pad.last_file_indexed = fileObj.absolutePath;
755
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
756
+ progress.skipItem('empty file');
757
+ continue;
758
+ }
759
+ const fileLines = code.split('\n').length;
760
+ progress.updateStatus(`Reading ${fileLines} lines — sending to AI…`);
761
+ let result = {};
762
+ let retries = 5;
763
+ let backoffMs = 10000;
764
+ while (retries > 0) {
765
+ try {
766
+ if (opts.provider === 'gemini') {
767
+ result = await extractWithGemini(modelName, opts.key, fileObj.absolutePath, code);
768
+ }
769
+ else if (opts.provider === 'vertex') {
770
+ const token = await getVertexToken();
771
+ result = await extractWithVertex(modelName, token, vertexProjectId, vertexLocation, fileObj.absolutePath, code);
772
+ }
773
+ else {
774
+ result = await extractWithOllama(opts.url, modelName, fileObj.absolutePath, code);
775
+ }
776
+ break;
269
777
  }
270
- else {
271
- result = await extractWithOllama(opts.url, modelName, fileObj.absolutePath, code);
778
+ catch (err) {
779
+ retries--;
780
+ if (retries === 0) {
781
+ progress.finishPhase(`Paused — API error. Run again to resume.`);
782
+ console.error(`❌ ${err.message}`);
783
+ db.close();
784
+ process.exit(1);
785
+ }
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
+ }
272
796
  }
273
- break;
274
797
  }
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);
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
+ }
803
+ if (result.nodes && Array.isArray(result.nodes)) {
804
+ for (const n of result.nodes) {
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}`);
818
+ db.upsertNode({
819
+ id: n.node_id,
820
+ name: n.name,
821
+ type: n.type,
822
+ file_path: fileObj.absolutePath,
823
+ signature: n.signature || null
824
+ });
825
+ newNodesCount++;
826
+ if (n.code_snapshot) {
827
+ db.updateHistory({
828
+ node_id: n.node_id,
829
+ code_snapshot: n.code_snapshot,
830
+ reasoning: {
831
+ what_changed: 'Initial code extraction during background indexing',
832
+ why: 'Initial index setup',
833
+ goal: 'Establish baseline codebase knowledge graph',
834
+ developer: 'devsmind background indexer',
835
+ model: modelName
836
+ }
837
+ });
838
+ progress.log(` \x1B[90m└ code snapshot saved (${n.code_snapshot.split('\n').length} lines)\x1B[0m`);
839
+ }
840
+ }
282
841
  }
283
- await sleep(2000);
284
842
  }
843
+ pad.files_done++;
844
+ pad.nodes_created += newNodesCount;
845
+ pad.last_file_indexed = fileObj.absolutePath;
846
+ pad.current_repo = fileObj.repoName;
847
+ pad.updated_at = new Date().toISOString();
848
+ const currentRepoFiles = repos.find(r => r.repo_name === fileObj.repoName)?.files || [];
849
+ const isRepoDone = currentRepoFiles.length > 0 && currentRepoFiles[currentRepoFiles.length - 1] === fileObj.absolutePath;
850
+ if (isRepoDone && !pad.repos_done.includes(fileObj.repoName)) {
851
+ pad.repos_done.push(fileObj.repoName);
852
+ }
853
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
854
+ progress.completeItem(`${pad.nodes_created} node(s) found so far`);
855
+ if (opts.provider === 'gemini' || opts.provider === 'vertex')
856
+ await sleep(2000);
857
+ else
858
+ await sleep(200);
285
859
  }
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++;
860
+ // Transition to Phase 2
861
+ const activeNodes = db.listNodes();
862
+ pad.phase = 2;
863
+ pad.nodes_total = activeNodes.length;
864
+ pad.nodes_done = 0;
865
+ pad.updated_at = new Date().toISOString();
866
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
867
+ progress.finishPhase(`Phase 1 done — ${activeNodes.length} node(s) extracted from ${pad.files_done} file(s)`);
868
+ }
869
+ // =========================================================================
870
+ // PHASE 2: AI CONNECTION RESOLUTION / LINKING
871
+ // =========================================================================
872
+ if (pad.phase === 2) {
873
+ const activeNodes = db.listNodes();
874
+ const allNodeIds = activeNodes.map(n => n.id);
875
+ const resumeIndex = pad.nodes_done || 0;
876
+ // Use total node count and resume offset so bar shows true progress
877
+ progress.startPhase(2, 'AI Connection Resolution', activeNodes.length, resumeIndex);
878
+ let nodeIndex = resumeIndex;
879
+ for (; nodeIndex < activeNodes.length; nodeIndex++) {
880
+ const node = activeNodes[nodeIndex];
881
+ progress.beginItem(node.id);
882
+ const latestCode = db.getLatestCode(node.id);
883
+ if (!latestCode || !latestCode.code_snapshot || latestCode.code_snapshot.trim().length === 0) {
884
+ pad.nodes_done = nodeIndex + 1;
885
+ pad.updated_at = new Date().toISOString();
886
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
887
+ progress.skipItem('no code snapshot');
888
+ continue;
889
+ }
890
+ const candidates = filterCandidates(latestCode.code_snapshot, allNodeIds);
891
+ const filteredCandidates = candidates.filter(id => id !== node.id);
892
+ if (filteredCandidates.length === 0) {
893
+ pad.nodes_done = nodeIndex + 1;
894
+ pad.updated_at = new Date().toISOString();
895
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
896
+ progress.skipItem('no matching candidates');
897
+ continue;
898
+ }
899
+ let connections = [];
900
+ let retries = 5;
901
+ let backoffMs = 10000;
902
+ while (retries > 0) {
903
+ try {
904
+ if (opts.provider === 'gemini') {
905
+ connections = await resolveConnectionsWithGemini(modelName, opts.key, node.id, latestCode.code_snapshot, filteredCandidates);
906
+ }
907
+ else if (opts.provider === 'vertex') {
908
+ const token = await getVertexToken();
909
+ connections = await resolveConnectionsWithVertex(modelName, token, vertexProjectId, vertexLocation, node.id, latestCode.code_snapshot, filteredCandidates);
910
+ }
911
+ else {
912
+ connections = await resolveConnectionsWithOllama(opts.url, modelName, node.id, latestCode.code_snapshot, filteredCandidates);
913
+ }
914
+ break;
915
+ }
916
+ catch (err) {
917
+ retries--;
918
+ if (retries === 0) {
919
+ progress.finishPhase('Paused — API error. Run again to resume.');
920
+ console.error(`❌ ${err.message}`);
921
+ db.close();
922
+ process.exit(1);
923
+ }
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
+ }
300
934
  }
301
935
  }
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++;
936
+ let addedCount = 0;
937
+ for (const targetId of connections) {
938
+ if (allNodeIds.includes(targetId)) {
939
+ progress.log(`Linked: \x1B[36m${node.id}\x1B[0m \x1B[36m${targetId}\x1B[0m`);
940
+ db.addConnection(node.id, targetId);
941
+ addedCount++;
308
942
  }
309
943
  }
944
+ pad.nodes_done = nodeIndex + 1;
945
+ pad.connections_created += addedCount;
946
+ pad.updated_at = new Date().toISOString();
947
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
948
+ progress.completeItem(`${pad.connections_created} connection(s) created so far`);
949
+ if (opts.provider === 'gemini' || opts.provider === 'vertex')
950
+ await sleep(2000);
951
+ else
952
+ await sleep(200);
310
953
  }
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
- }
954
+ progress.finishPhase(`Phase 2 done — ${pad.connections_created} connection(s) linked across ${pad.nodes_total} node(s)`);
341
955
  }
342
- // Mark complete
343
- (0, indexer_1.completeScratchpad)(resolvedDevmind);
956
+ // Mark indexing session as fully complete
957
+ pad.status = 'complete';
958
+ pad.updated_at = new Date().toISOString();
959
+ (0, indexer_1.writeScratchpad)(resolvedDevmind, pad);
344
960
  db.vacuum();
345
961
  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');
962
+ console.log('');
963
+ console.log('\x1B[1m\x1B[32m ✔ Indexing complete!\x1B[0m');
964
+ console.log(` ├─ Files indexed : \x1B[33m${pad.files_done}\x1B[0m`);
965
+ console.log(` ├─ Nodes created : \x1B[33m${pad.nodes_created}\x1B[0m`);
966
+ console.log(` └─ Connections : \x1B[33m${pad.connections_created}\x1B[0m`);
967
+ console.log('');
351
968
  }
352
969
  //# sourceMappingURL=runner.js.map