gitnexus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +181 -0
  2. package/dist/cli/ai-context.d.ts +21 -0
  3. package/dist/cli/ai-context.js +219 -0
  4. package/dist/cli/analyze.d.ts +10 -0
  5. package/dist/cli/analyze.js +118 -0
  6. package/dist/cli/clean.d.ts +8 -0
  7. package/dist/cli/clean.js +29 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/index.js +42 -0
  10. package/dist/cli/list.d.ts +6 -0
  11. package/dist/cli/list.js +27 -0
  12. package/dist/cli/mcp.d.ts +7 -0
  13. package/dist/cli/mcp.js +85 -0
  14. package/dist/cli/serve.d.ts +3 -0
  15. package/dist/cli/serve.js +5 -0
  16. package/dist/cli/status.d.ts +6 -0
  17. package/dist/cli/status.js +27 -0
  18. package/dist/config/ignore-service.d.ts +1 -0
  19. package/dist/config/ignore-service.js +208 -0
  20. package/dist/config/supported-languages.d.ts +11 -0
  21. package/dist/config/supported-languages.js +15 -0
  22. package/dist/core/embeddings/embedder.d.ts +60 -0
  23. package/dist/core/embeddings/embedder.js +205 -0
  24. package/dist/core/embeddings/embedding-pipeline.d.ts +50 -0
  25. package/dist/core/embeddings/embedding-pipeline.js +321 -0
  26. package/dist/core/embeddings/index.d.ts +9 -0
  27. package/dist/core/embeddings/index.js +9 -0
  28. package/dist/core/embeddings/text-generator.d.ts +24 -0
  29. package/dist/core/embeddings/text-generator.js +182 -0
  30. package/dist/core/embeddings/types.d.ts +87 -0
  31. package/dist/core/embeddings/types.js +32 -0
  32. package/dist/core/graph/graph.d.ts +2 -0
  33. package/dist/core/graph/graph.js +61 -0
  34. package/dist/core/graph/types.d.ts +50 -0
  35. package/dist/core/graph/types.js +1 -0
  36. package/dist/core/ingestion/ast-cache.d.ts +11 -0
  37. package/dist/core/ingestion/ast-cache.js +34 -0
  38. package/dist/core/ingestion/call-processor.d.ts +8 -0
  39. package/dist/core/ingestion/call-processor.js +269 -0
  40. package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
  41. package/dist/core/ingestion/cluster-enricher.js +170 -0
  42. package/dist/core/ingestion/community-processor.d.ts +39 -0
  43. package/dist/core/ingestion/community-processor.js +269 -0
  44. package/dist/core/ingestion/entry-point-scoring.d.ts +39 -0
  45. package/dist/core/ingestion/entry-point-scoring.js +235 -0
  46. package/dist/core/ingestion/filesystem-walker.d.ts +5 -0
  47. package/dist/core/ingestion/filesystem-walker.js +26 -0
  48. package/dist/core/ingestion/framework-detection.d.ts +38 -0
  49. package/dist/core/ingestion/framework-detection.js +183 -0
  50. package/dist/core/ingestion/heritage-processor.d.ts +14 -0
  51. package/dist/core/ingestion/heritage-processor.js +134 -0
  52. package/dist/core/ingestion/import-processor.d.ts +8 -0
  53. package/dist/core/ingestion/import-processor.js +490 -0
  54. package/dist/core/ingestion/parsing-processor.d.ts +8 -0
  55. package/dist/core/ingestion/parsing-processor.js +249 -0
  56. package/dist/core/ingestion/pipeline.d.ts +2 -0
  57. package/dist/core/ingestion/pipeline.js +228 -0
  58. package/dist/core/ingestion/process-processor.d.ts +51 -0
  59. package/dist/core/ingestion/process-processor.js +278 -0
  60. package/dist/core/ingestion/structure-processor.d.ts +2 -0
  61. package/dist/core/ingestion/structure-processor.js +36 -0
  62. package/dist/core/ingestion/symbol-table.d.ts +33 -0
  63. package/dist/core/ingestion/symbol-table.js +38 -0
  64. package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -0
  65. package/dist/core/ingestion/tree-sitter-queries.js +319 -0
  66. package/dist/core/ingestion/utils.d.ts +10 -0
  67. package/dist/core/ingestion/utils.js +44 -0
  68. package/dist/core/kuzu/csv-generator.d.ts +22 -0
  69. package/dist/core/kuzu/csv-generator.js +272 -0
  70. package/dist/core/kuzu/kuzu-adapter.d.ts +81 -0
  71. package/dist/core/kuzu/kuzu-adapter.js +568 -0
  72. package/dist/core/kuzu/schema.d.ts +53 -0
  73. package/dist/core/kuzu/schema.js +380 -0
  74. package/dist/core/search/bm25-index.d.ts +22 -0
  75. package/dist/core/search/bm25-index.js +52 -0
  76. package/dist/core/search/hybrid-search.d.ts +49 -0
  77. package/dist/core/search/hybrid-search.js +118 -0
  78. package/dist/core/tree-sitter/parser-loader.d.ts +4 -0
  79. package/dist/core/tree-sitter/parser-loader.js +42 -0
  80. package/dist/lib/utils.d.ts +1 -0
  81. package/dist/lib/utils.js +3 -0
  82. package/dist/mcp/core/embedder.d.ts +27 -0
  83. package/dist/mcp/core/embedder.js +93 -0
  84. package/dist/mcp/core/kuzu-adapter.d.ts +23 -0
  85. package/dist/mcp/core/kuzu-adapter.js +62 -0
  86. package/dist/mcp/local/local-backend.d.ts +73 -0
  87. package/dist/mcp/local/local-backend.js +752 -0
  88. package/dist/mcp/resources.d.ts +31 -0
  89. package/dist/mcp/resources.js +279 -0
  90. package/dist/mcp/server.d.ts +12 -0
  91. package/dist/mcp/server.js +130 -0
  92. package/dist/mcp/staleness.d.ts +15 -0
  93. package/dist/mcp/staleness.js +29 -0
  94. package/dist/mcp/tools.d.ts +24 -0
  95. package/dist/mcp/tools.js +160 -0
  96. package/dist/server/api.d.ts +6 -0
  97. package/dist/server/api.js +156 -0
  98. package/dist/storage/git.d.ts +7 -0
  99. package/dist/storage/git.js +39 -0
  100. package/dist/storage/repo-manager.d.ts +61 -0
  101. package/dist/storage/repo-manager.js +106 -0
  102. package/dist/types/pipeline.d.ts +28 -0
  103. package/dist/types/pipeline.js +16 -0
  104. package/package.json +80 -0
  105. package/skills/debugging.md +104 -0
  106. package/skills/exploring.md +112 -0
  107. package/skills/impact-analysis.md +114 -0
  108. package/skills/refactoring.md +119 -0
  109. package/vendor/leiden/index.cjs +355 -0
  110. package/vendor/leiden/utils.cjs +392 -0
@@ -0,0 +1,752 @@
1
+ /**
2
+ * Local Backend
3
+ *
4
+ * Provides tool implementations using local .gitnexus/ index.
5
+ * This enables MCP to work without the browser.
6
+ */
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { initKuzu, executeQuery, closeKuzu, isKuzuReady } from '../core/kuzu-adapter.js';
10
+ import { embedQuery, getEmbeddingDims, disposeEmbedder } from '../core/embedder.js';
11
+ import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
12
+ import { getStoragePaths as getRepoStoragePaths, saveMeta as saveRepoMeta, loadMeta as loadRepoMeta, addToGitignore, } from '../../storage/repo-manager.js';
13
+ import { generateAIContextFiles } from '../../cli/ai-context.js';
14
+ /**
15
+ * Quick test-file detection for filtering impact results.
16
+ * Matches common test file patterns across all supported languages.
17
+ */
18
+ function isTestFilePath(filePath) {
19
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
20
+ return (p.includes('.test.') || p.includes('.spec.') ||
21
+ p.includes('__tests__/') || p.includes('__mocks__/') ||
22
+ p.includes('/test/') || p.includes('/tests/') ||
23
+ p.includes('/testing/') || p.includes('/fixtures/') ||
24
+ p.endsWith('_test.go') || p.endsWith('_test.py') ||
25
+ p.includes('/test_') || p.includes('/conftest.'));
26
+ }
27
+ const GITNEXUS_DIR = '.gitnexus';
28
+ function getStoragePaths(repoPath) {
29
+ const storagePath = path.join(path.resolve(repoPath), GITNEXUS_DIR);
30
+ return {
31
+ storagePath,
32
+ kuzuPath: path.join(storagePath, 'kuzu'),
33
+ metaPath: path.join(storagePath, 'meta.json'),
34
+ };
35
+ }
36
+ async function loadMeta(storagePath) {
37
+ try {
38
+ // Verify both meta.json and kuzu exist for a valid index
39
+ const metaPath = path.join(storagePath, 'meta.json');
40
+ const kuzuPath = path.join(storagePath, 'kuzu');
41
+ // Check kuzu exists (can be file or directory depending on how it was saved)
42
+ try {
43
+ await fs.stat(kuzuPath);
44
+ }
45
+ catch {
46
+ return null; // kuzu doesn't exist
47
+ }
48
+ // Load and parse meta.json
49
+ const raw = await fs.readFile(metaPath, 'utf-8');
50
+ return JSON.parse(raw);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ async function loadRepo(repoPath) {
57
+ const paths = getStoragePaths(repoPath);
58
+ const meta = await loadMeta(paths.storagePath);
59
+ if (!meta)
60
+ return null;
61
+ return {
62
+ repoPath: path.resolve(repoPath),
63
+ ...paths,
64
+ meta,
65
+ };
66
+ }
67
+ export async function findRepo(startPath) {
68
+ let current = path.resolve(startPath);
69
+ const root = path.parse(current).root;
70
+ while (current !== root) {
71
+ const repo = await loadRepo(current);
72
+ if (repo)
73
+ return repo;
74
+ current = path.dirname(current);
75
+ }
76
+ return null;
77
+ }
78
+ export class LocalBackend {
79
+ repo = null;
80
+ _context = null;
81
+ initialized = false;
82
+ async init(cwd) {
83
+ this.repo = await findRepo(cwd);
84
+ if (!this.repo)
85
+ return false;
86
+ const stats = this.repo.meta.stats || {};
87
+ this._context = {
88
+ projectName: path.basename(this.repo.repoPath),
89
+ stats: {
90
+ fileCount: stats.files || 0,
91
+ functionCount: stats.nodes || 0,
92
+ classCount: 0,
93
+ interfaceCount: 0,
94
+ methodCount: 0,
95
+ communityCount: stats.communities || 0,
96
+ processCount: stats.processes || 0,
97
+ },
98
+ hotspots: [],
99
+ folderTree: '',
100
+ };
101
+ return true;
102
+ }
103
+ async ensureInitialized() {
104
+ if (this.initialized || !this.repo)
105
+ return;
106
+ await initKuzu(this.repo.kuzuPath);
107
+ this.initialized = true;
108
+ }
109
+ get context() {
110
+ return this._context;
111
+ }
112
+ get isReady() {
113
+ return this.repo !== null;
114
+ }
115
+ get repoPath() {
116
+ return this.repo?.repoPath || null;
117
+ }
118
+ get storagePath() {
119
+ return this.repo?.storagePath || null;
120
+ }
121
+ get meta() {
122
+ return this.repo?.meta || null;
123
+ }
124
+ async callTool(method, params) {
125
+ if (!this.repo) {
126
+ throw new Error('Repository not indexed. Run: gitnexus analyze');
127
+ }
128
+ switch (method) {
129
+ case 'search':
130
+ return this.search(params);
131
+ case 'cypher':
132
+ return this.cypher(params);
133
+ case 'overview':
134
+ return this.overview(params);
135
+ case 'explore':
136
+ return this.explore(params);
137
+ case 'impact':
138
+ return this.impact(params);
139
+ case 'analyze':
140
+ return this.analyze(params);
141
+ default:
142
+ throw new Error(`Unknown tool: ${method}`);
143
+ }
144
+ }
145
+ async search(params) {
146
+ await this.ensureInitialized();
147
+ const limit = params.limit || 10;
148
+ const query = params.query;
149
+ const depth = params.depth || 'definitions';
150
+ // Run BM25 and semantic search in parallel
151
+ const [bm25Results, semanticResults] = await Promise.all([
152
+ this.bm25Search(query, limit * 2),
153
+ this.semanticSearch(query, limit * 2),
154
+ ]);
155
+ // Merge and deduplicate results using reciprocal rank fusion
156
+ const scoreMap = new Map();
157
+ // BM25 results
158
+ for (let i = 0; i < bm25Results.length; i++) {
159
+ const result = bm25Results[i];
160
+ const key = result.filePath;
161
+ const rrfScore = 1 / (60 + i); // RRF formula with k=60
162
+ const existing = scoreMap.get(key);
163
+ if (existing) {
164
+ existing.score += rrfScore;
165
+ existing.source = 'hybrid';
166
+ }
167
+ else {
168
+ scoreMap.set(key, { score: rrfScore, source: 'bm25', data: result });
169
+ }
170
+ }
171
+ // Semantic results
172
+ for (let i = 0; i < semanticResults.length; i++) {
173
+ const result = semanticResults[i];
174
+ const key = result.filePath;
175
+ const rrfScore = 1 / (60 + i);
176
+ const existing = scoreMap.get(key);
177
+ if (existing) {
178
+ existing.score += rrfScore;
179
+ existing.source = 'hybrid';
180
+ }
181
+ else {
182
+ scoreMap.set(key, { score: rrfScore, source: 'semantic', data: result });
183
+ }
184
+ }
185
+ // Sort by fused score and take top results
186
+ const merged = Array.from(scoreMap.entries())
187
+ .sort((a, b) => b[1].score - a[1].score)
188
+ .slice(0, limit);
189
+ // Enrich with graph data
190
+ const results = [];
191
+ for (const [_, item] of merged) {
192
+ const result = item.data;
193
+ result.searchSource = item.source;
194
+ result.fusedScore = item.score;
195
+ // Add cluster membership context for each result with a nodeId
196
+ if (result.nodeId) {
197
+ try {
198
+ const clusterQuery = `
199
+ MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
200
+ RETURN c.label AS label, c.heuristicLabel AS heuristicLabel
201
+ LIMIT 1
202
+ `;
203
+ const clusters = await executeQuery(clusterQuery);
204
+ if (clusters.length > 0) {
205
+ result.cluster = {
206
+ label: clusters[0].label || clusters[0][0],
207
+ heuristicLabel: clusters[0].heuristicLabel || clusters[0][1],
208
+ };
209
+ }
210
+ }
211
+ catch {
212
+ // Cluster lookup failed - continue without it
213
+ }
214
+ }
215
+ // Add relationships if depth is 'full' and we have a node ID
216
+ if (depth === 'full' && result.nodeId) {
217
+ try {
218
+ const relQuery = `
219
+ MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[r:CodeRelation]->(m)
220
+ RETURN r.type AS type, m.name AS targetName, m.filePath AS targetPath
221
+ LIMIT 5
222
+ `;
223
+ const rels = await executeQuery(relQuery);
224
+ result.connections = rels.map((rel) => ({
225
+ type: rel.type || rel[0],
226
+ name: rel.targetName || rel[1],
227
+ path: rel.targetPath || rel[2],
228
+ }));
229
+ }
230
+ catch {
231
+ result.connections = [];
232
+ }
233
+ }
234
+ results.push(result);
235
+ }
236
+ return results;
237
+ }
238
+ /**
239
+ * BM25 keyword search helper - uses KuzuDB FTS for always-fresh results
240
+ */
241
+ async bm25Search(query, limit) {
242
+ // Import dynamically to avoid circular dependency
243
+ const { searchFTSFromKuzu } = await import('../../core/search/bm25-index.js');
244
+ const bm25Results = await searchFTSFromKuzu(query, limit);
245
+ const results = [];
246
+ for (const bm25Result of bm25Results) {
247
+ const fileName = bm25Result.filePath.split('/').pop() || bm25Result.filePath;
248
+ try {
249
+ const symbolQuery = `
250
+ MATCH (n)
251
+ WHERE n.filePath CONTAINS '${fileName.replace(/'/g, "''")}'
252
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
253
+ LIMIT 3
254
+ `;
255
+ const symbols = await executeQuery(symbolQuery);
256
+ if (symbols.length > 0) {
257
+ for (const sym of symbols) {
258
+ results.push({
259
+ nodeId: sym.id || sym[0],
260
+ name: sym.name || sym[1],
261
+ type: sym.type || sym[2],
262
+ filePath: sym.filePath || sym[3],
263
+ startLine: sym.startLine || sym[4],
264
+ endLine: sym.endLine || sym[5],
265
+ bm25Score: bm25Result.score,
266
+ });
267
+ }
268
+ }
269
+ else {
270
+ results.push({
271
+ name: fileName,
272
+ type: 'File',
273
+ filePath: bm25Result.filePath,
274
+ bm25Score: bm25Result.score,
275
+ });
276
+ }
277
+ }
278
+ catch {
279
+ results.push({
280
+ name: fileName,
281
+ type: 'File',
282
+ filePath: bm25Result.filePath,
283
+ bm25Score: bm25Result.score,
284
+ });
285
+ }
286
+ }
287
+ return results;
288
+ }
289
+ /**
290
+ * Semantic vector search helper
291
+ */
292
+ async semanticSearch(query, limit) {
293
+ try {
294
+ // Embed the query
295
+ const queryVec = await embedQuery(query);
296
+ const dims = getEmbeddingDims();
297
+ const queryVecStr = `[${queryVec.join(',')}]`;
298
+ // Query vector index
299
+ const vectorQuery = `
300
+ CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
301
+ CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
302
+ YIELD node AS emb, distance
303
+ WITH emb, distance
304
+ WHERE distance < 0.6
305
+ RETURN emb.nodeId AS nodeId, distance
306
+ ORDER BY distance
307
+ `;
308
+ const embResults = await executeQuery(vectorQuery);
309
+ if (embResults.length === 0)
310
+ return [];
311
+ // Get metadata for each result
312
+ const results = [];
313
+ for (const embRow of embResults) {
314
+ const nodeId = embRow.nodeId ?? embRow[0];
315
+ const distance = embRow.distance ?? embRow[1];
316
+ // Extract label from node ID
317
+ const labelEndIdx = nodeId.indexOf(':');
318
+ const label = labelEndIdx > 0 ? nodeId.substring(0, labelEndIdx) : 'Unknown';
319
+ try {
320
+ const nodeQuery = label === 'File'
321
+ ? `MATCH (n:File {id: '${nodeId.replace(/'/g, "''")}'}) RETURN n.name AS name, n.filePath AS filePath`
322
+ : `MATCH (n:${label} {id: '${nodeId.replace(/'/g, "''")}'}) RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine`;
323
+ const nodeRows = await executeQuery(nodeQuery);
324
+ if (nodeRows.length > 0) {
325
+ const nodeRow = nodeRows[0];
326
+ results.push({
327
+ nodeId,
328
+ name: nodeRow.name ?? nodeRow[0] ?? '',
329
+ type: label,
330
+ filePath: nodeRow.filePath ?? nodeRow[1] ?? '',
331
+ distance,
332
+ startLine: label !== 'File' ? (nodeRow.startLine ?? nodeRow[2]) : undefined,
333
+ endLine: label !== 'File' ? (nodeRow.endLine ?? nodeRow[3]) : undefined,
334
+ });
335
+ }
336
+ }
337
+ catch { }
338
+ }
339
+ return results;
340
+ }
341
+ catch (err) {
342
+ // Semantic search unavailable (no embeddings or model not loaded)
343
+ console.error('GitNexus: Semantic search unavailable -', err.message);
344
+ return [];
345
+ }
346
+ }
347
+ async cypher(params) {
348
+ await this.ensureInitialized();
349
+ if (!isKuzuReady()) {
350
+ return { error: 'KuzuDB not ready. Index may be corrupted.' };
351
+ }
352
+ try {
353
+ const result = await executeQuery(params.query);
354
+ return result;
355
+ }
356
+ catch (err) {
357
+ return { error: err.message || 'Query failed' };
358
+ }
359
+ }
360
+ async overview(params) {
361
+ await this.ensureInitialized();
362
+ const limit = params.limit || 20;
363
+ const result = {
364
+ repoPath: this.repo.repoPath,
365
+ stats: this.repo.meta.stats,
366
+ indexedAt: this.repo.meta.indexedAt,
367
+ lastCommit: this.repo.meta.lastCommit,
368
+ };
369
+ if (params.showClusters !== false) {
370
+ try {
371
+ const clusters = await executeQuery(`
372
+ MATCH (c:Community)
373
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
374
+ ORDER BY c.symbolCount DESC
375
+ LIMIT ${limit}
376
+ `);
377
+ result.clusters = clusters.map((c) => ({
378
+ id: c.id || c[0],
379
+ label: c.label || c[1],
380
+ heuristicLabel: c.heuristicLabel || c[2],
381
+ cohesion: c.cohesion || c[3],
382
+ symbolCount: c.symbolCount || c[4],
383
+ }));
384
+ }
385
+ catch {
386
+ result.clusters = [];
387
+ }
388
+ }
389
+ if (params.showProcesses !== false) {
390
+ try {
391
+ const processes = await executeQuery(`
392
+ MATCH (p:Process)
393
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
394
+ ORDER BY p.stepCount DESC
395
+ LIMIT ${limit}
396
+ `);
397
+ result.processes = processes.map((p) => ({
398
+ id: p.id || p[0],
399
+ label: p.label || p[1],
400
+ heuristicLabel: p.heuristicLabel || p[2],
401
+ processType: p.processType || p[3],
402
+ stepCount: p.stepCount || p[4],
403
+ }));
404
+ }
405
+ catch {
406
+ result.processes = [];
407
+ }
408
+ }
409
+ return result;
410
+ }
411
+ async explore(params) {
412
+ await this.ensureInitialized();
413
+ const { name, type } = params;
414
+ if (type === 'symbol') {
415
+ // Find symbol and its context
416
+ const symbolQuery = `
417
+ MATCH (n)
418
+ WHERE n.name = '${name.replace(/'/g, "''")}'
419
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
420
+ LIMIT 1
421
+ `;
422
+ const symbols = await executeQuery(symbolQuery);
423
+ if (symbols.length === 0)
424
+ return { error: `Symbol '${name}' not found` };
425
+ const sym = symbols[0];
426
+ const symId = sym.id || sym[0];
427
+ // Get callers
428
+ const callersQuery = `
429
+ MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${symId}'})
430
+ RETURN caller.name AS name, caller.filePath AS filePath
431
+ LIMIT 10
432
+ `;
433
+ const callers = await executeQuery(callersQuery);
434
+ // Get callees
435
+ const calleesQuery = `
436
+ MATCH (n {id: '${symId}'})-[:CodeRelation {type: 'CALLS'}]->(callee)
437
+ RETURN callee.name AS name, callee.filePath AS filePath
438
+ LIMIT 10
439
+ `;
440
+ const callees = await executeQuery(calleesQuery);
441
+ // Get community
442
+ const communityQuery = `
443
+ MATCH (n {id: '${symId}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
444
+ RETURN c.label AS label, c.heuristicLabel AS heuristicLabel
445
+ LIMIT 1
446
+ `;
447
+ const communities = await executeQuery(communityQuery);
448
+ return {
449
+ symbol: {
450
+ id: symId,
451
+ name: sym.name || sym[1],
452
+ type: sym.type || sym[2],
453
+ filePath: sym.filePath || sym[3],
454
+ startLine: sym.startLine || sym[4],
455
+ endLine: sym.endLine || sym[5],
456
+ },
457
+ callers: callers.map((c) => ({ name: c.name || c[0], filePath: c.filePath || c[1] })),
458
+ callees: callees.map((c) => ({ name: c.name || c[0], filePath: c.filePath || c[1] })),
459
+ community: communities.length > 0 ? {
460
+ label: communities[0].label || communities[0][0],
461
+ heuristicLabel: communities[0].heuristicLabel || communities[0][1],
462
+ } : null,
463
+ };
464
+ }
465
+ if (type === 'cluster') {
466
+ const clusterQuery = `
467
+ MATCH (c:Community)
468
+ WHERE c.label = '${name.replace(/'/g, "''")}' OR c.heuristicLabel = '${name.replace(/'/g, "''")}'
469
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
470
+ LIMIT 1
471
+ `;
472
+ const clusters = await executeQuery(clusterQuery);
473
+ if (clusters.length === 0)
474
+ return { error: `Cluster '${name}' not found` };
475
+ const cluster = clusters[0];
476
+ const clusterId = cluster.id || cluster[0];
477
+ const membersQuery = `
478
+ MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c {id: '${clusterId}'})
479
+ RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
480
+ LIMIT 20
481
+ `;
482
+ const members = await executeQuery(membersQuery);
483
+ return {
484
+ cluster: {
485
+ id: clusterId,
486
+ label: cluster.label || cluster[1],
487
+ heuristicLabel: cluster.heuristicLabel || cluster[2],
488
+ cohesion: cluster.cohesion || cluster[3],
489
+ symbolCount: cluster.symbolCount || cluster[4],
490
+ },
491
+ members: members.map((m) => ({
492
+ name: m.name || m[0],
493
+ type: m.type || m[1],
494
+ filePath: m.filePath || m[2],
495
+ })),
496
+ };
497
+ }
498
+ if (type === 'process') {
499
+ const processQuery = `
500
+ MATCH (p:Process)
501
+ WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
502
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, p.entryPointId AS entryPointId, p.terminalId AS terminalId
503
+ LIMIT 1
504
+ `;
505
+ const processes = await executeQuery(processQuery);
506
+ if (processes.length === 0)
507
+ return { error: `Process '${name}' not found` };
508
+ const proc = processes[0];
509
+ const procId = proc.id || proc[0];
510
+ const stepsQuery = `
511
+ MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
512
+ RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
513
+ ORDER BY r.step
514
+ `;
515
+ const steps = await executeQuery(stepsQuery);
516
+ return {
517
+ process: {
518
+ id: procId,
519
+ label: proc.label || proc[1],
520
+ heuristicLabel: proc.heuristicLabel || proc[2],
521
+ processType: proc.processType || proc[3],
522
+ stepCount: proc.stepCount || proc[4],
523
+ },
524
+ steps: steps.map((s) => ({
525
+ step: s.step || s[3],
526
+ name: s.name || s[0],
527
+ type: s.type || s[1],
528
+ filePath: s.filePath || s[2],
529
+ })),
530
+ };
531
+ }
532
+ return { error: 'Invalid type. Use: symbol, cluster, or process' };
533
+ }
534
+ async impact(params) {
535
+ await this.ensureInitialized();
536
+ const { target, direction } = params;
537
+ const maxDepth = params.maxDepth || 3;
538
+ const relationTypes = params.relationTypes && params.relationTypes.length > 0
539
+ ? params.relationTypes
540
+ : ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
541
+ const includeTests = params.includeTests ?? false;
542
+ const minConfidence = params.minConfidence ?? 0;
543
+ // Build the relation type filter for Cypher
544
+ const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
545
+ const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
546
+ // Find target symbol
547
+ const targetQuery = `
548
+ MATCH (n)
549
+ WHERE n.name = '${target.replace(/'/g, "''")}'
550
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
551
+ LIMIT 1
552
+ `;
553
+ const targets = await executeQuery(targetQuery);
554
+ if (targets.length === 0)
555
+ return { error: `Target '${target}' not found` };
556
+ const sym = targets[0];
557
+ const symId = sym.id || sym[0];
558
+ // BFS to find impacted nodes
559
+ const impacted = [];
560
+ const visited = new Set([symId]);
561
+ let frontier = [symId];
562
+ for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
563
+ const nextFrontier = [];
564
+ for (const nodeId of frontier) {
565
+ const query = direction === 'upstream'
566
+ ? `MATCH (caller)-[r:CodeRelation]->(n {id: '${nodeId}'}) WHERE r.type IN [${relTypeFilter}]${confidenceFilter} RETURN caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence`
567
+ : `MATCH (n {id: '${nodeId}'})-[r:CodeRelation]->(callee) WHERE r.type IN [${relTypeFilter}]${confidenceFilter} RETURN callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
568
+ const related = await executeQuery(query);
569
+ for (const rel of related) {
570
+ const relId = rel.id || rel[0];
571
+ const filePath = rel.filePath || rel[3] || '';
572
+ // Skip test files unless explicitly included
573
+ if (!includeTests && isTestFilePath(filePath))
574
+ continue;
575
+ if (!visited.has(relId)) {
576
+ visited.add(relId);
577
+ nextFrontier.push(relId);
578
+ impacted.push({
579
+ depth,
580
+ id: relId,
581
+ name: rel.name || rel[1],
582
+ type: rel.type || rel[2],
583
+ filePath,
584
+ relationType: rel.relType || rel[4],
585
+ confidence: rel.confidence || rel[5] || 1.0,
586
+ });
587
+ }
588
+ }
589
+ }
590
+ frontier = nextFrontier;
591
+ }
592
+ // Group by depth
593
+ const grouped = {};
594
+ for (const item of impacted) {
595
+ if (!grouped[item.depth])
596
+ grouped[item.depth] = [];
597
+ grouped[item.depth].push(item);
598
+ }
599
+ return {
600
+ target: {
601
+ id: symId,
602
+ name: sym.name || sym[1],
603
+ type: sym.type || sym[2],
604
+ filePath: sym.filePath || sym[3],
605
+ },
606
+ direction,
607
+ impactedCount: impacted.length,
608
+ byDepth: grouped,
609
+ };
610
+ }
611
+ async analyze(params) {
612
+ // Determine target repo path
613
+ let repoPath;
614
+ if (params.path) {
615
+ repoPath = path.resolve(params.path);
616
+ }
617
+ else if (this.repo) {
618
+ repoPath = this.repo.repoPath;
619
+ }
620
+ else {
621
+ const gitRoot = getGitRoot(process.cwd());
622
+ if (!gitRoot) {
623
+ return { error: 'Not inside a git repository' };
624
+ }
625
+ repoPath = gitRoot;
626
+ }
627
+ if (!isGitRepo(repoPath)) {
628
+ return { error: 'Not a git repository' };
629
+ }
630
+ const { storagePath, kuzuPath } = getRepoStoragePaths(repoPath);
631
+ const currentCommit = getCurrentCommit(repoPath);
632
+ const existingMeta = await loadRepoMeta(storagePath);
633
+ // Skip if already indexed at same commit (unless force)
634
+ if (existingMeta && !params.force && existingMeta.lastCommit === currentCommit) {
635
+ return { status: 'up_to_date', message: 'Repository already up to date.' };
636
+ }
637
+ // Close MCP's persistent connection before pipeline takes over
638
+ await closeKuzu();
639
+ this.initialized = false;
640
+ try {
641
+ // Import pipeline modules dynamically to avoid circular deps
642
+ const { runPipelineFromRepo } = await import('../../core/ingestion/pipeline.js');
643
+ const coreKuzu = await import('../../core/kuzu/kuzu-adapter.js');
644
+ // Run ingestion pipeline
645
+ console.error('GitNexus: Running indexing pipeline...');
646
+ const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
647
+ if (progress.percent % 20 === 0) {
648
+ console.error(`GitNexus: ${progress.phase} ${progress.percent}%`);
649
+ }
650
+ });
651
+ // Load graph into KuzuDB
652
+ console.error('GitNexus: Loading graph into KuzuDB...');
653
+ await coreKuzu.initKuzu(kuzuPath);
654
+ await coreKuzu.loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath);
655
+ // Create FTS indexes
656
+ console.error('GitNexus: Creating FTS indexes...');
657
+ try {
658
+ await coreKuzu.createFTSIndex('File', 'file_fts', ['name', 'content']);
659
+ await coreKuzu.createFTSIndex('Function', 'function_fts', ['name', 'content']);
660
+ await coreKuzu.createFTSIndex('Class', 'class_fts', ['name', 'content']);
661
+ await coreKuzu.createFTSIndex('Method', 'method_fts', ['name', 'content']);
662
+ }
663
+ catch (e) {
664
+ console.error('GitNexus: Some FTS indexes may not have been created:', e.message);
665
+ }
666
+ // Generate embeddings (unless skipped)
667
+ if (!params.skipEmbeddings) {
668
+ try {
669
+ console.error('GitNexus: Generating embeddings...');
670
+ const { runEmbeddingPipeline } = await import('../../core/embeddings/embedding-pipeline.js');
671
+ await runEmbeddingPipeline(coreKuzu.executeQuery, coreKuzu.executeWithReusedStatement, (progress) => {
672
+ if (progress.percent % 25 === 0) {
673
+ console.error(`GitNexus: Embeddings ${progress.percent}%`);
674
+ }
675
+ });
676
+ }
677
+ catch (e) {
678
+ console.error('GitNexus: Embedding generation failed (non-fatal):', e.message);
679
+ }
680
+ }
681
+ // Save metadata
682
+ const stats = await coreKuzu.getKuzuStats();
683
+ await saveRepoMeta(storagePath, {
684
+ repoPath,
685
+ lastCommit: currentCommit,
686
+ indexedAt: new Date().toISOString(),
687
+ stats: {
688
+ files: pipelineResult.fileContents.size,
689
+ nodes: stats.nodes,
690
+ edges: stats.edges,
691
+ communities: pipelineResult.communityResult?.stats.totalCommunities,
692
+ processes: pipelineResult.processResult?.stats.totalProcesses,
693
+ },
694
+ });
695
+ // Add .gitnexus to .gitignore
696
+ await addToGitignore(repoPath);
697
+ // Generate AI context files
698
+ const projectName = path.basename(repoPath);
699
+ await generateAIContextFiles(repoPath, storagePath, projectName, {
700
+ files: pipelineResult.fileContents.size,
701
+ nodes: stats.nodes,
702
+ edges: stats.edges,
703
+ communities: pipelineResult.communityResult?.stats.totalCommunities,
704
+ processes: pipelineResult.processResult?.stats.totalProcesses,
705
+ });
706
+ // Close core kuzu connection (pipeline is done)
707
+ await coreKuzu.closeKuzu();
708
+ // Re-init MCP state so next tool call reconnects
709
+ this.repo = await loadRepo(repoPath);
710
+ if (this.repo) {
711
+ const repoStats = this.repo.meta.stats || {};
712
+ this._context = {
713
+ projectName: path.basename(this.repo.repoPath),
714
+ stats: {
715
+ fileCount: repoStats.files || 0,
716
+ functionCount: repoStats.nodes || 0,
717
+ classCount: 0,
718
+ interfaceCount: 0,
719
+ methodCount: 0,
720
+ communityCount: repoStats.communities || 0,
721
+ processCount: repoStats.processes || 0,
722
+ },
723
+ hotspots: [],
724
+ folderTree: '',
725
+ };
726
+ }
727
+ console.error('GitNexus: Indexing complete!');
728
+ return {
729
+ status: 'success',
730
+ message: `Repository indexed successfully.`,
731
+ stats: {
732
+ files: pipelineResult.fileContents.size,
733
+ nodes: stats.nodes,
734
+ edges: stats.edges,
735
+ communities: pipelineResult.communityResult?.stats.totalCommunities,
736
+ processes: pipelineResult.processResult?.stats.totalProcesses,
737
+ },
738
+ };
739
+ }
740
+ catch (e) {
741
+ console.error('GitNexus: Indexing failed:', e.message);
742
+ return { error: `Indexing failed: ${e.message}` };
743
+ }
744
+ }
745
+ async disconnect() {
746
+ closeKuzu();
747
+ await disposeEmbedder();
748
+ this.repo = null;
749
+ this._context = null;
750
+ this.initialized = false;
751
+ }
752
+ }