gitnexus 1.2.6 → 1.2.7
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/README.md +186 -187
- package/dist/cli/ai-context.js +71 -71
- package/dist/cli/eval-server.js +1 -1
- package/dist/cli/mcp.js +1 -1
- package/dist/core/embeddings/embedder.js +6 -0
- package/dist/core/ingestion/filesystem-walker.js +17 -3
- package/dist/core/ingestion/parsing-processor.js +4 -1
- package/dist/core/ingestion/tree-sitter-queries.js +282 -282
- package/dist/core/ingestion/workers/parse-worker.js +13 -4
- package/dist/core/ingestion/workers/worker-pool.js +43 -9
- package/dist/core/kuzu/schema.js +256 -256
- package/dist/core/search/bm25-index.js +5 -5
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/mcp/local/local-backend.d.ts +18 -3
- package/dist/mcp/local/local-backend.js +169 -125
- package/dist/mcp/resources.js +46 -46
- package/dist/mcp/server.js +16 -16
- package/dist/mcp/tools.js +77 -77
- package/package.json +82 -82
- package/skills/debugging.md +85 -85
- package/skills/exploring.md +75 -75
- package/skills/impact-analysis.md +94 -94
- package/skills/refactoring.md +113 -113
|
@@ -44,9 +44,20 @@ export class LocalBackend {
|
|
|
44
44
|
* Returns true if at least one repo is available.
|
|
45
45
|
*/
|
|
46
46
|
async init() {
|
|
47
|
+
await this.refreshRepos();
|
|
48
|
+
return this.repos.size > 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Re-read the global registry and update the in-memory repo map.
|
|
52
|
+
* New repos are added, existing repos are updated, removed repos are pruned.
|
|
53
|
+
* KuzuDB connections for removed repos are NOT closed (they idle-timeout naturally).
|
|
54
|
+
*/
|
|
55
|
+
async refreshRepos() {
|
|
47
56
|
const entries = await listRegisteredRepos({ validate: true });
|
|
57
|
+
const freshIds = new Set();
|
|
48
58
|
for (const entry of entries) {
|
|
49
59
|
const id = this.repoId(entry.name, entry.path);
|
|
60
|
+
freshIds.add(id);
|
|
50
61
|
const storagePath = entry.storagePath;
|
|
51
62
|
const kuzuPath = path.join(storagePath, 'kuzu');
|
|
52
63
|
const handle = {
|
|
@@ -72,7 +83,14 @@ export class LocalBackend {
|
|
|
72
83
|
},
|
|
73
84
|
});
|
|
74
85
|
}
|
|
75
|
-
|
|
86
|
+
// Prune repos that no longer exist in the registry
|
|
87
|
+
for (const id of this.repos.keys()) {
|
|
88
|
+
if (!freshIds.has(id)) {
|
|
89
|
+
this.repos.delete(id);
|
|
90
|
+
this.contextCache.delete(id);
|
|
91
|
+
this.initializedRepos.delete(id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
76
94
|
}
|
|
77
95
|
/**
|
|
78
96
|
* Generate a stable repo ID from name + path.
|
|
@@ -96,11 +114,36 @@ export class LocalBackend {
|
|
|
96
114
|
* - If repoParam is given, match by name or path
|
|
97
115
|
* - If only 1 repo, use it
|
|
98
116
|
* - If 0 or multiple without param, throw with helpful message
|
|
117
|
+
*
|
|
118
|
+
* On a miss, re-reads the registry once in case a new repo was indexed
|
|
119
|
+
* while the MCP server was running.
|
|
99
120
|
*/
|
|
100
|
-
resolveRepo(repoParam) {
|
|
121
|
+
async resolveRepo(repoParam) {
|
|
122
|
+
const result = this.resolveRepoFromCache(repoParam);
|
|
123
|
+
if (result)
|
|
124
|
+
return result;
|
|
125
|
+
// Miss — refresh registry and try once more
|
|
126
|
+
await this.refreshRepos();
|
|
127
|
+
const retried = this.resolveRepoFromCache(repoParam);
|
|
128
|
+
if (retried)
|
|
129
|
+
return retried;
|
|
130
|
+
// Still no match — throw with helpful message
|
|
101
131
|
if (this.repos.size === 0) {
|
|
102
132
|
throw new Error('No indexed repositories. Run: gitnexus analyze');
|
|
103
133
|
}
|
|
134
|
+
if (repoParam) {
|
|
135
|
+
const names = [...this.repos.values()].map(h => h.name);
|
|
136
|
+
throw new Error(`Repository "${repoParam}" not found. Available: ${names.join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
const names = [...this.repos.values()].map(h => h.name);
|
|
139
|
+
throw new Error(`Multiple repositories indexed. Specify which one with the "repo" parameter. Available: ${names.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Try to resolve a repo from the in-memory cache. Returns null on miss.
|
|
143
|
+
*/
|
|
144
|
+
resolveRepoFromCache(repoParam) {
|
|
145
|
+
if (this.repos.size === 0)
|
|
146
|
+
return null;
|
|
104
147
|
if (repoParam) {
|
|
105
148
|
const paramLower = repoParam.toLowerCase();
|
|
106
149
|
// Match by id
|
|
@@ -122,14 +165,12 @@ export class LocalBackend {
|
|
|
122
165
|
if (handle.name.toLowerCase().includes(paramLower))
|
|
123
166
|
return handle;
|
|
124
167
|
}
|
|
125
|
-
|
|
126
|
-
throw new Error(`Repository "${repoParam}" not found. Available: ${names.join(', ')}`);
|
|
168
|
+
return null;
|
|
127
169
|
}
|
|
128
170
|
if (this.repos.size === 1) {
|
|
129
171
|
return this.repos.values().next().value;
|
|
130
172
|
}
|
|
131
|
-
|
|
132
|
-
throw new Error(`Multiple repositories indexed. Specify which one with the "repo" parameter. Available: ${names.join(', ')}`);
|
|
173
|
+
return null; // Multiple repos, no param — ambiguous
|
|
133
174
|
}
|
|
134
175
|
// ─── Lazy KuzuDB Init ────────────────────────────────────────────
|
|
135
176
|
async ensureInitialized(repoId) {
|
|
@@ -164,8 +205,11 @@ export class LocalBackend {
|
|
|
164
205
|
}
|
|
165
206
|
/**
|
|
166
207
|
* List all registered repos with their metadata.
|
|
208
|
+
* Re-reads the global registry so newly indexed repos are discovered
|
|
209
|
+
* without restarting the MCP server.
|
|
167
210
|
*/
|
|
168
|
-
listRepos() {
|
|
211
|
+
async listRepos() {
|
|
212
|
+
await this.refreshRepos();
|
|
169
213
|
return [...this.repos.values()].map(h => ({
|
|
170
214
|
name: h.name,
|
|
171
215
|
path: h.repoPath,
|
|
@@ -179,8 +223,8 @@ export class LocalBackend {
|
|
|
179
223
|
if (method === 'list_repos') {
|
|
180
224
|
return this.listRepos();
|
|
181
225
|
}
|
|
182
|
-
// Resolve repo from optional param
|
|
183
|
-
const repo = this.resolveRepo(params?.repo);
|
|
226
|
+
// Resolve repo from optional param (re-reads registry on miss)
|
|
227
|
+
const repo = await this.resolveRepo(params?.repo);
|
|
184
228
|
switch (method) {
|
|
185
229
|
case 'query':
|
|
186
230
|
return this.query(repo, params);
|
|
@@ -276,19 +320,19 @@ export class LocalBackend {
|
|
|
276
320
|
// Find processes this symbol participates in
|
|
277
321
|
let processRows = [];
|
|
278
322
|
try {
|
|
279
|
-
processRows = await executeQuery(repo.id, `
|
|
280
|
-
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
281
|
-
RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
323
|
+
processRows = await executeQuery(repo.id, `
|
|
324
|
+
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
325
|
+
RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
282
326
|
`);
|
|
283
327
|
}
|
|
284
328
|
catch { /* symbol might not be in any process */ }
|
|
285
329
|
// Get cluster cohesion as internal ranking signal (never exposed)
|
|
286
330
|
let cohesion = 0;
|
|
287
331
|
try {
|
|
288
|
-
const cohesionRows = await executeQuery(repo.id, `
|
|
289
|
-
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
290
|
-
RETURN c.cohesion AS cohesion
|
|
291
|
-
LIMIT 1
|
|
332
|
+
const cohesionRows = await executeQuery(repo.id, `
|
|
333
|
+
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
334
|
+
RETURN c.cohesion AS cohesion
|
|
335
|
+
LIMIT 1
|
|
292
336
|
`);
|
|
293
337
|
if (cohesionRows.length > 0) {
|
|
294
338
|
cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0;
|
|
@@ -299,9 +343,9 @@ export class LocalBackend {
|
|
|
299
343
|
let content;
|
|
300
344
|
if (includeContent) {
|
|
301
345
|
try {
|
|
302
|
-
const contentRows = await executeQuery(repo.id, `
|
|
303
|
-
MATCH (n {id: '${escaped}'})
|
|
304
|
-
RETURN n.content AS content
|
|
346
|
+
const contentRows = await executeQuery(repo.id, `
|
|
347
|
+
MATCH (n {id: '${escaped}'})
|
|
348
|
+
RETURN n.content AS content
|
|
305
349
|
`);
|
|
306
350
|
if (contentRows.length > 0) {
|
|
307
351
|
content = contentRows[0].content ?? contentRows[0][0];
|
|
@@ -406,11 +450,11 @@ export class LocalBackend {
|
|
|
406
450
|
for (const bm25Result of bm25Results) {
|
|
407
451
|
const fullPath = bm25Result.filePath;
|
|
408
452
|
try {
|
|
409
|
-
const symbolQuery = `
|
|
410
|
-
MATCH (n)
|
|
411
|
-
WHERE n.filePath = '${fullPath.replace(/'/g, "''")}'
|
|
412
|
-
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
|
|
413
|
-
LIMIT 3
|
|
453
|
+
const symbolQuery = `
|
|
454
|
+
MATCH (n)
|
|
455
|
+
WHERE n.filePath = '${fullPath.replace(/'/g, "''")}'
|
|
456
|
+
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
|
|
457
|
+
LIMIT 3
|
|
414
458
|
`;
|
|
415
459
|
const symbols = await executeQuery(repo.id, symbolQuery);
|
|
416
460
|
if (symbols.length > 0) {
|
|
@@ -456,14 +500,14 @@ export class LocalBackend {
|
|
|
456
500
|
const queryVec = await embedQuery(query);
|
|
457
501
|
const dims = getEmbeddingDims();
|
|
458
502
|
const queryVecStr = `[${queryVec.join(',')}]`;
|
|
459
|
-
const vectorQuery = `
|
|
460
|
-
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
|
|
461
|
-
CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
|
|
462
|
-
YIELD node AS emb, distance
|
|
463
|
-
WITH emb, distance
|
|
464
|
-
WHERE distance < 0.6
|
|
465
|
-
RETURN emb.nodeId AS nodeId, distance
|
|
466
|
-
ORDER BY distance
|
|
503
|
+
const vectorQuery = `
|
|
504
|
+
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
|
|
505
|
+
CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
|
|
506
|
+
YIELD node AS emb, distance
|
|
507
|
+
WITH emb, distance
|
|
508
|
+
WHERE distance < 0.6
|
|
509
|
+
RETURN emb.nodeId AS nodeId, distance
|
|
510
|
+
ORDER BY distance
|
|
467
511
|
`;
|
|
468
512
|
const embResults = await executeQuery(repo.id, vectorQuery);
|
|
469
513
|
if (embResults.length === 0)
|
|
@@ -568,11 +612,11 @@ export class LocalBackend {
|
|
|
568
612
|
try {
|
|
569
613
|
// Fetch more raw communities than the display limit so aggregation has enough data
|
|
570
614
|
const rawLimit = Math.max(limit * 5, 200);
|
|
571
|
-
const clusters = await executeQuery(repo.id, `
|
|
572
|
-
MATCH (c:Community)
|
|
573
|
-
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
574
|
-
ORDER BY c.symbolCount DESC
|
|
575
|
-
LIMIT ${rawLimit}
|
|
615
|
+
const clusters = await executeQuery(repo.id, `
|
|
616
|
+
MATCH (c:Community)
|
|
617
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
618
|
+
ORDER BY c.symbolCount DESC
|
|
619
|
+
LIMIT ${rawLimit}
|
|
576
620
|
`);
|
|
577
621
|
const rawClusters = clusters.map((c) => ({
|
|
578
622
|
id: c.id || c[0],
|
|
@@ -589,11 +633,11 @@ export class LocalBackend {
|
|
|
589
633
|
}
|
|
590
634
|
if (params.showProcesses !== false) {
|
|
591
635
|
try {
|
|
592
|
-
const processes = await executeQuery(repo.id, `
|
|
593
|
-
MATCH (p:Process)
|
|
594
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
595
|
-
ORDER BY p.stepCount DESC
|
|
596
|
-
LIMIT ${limit}
|
|
636
|
+
const processes = await executeQuery(repo.id, `
|
|
637
|
+
MATCH (p:Process)
|
|
638
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
639
|
+
ORDER BY p.stepCount DESC
|
|
640
|
+
LIMIT ${limit}
|
|
597
641
|
`);
|
|
598
642
|
result.processes = processes.map((p) => ({
|
|
599
643
|
id: p.id || p[0],
|
|
@@ -624,10 +668,10 @@ export class LocalBackend {
|
|
|
624
668
|
let symbols;
|
|
625
669
|
if (uid) {
|
|
626
670
|
const escaped = uid.replace(/'/g, "''");
|
|
627
|
-
symbols = await executeQuery(repo.id, `
|
|
628
|
-
MATCH (n {id: '${escaped}'})
|
|
629
|
-
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${include_content ? ', n.content AS content' : ''}
|
|
630
|
-
LIMIT 1
|
|
671
|
+
symbols = await executeQuery(repo.id, `
|
|
672
|
+
MATCH (n {id: '${escaped}'})
|
|
673
|
+
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${include_content ? ', n.content AS content' : ''}
|
|
674
|
+
LIMIT 1
|
|
631
675
|
`);
|
|
632
676
|
}
|
|
633
677
|
else {
|
|
@@ -644,10 +688,10 @@ export class LocalBackend {
|
|
|
644
688
|
else {
|
|
645
689
|
whereClause = `WHERE n.name = '${escaped}'`;
|
|
646
690
|
}
|
|
647
|
-
symbols = await executeQuery(repo.id, `
|
|
648
|
-
MATCH (n) ${whereClause}
|
|
649
|
-
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${include_content ? ', n.content AS content' : ''}
|
|
650
|
-
LIMIT 10
|
|
691
|
+
symbols = await executeQuery(repo.id, `
|
|
692
|
+
MATCH (n) ${whereClause}
|
|
693
|
+
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${include_content ? ', n.content AS content' : ''}
|
|
694
|
+
LIMIT 10
|
|
651
695
|
`);
|
|
652
696
|
}
|
|
653
697
|
if (symbols.length === 0) {
|
|
@@ -671,25 +715,25 @@ export class LocalBackend {
|
|
|
671
715
|
const sym = symbols[0];
|
|
672
716
|
const symId = (sym.id || sym[0]).replace(/'/g, "''");
|
|
673
717
|
// Categorized incoming refs
|
|
674
|
-
const incomingRows = await executeQuery(repo.id, `
|
|
675
|
-
MATCH (caller)-[r:CodeRelation]->(n {id: '${symId}'})
|
|
676
|
-
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
677
|
-
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
678
|
-
LIMIT 30
|
|
718
|
+
const incomingRows = await executeQuery(repo.id, `
|
|
719
|
+
MATCH (caller)-[r:CodeRelation]->(n {id: '${symId}'})
|
|
720
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
721
|
+
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
722
|
+
LIMIT 30
|
|
679
723
|
`);
|
|
680
724
|
// Categorized outgoing refs
|
|
681
|
-
const outgoingRows = await executeQuery(repo.id, `
|
|
682
|
-
MATCH (n {id: '${symId}'})-[r:CodeRelation]->(target)
|
|
683
|
-
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
684
|
-
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
685
|
-
LIMIT 30
|
|
725
|
+
const outgoingRows = await executeQuery(repo.id, `
|
|
726
|
+
MATCH (n {id: '${symId}'})-[r:CodeRelation]->(target)
|
|
727
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
728
|
+
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
729
|
+
LIMIT 30
|
|
686
730
|
`);
|
|
687
731
|
// Process participation
|
|
688
732
|
let processRows = [];
|
|
689
733
|
try {
|
|
690
|
-
processRows = await executeQuery(repo.id, `
|
|
691
|
-
MATCH (n {id: '${symId}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
692
|
-
RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
734
|
+
processRows = await executeQuery(repo.id, `
|
|
735
|
+
MATCH (n {id: '${symId}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
736
|
+
RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
693
737
|
`);
|
|
694
738
|
}
|
|
695
739
|
catch { /* no process info */ }
|
|
@@ -743,10 +787,10 @@ export class LocalBackend {
|
|
|
743
787
|
}
|
|
744
788
|
if (type === 'cluster') {
|
|
745
789
|
const escaped = name.replace(/'/g, "''");
|
|
746
|
-
const clusterQuery = `
|
|
747
|
-
MATCH (c:Community)
|
|
748
|
-
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
749
|
-
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
790
|
+
const clusterQuery = `
|
|
791
|
+
MATCH (c:Community)
|
|
792
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
793
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
750
794
|
`;
|
|
751
795
|
const clusters = await executeQuery(repo.id, clusterQuery);
|
|
752
796
|
if (clusters.length === 0)
|
|
@@ -761,11 +805,11 @@ export class LocalBackend {
|
|
|
761
805
|
totalSymbols += s;
|
|
762
806
|
weightedCohesion += (c.cohesion || 0) * s;
|
|
763
807
|
}
|
|
764
|
-
const members = await executeQuery(repo.id, `
|
|
765
|
-
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
766
|
-
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
767
|
-
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
768
|
-
LIMIT 30
|
|
808
|
+
const members = await executeQuery(repo.id, `
|
|
809
|
+
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
810
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
811
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
812
|
+
LIMIT 30
|
|
769
813
|
`);
|
|
770
814
|
return {
|
|
771
815
|
cluster: {
|
|
@@ -782,20 +826,20 @@ export class LocalBackend {
|
|
|
782
826
|
};
|
|
783
827
|
}
|
|
784
828
|
if (type === 'process') {
|
|
785
|
-
const processes = await executeQuery(repo.id, `
|
|
786
|
-
MATCH (p:Process)
|
|
787
|
-
WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
|
|
788
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
789
|
-
LIMIT 1
|
|
829
|
+
const processes = await executeQuery(repo.id, `
|
|
830
|
+
MATCH (p:Process)
|
|
831
|
+
WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
|
|
832
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
833
|
+
LIMIT 1
|
|
790
834
|
`);
|
|
791
835
|
if (processes.length === 0)
|
|
792
836
|
return { error: `Process '${name}' not found` };
|
|
793
837
|
const proc = processes[0];
|
|
794
838
|
const procId = proc.id || proc[0];
|
|
795
|
-
const steps = await executeQuery(repo.id, `
|
|
796
|
-
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
797
|
-
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
798
|
-
ORDER BY r.step
|
|
839
|
+
const steps = await executeQuery(repo.id, `
|
|
840
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
841
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
842
|
+
ORDER BY r.step
|
|
799
843
|
`);
|
|
800
844
|
return {
|
|
801
845
|
process: {
|
|
@@ -856,10 +900,10 @@ export class LocalBackend {
|
|
|
856
900
|
for (const file of changedFiles) {
|
|
857
901
|
const escaped = file.replace(/\\/g, '/').replace(/'/g, "''");
|
|
858
902
|
try {
|
|
859
|
-
const symbols = await executeQuery(repo.id, `
|
|
860
|
-
MATCH (n) WHERE n.filePath CONTAINS '${escaped}'
|
|
861
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
862
|
-
LIMIT 20
|
|
903
|
+
const symbols = await executeQuery(repo.id, `
|
|
904
|
+
MATCH (n) WHERE n.filePath CONTAINS '${escaped}'
|
|
905
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
906
|
+
LIMIT 20
|
|
863
907
|
`);
|
|
864
908
|
for (const sym of symbols) {
|
|
865
909
|
changedSymbols.push({
|
|
@@ -878,9 +922,9 @@ export class LocalBackend {
|
|
|
878
922
|
for (const sym of changedSymbols) {
|
|
879
923
|
const escaped = sym.id.replace(/'/g, "''");
|
|
880
924
|
try {
|
|
881
|
-
const procs = await executeQuery(repo.id, `
|
|
882
|
-
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
883
|
-
RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
925
|
+
const procs = await executeQuery(repo.id, `
|
|
926
|
+
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
927
|
+
RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
884
928
|
`);
|
|
885
929
|
for (const proc of procs) {
|
|
886
930
|
const pid = proc.pid || proc[0];
|
|
@@ -1055,11 +1099,11 @@ export class LocalBackend {
|
|
|
1055
1099
|
const minConfidence = params.minConfidence ?? 0;
|
|
1056
1100
|
const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
|
|
1057
1101
|
const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
|
|
1058
|
-
const targetQuery = `
|
|
1059
|
-
MATCH (n)
|
|
1060
|
-
WHERE n.name = '${target.replace(/'/g, "''")}'
|
|
1061
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1062
|
-
LIMIT 1
|
|
1102
|
+
const targetQuery = `
|
|
1103
|
+
MATCH (n)
|
|
1104
|
+
WHERE n.name = '${target.replace(/'/g, "''")}'
|
|
1105
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1106
|
+
LIMIT 1
|
|
1063
1107
|
`;
|
|
1064
1108
|
const targets = await executeQuery(repo.id, targetQuery);
|
|
1065
1109
|
if (targets.length === 0)
|
|
@@ -1125,15 +1169,15 @@ export class LocalBackend {
|
|
|
1125
1169
|
* Used by getClustersResource — avoids legacy overview() dispatch.
|
|
1126
1170
|
*/
|
|
1127
1171
|
async queryClusters(repoName, limit = 100) {
|
|
1128
|
-
const repo = this.resolveRepo(repoName);
|
|
1172
|
+
const repo = await this.resolveRepo(repoName);
|
|
1129
1173
|
await this.ensureInitialized(repo.id);
|
|
1130
1174
|
try {
|
|
1131
1175
|
const rawLimit = Math.max(limit * 5, 200);
|
|
1132
|
-
const clusters = await executeQuery(repo.id, `
|
|
1133
|
-
MATCH (c:Community)
|
|
1134
|
-
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1135
|
-
ORDER BY c.symbolCount DESC
|
|
1136
|
-
LIMIT ${rawLimit}
|
|
1176
|
+
const clusters = await executeQuery(repo.id, `
|
|
1177
|
+
MATCH (c:Community)
|
|
1178
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1179
|
+
ORDER BY c.symbolCount DESC
|
|
1180
|
+
LIMIT ${rawLimit}
|
|
1137
1181
|
`);
|
|
1138
1182
|
const rawClusters = clusters.map((c) => ({
|
|
1139
1183
|
id: c.id || c[0],
|
|
@@ -1153,14 +1197,14 @@ export class LocalBackend {
|
|
|
1153
1197
|
* Used by getProcessesResource — avoids legacy overview() dispatch.
|
|
1154
1198
|
*/
|
|
1155
1199
|
async queryProcesses(repoName, limit = 50) {
|
|
1156
|
-
const repo = this.resolveRepo(repoName);
|
|
1200
|
+
const repo = await this.resolveRepo(repoName);
|
|
1157
1201
|
await this.ensureInitialized(repo.id);
|
|
1158
1202
|
try {
|
|
1159
|
-
const processes = await executeQuery(repo.id, `
|
|
1160
|
-
MATCH (p:Process)
|
|
1161
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1162
|
-
ORDER BY p.stepCount DESC
|
|
1163
|
-
LIMIT ${limit}
|
|
1203
|
+
const processes = await executeQuery(repo.id, `
|
|
1204
|
+
MATCH (p:Process)
|
|
1205
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1206
|
+
ORDER BY p.stepCount DESC
|
|
1207
|
+
LIMIT ${limit}
|
|
1164
1208
|
`);
|
|
1165
1209
|
return {
|
|
1166
1210
|
processes: processes.map((p) => ({
|
|
@@ -1181,13 +1225,13 @@ export class LocalBackend {
|
|
|
1181
1225
|
* Used by getClusterDetailResource.
|
|
1182
1226
|
*/
|
|
1183
1227
|
async queryClusterDetail(name, repoName) {
|
|
1184
|
-
const repo = this.resolveRepo(repoName);
|
|
1228
|
+
const repo = await this.resolveRepo(repoName);
|
|
1185
1229
|
await this.ensureInitialized(repo.id);
|
|
1186
1230
|
const escaped = name.replace(/'/g, "''");
|
|
1187
|
-
const clusterQuery = `
|
|
1188
|
-
MATCH (c:Community)
|
|
1189
|
-
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
1190
|
-
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1231
|
+
const clusterQuery = `
|
|
1232
|
+
MATCH (c:Community)
|
|
1233
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
1234
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1191
1235
|
`;
|
|
1192
1236
|
const clusters = await executeQuery(repo.id, clusterQuery);
|
|
1193
1237
|
if (clusters.length === 0)
|
|
@@ -1202,11 +1246,11 @@ export class LocalBackend {
|
|
|
1202
1246
|
totalSymbols += s;
|
|
1203
1247
|
weightedCohesion += (c.cohesion || 0) * s;
|
|
1204
1248
|
}
|
|
1205
|
-
const members = await executeQuery(repo.id, `
|
|
1206
|
-
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
1207
|
-
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
1208
|
-
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1209
|
-
LIMIT 30
|
|
1249
|
+
const members = await executeQuery(repo.id, `
|
|
1250
|
+
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
1251
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
1252
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1253
|
+
LIMIT 30
|
|
1210
1254
|
`);
|
|
1211
1255
|
return {
|
|
1212
1256
|
cluster: {
|
|
@@ -1227,23 +1271,23 @@ export class LocalBackend {
|
|
|
1227
1271
|
* Used by getProcessDetailResource.
|
|
1228
1272
|
*/
|
|
1229
1273
|
async queryProcessDetail(name, repoName) {
|
|
1230
|
-
const repo = this.resolveRepo(repoName);
|
|
1274
|
+
const repo = await this.resolveRepo(repoName);
|
|
1231
1275
|
await this.ensureInitialized(repo.id);
|
|
1232
1276
|
const escaped = name.replace(/'/g, "''");
|
|
1233
|
-
const processes = await executeQuery(repo.id, `
|
|
1234
|
-
MATCH (p:Process)
|
|
1235
|
-
WHERE p.label = '${escaped}' OR p.heuristicLabel = '${escaped}'
|
|
1236
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1237
|
-
LIMIT 1
|
|
1277
|
+
const processes = await executeQuery(repo.id, `
|
|
1278
|
+
MATCH (p:Process)
|
|
1279
|
+
WHERE p.label = '${escaped}' OR p.heuristicLabel = '${escaped}'
|
|
1280
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1281
|
+
LIMIT 1
|
|
1238
1282
|
`);
|
|
1239
1283
|
if (processes.length === 0)
|
|
1240
1284
|
return { error: `Process '${name}' not found` };
|
|
1241
1285
|
const proc = processes[0];
|
|
1242
1286
|
const procId = proc.id || proc[0];
|
|
1243
|
-
const steps = await executeQuery(repo.id, `
|
|
1244
|
-
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
1245
|
-
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
1246
|
-
ORDER BY r.step
|
|
1287
|
+
const steps = await executeQuery(repo.id, `
|
|
1288
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
1289
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
1290
|
+
ORDER BY r.step
|
|
1247
1291
|
`);
|
|
1248
1292
|
return {
|
|
1249
1293
|
process: {
|
package/dist/mcp/resources.js
CHANGED
|
@@ -125,8 +125,8 @@ export async function readResource(uri, backend) {
|
|
|
125
125
|
/**
|
|
126
126
|
* Repos resource — list all indexed repositories
|
|
127
127
|
*/
|
|
128
|
-
function getReposResource(backend) {
|
|
129
|
-
const repos = backend.listRepos();
|
|
128
|
+
async function getReposResource(backend) {
|
|
129
|
+
const repos = await backend.listRepos();
|
|
130
130
|
if (repos.length === 0) {
|
|
131
131
|
return 'repos: []\n# No repositories indexed. Run: gitnexus analyze';
|
|
132
132
|
}
|
|
@@ -154,7 +154,7 @@ function getReposResource(backend) {
|
|
|
154
154
|
*/
|
|
155
155
|
async function getContextResource(backend, repoName) {
|
|
156
156
|
// Resolve repo
|
|
157
|
-
const repo = backend.resolveRepo(repoName);
|
|
157
|
+
const repo = await backend.resolveRepo(repoName);
|
|
158
158
|
const repoId = repo.name.toLowerCase();
|
|
159
159
|
const context = backend.getContext(repoId) || backend.getContext();
|
|
160
160
|
if (!context) {
|
|
@@ -256,48 +256,48 @@ async function getProcessesResource(backend, repoName) {
|
|
|
256
256
|
* Schema resource — graph structure for Cypher queries
|
|
257
257
|
*/
|
|
258
258
|
function getSchemaResource() {
|
|
259
|
-
return `# GitNexus Graph Schema
|
|
260
|
-
|
|
261
|
-
nodes:
|
|
262
|
-
- File: Source code files
|
|
263
|
-
- Folder: Directory containers
|
|
264
|
-
- Function: Functions and arrow functions
|
|
265
|
-
- Class: Class definitions
|
|
266
|
-
- Interface: Interface/type definitions
|
|
267
|
-
- Method: Class methods
|
|
268
|
-
- CodeElement: Catch-all for other code elements
|
|
269
|
-
- Community: Auto-detected functional area (Leiden algorithm)
|
|
270
|
-
- Process: Execution flow trace
|
|
271
|
-
|
|
272
|
-
additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
|
|
273
|
-
|
|
274
|
-
relationships:
|
|
275
|
-
- CONTAINS: File/Folder contains child
|
|
276
|
-
- DEFINES: File defines a symbol
|
|
277
|
-
- CALLS: Function/method invocation
|
|
278
|
-
- IMPORTS: Module imports
|
|
279
|
-
- EXTENDS: Class inheritance
|
|
280
|
-
- IMPLEMENTS: Interface implementation
|
|
281
|
-
- MEMBER_OF: Symbol belongs to community
|
|
282
|
-
- STEP_IN_PROCESS: Symbol is step N in process
|
|
283
|
-
|
|
284
|
-
relationship_table: "All relationships use a single CodeRelation table with a 'type' property. Properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)"
|
|
285
|
-
|
|
286
|
-
example_queries:
|
|
287
|
-
find_callers: |
|
|
288
|
-
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
|
|
289
|
-
RETURN caller.name, caller.filePath
|
|
290
|
-
|
|
291
|
-
find_community_members: |
|
|
292
|
-
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
293
|
-
WHERE c.heuristicLabel = "Auth"
|
|
294
|
-
RETURN s.name, labels(s)[0] AS type
|
|
295
|
-
|
|
296
|
-
trace_process: |
|
|
297
|
-
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
298
|
-
WHERE p.heuristicLabel = "LoginFlow"
|
|
299
|
-
RETURN s.name, r.step
|
|
300
|
-
ORDER BY r.step
|
|
259
|
+
return `# GitNexus Graph Schema
|
|
260
|
+
|
|
261
|
+
nodes:
|
|
262
|
+
- File: Source code files
|
|
263
|
+
- Folder: Directory containers
|
|
264
|
+
- Function: Functions and arrow functions
|
|
265
|
+
- Class: Class definitions
|
|
266
|
+
- Interface: Interface/type definitions
|
|
267
|
+
- Method: Class methods
|
|
268
|
+
- CodeElement: Catch-all for other code elements
|
|
269
|
+
- Community: Auto-detected functional area (Leiden algorithm)
|
|
270
|
+
- Process: Execution flow trace
|
|
271
|
+
|
|
272
|
+
additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
|
|
273
|
+
|
|
274
|
+
relationships:
|
|
275
|
+
- CONTAINS: File/Folder contains child
|
|
276
|
+
- DEFINES: File defines a symbol
|
|
277
|
+
- CALLS: Function/method invocation
|
|
278
|
+
- IMPORTS: Module imports
|
|
279
|
+
- EXTENDS: Class inheritance
|
|
280
|
+
- IMPLEMENTS: Interface implementation
|
|
281
|
+
- MEMBER_OF: Symbol belongs to community
|
|
282
|
+
- STEP_IN_PROCESS: Symbol is step N in process
|
|
283
|
+
|
|
284
|
+
relationship_table: "All relationships use a single CodeRelation table with a 'type' property. Properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)"
|
|
285
|
+
|
|
286
|
+
example_queries:
|
|
287
|
+
find_callers: |
|
|
288
|
+
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
|
|
289
|
+
RETURN caller.name, caller.filePath
|
|
290
|
+
|
|
291
|
+
find_community_members: |
|
|
292
|
+
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
293
|
+
WHERE c.heuristicLabel = "Auth"
|
|
294
|
+
RETURN s.name, labels(s)[0] AS type
|
|
295
|
+
|
|
296
|
+
trace_process: |
|
|
297
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
298
|
+
WHERE p.heuristicLabel = "LoginFlow"
|
|
299
|
+
RETURN s.name, r.step
|
|
300
|
+
ORDER BY r.step
|
|
301
301
|
`;
|
|
302
302
|
}
|
|
303
303
|
/**
|
|
@@ -370,7 +370,7 @@ async function getProcessDetailResource(name, backend, repoName) {
|
|
|
370
370
|
* Useful for `gitnexus setup` onboarding or dynamic content injection.
|
|
371
371
|
*/
|
|
372
372
|
async function getSetupResource(backend) {
|
|
373
|
-
const repos = backend.listRepos();
|
|
373
|
+
const repos = await backend.listRepos();
|
|
374
374
|
if (repos.length === 0) {
|
|
375
375
|
return '# GitNexus\n\nNo repositories indexed. Run: `npx gitnexus analyze` in a repository.';
|
|
376
376
|
}
|