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.
- package/LICENSE +21 -21
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/runner.d.ts +1 -1
- package/dist/cli/runner.js +783 -166
- package/dist/cli/runner.js.map +1 -1
- package/dist/db/database.d.ts +7 -1
- package/dist/db/database.js +60 -19
- package/dist/db/database.js.map +1 -1
- package/dist/db/indexer.d.ts +3 -0
- package/dist/db/indexer.js +3 -0
- package/dist/db/indexer.js.map +1 -1
- package/dist/db/schema.js +33 -33
- package/dist/mcp/server.js +22 -39
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/visualizer_2d.html +635 -635
- package/dist/mcp/visualizer_3d.html +628 -628
- package/dist/utils/json.d.ts +16 -0
- package/dist/utils/json.js +151 -0
- package/dist/utils/json.js.map +1 -0
- package/package.json +7 -3
package/dist/cli/runner.js
CHANGED
|
@@ -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)
|
|
103
|
-
Return ONLY a valid JSON object matching the schema:
|
|
104
|
-
{
|
|
105
|
-
"nodes": [
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 =
|
|
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
|
|
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)
|
|
149
|
-
Return ONLY a valid JSON object matching the schema:
|
|
150
|
-
{
|
|
151
|
-
"nodes": [
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
db.addConnection(
|
|
307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
347
|
-
console.log(
|
|
348
|
-
console.log(`
|
|
349
|
-
console.log(`
|
|
350
|
-
console.log(
|
|
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
|