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.
@@ -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
- return this.repos.size > 0;
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
- const names = [...this.repos.values()].map(h => h.name);
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
- const names = [...this.repos.values()].map(h => h.name);
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: {
@@ -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
  }