gitnexus 1.1.4 → 1.1.6
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/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +13 -8
- package/dist/cli/analyze.js +13 -0
- package/dist/core/ingestion/process-processor.js +8 -2
- package/dist/core/search/bm25-index.js +6 -3
- package/dist/mcp/local/local-backend.d.ts +0 -1
- package/dist/mcp/local/local-backend.js +38 -132
- package/dist/mcp/resources.js +12 -3
- package/dist/mcp/tools.js +0 -31
- package/package.json +1 -1
- package/skills/debugging.md +1 -1
- package/skills/exploring.md +1 -1
- package/skills/impact-analysis.md +1 -1
- package/skills/refactoring.md +1 -1
package/dist/cli/ai-context.d.ts
CHANGED
package/dist/cli/ai-context.js
CHANGED
|
@@ -17,6 +17,7 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
|
|
|
17
17
|
* Generate the full GitNexus context content (resources-first approach)
|
|
18
18
|
*/
|
|
19
19
|
function generateGitNexusContent(projectName, stats) {
|
|
20
|
+
const clusterCount = stats.clusters || stats.communities || 0;
|
|
20
21
|
return `${GITNEXUS_START_MARKER}
|
|
21
22
|
# GitNexus MCP
|
|
22
23
|
|
|
@@ -29,9 +30,11 @@ This project is indexed as **${projectName}** by GitNexus, providing AI agents w
|
|
|
29
30
|
| Files | ${stats.files || 0} |
|
|
30
31
|
| Symbols | ${stats.nodes || 0} |
|
|
31
32
|
| Relationships | ${stats.edges || 0} |
|
|
32
|
-
|
|
|
33
|
+
| Clusters | ${clusterCount} |
|
|
33
34
|
| Processes | ${stats.processes || 0} |
|
|
34
35
|
|
|
36
|
+
> **Staleness:** If the index is out of date, run \`npx gitnexus analyze\` in the terminal to refresh. The \`gitnexus://repo/${projectName}/context\` resource will warn you when the index is stale.
|
|
37
|
+
|
|
35
38
|
## Quick Start
|
|
36
39
|
|
|
37
40
|
\`\`\`
|
|
@@ -64,6 +67,8 @@ This project is indexed as **${projectName}** by GitNexus, providing AI agents w
|
|
|
64
67
|
| \`impact\` | Blast radius analysis | Before making changes |
|
|
65
68
|
| \`cypher\` | Raw graph queries | Complex analysis |
|
|
66
69
|
|
|
70
|
+
> **Re-indexing:** To refresh a stale index, run \`npx gitnexus analyze\` in the terminal. Use \`--force\` only to rebuild from scratch. This is a CLI command, not an MCP tool.
|
|
71
|
+
|
|
67
72
|
> **Multi-repo:** When multiple repos are indexed, pass \`repo: "${projectName}"\` to target this project.
|
|
68
73
|
|
|
69
74
|
## Workflow Examples
|
|
@@ -71,17 +76,17 @@ This project is indexed as **${projectName}** by GitNexus, providing AI agents w
|
|
|
71
76
|
### Exploring the Codebase
|
|
72
77
|
\`\`\`
|
|
73
78
|
READ gitnexus://repos → Discover repos
|
|
74
|
-
READ gitnexus://repo/${projectName}/context → Stats and overview
|
|
75
|
-
READ gitnexus://repo/${projectName}/clusters → Find relevant cluster
|
|
76
|
-
READ gitnexus://repo/${projectName}/cluster/
|
|
77
|
-
gitnexus_explore({name: "
|
|
79
|
+
READ gitnexus://repo/${projectName}/context → Stats and overview (check for staleness)
|
|
80
|
+
READ gitnexus://repo/${projectName}/clusters → Find relevant cluster by name
|
|
81
|
+
READ gitnexus://repo/${projectName}/cluster/{name} → See members of that cluster
|
|
82
|
+
gitnexus_explore({name: "<symbol_name>", type: "symbol", repo: "${projectName}"})
|
|
78
83
|
\`\`\`
|
|
79
84
|
|
|
80
85
|
### Planning a Change
|
|
81
86
|
\`\`\`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
gitnexus_search({query: "<what you want to change>", repo: "${projectName}"})
|
|
88
|
+
gitnexus_impact({target: "<symbol_name>", direction: "upstream", repo: "${projectName}"})
|
|
89
|
+
READ gitnexus://repo/${projectName}/processes → Check affected execution flows
|
|
85
90
|
\`\`\`
|
|
86
91
|
|
|
87
92
|
## Graph Schema
|
package/dist/cli/analyze.js
CHANGED
|
@@ -70,6 +70,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
70
70
|
await createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
|
71
71
|
await createFTSIndex('Class', 'class_fts', ['name', 'content']);
|
|
72
72
|
await createFTSIndex('Method', 'method_fts', ['name', 'content']);
|
|
73
|
+
await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
|
|
73
74
|
}
|
|
74
75
|
catch (e) {
|
|
75
76
|
// FTS index creation may fail if tables are empty (no data for that type)
|
|
@@ -103,11 +104,23 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
103
104
|
await addToGitignore(repoPath);
|
|
104
105
|
// Generate AI context files
|
|
105
106
|
const projectName = path.basename(repoPath);
|
|
107
|
+
// Compute aggregated cluster count (grouped by heuristicLabel, >=5 symbols)
|
|
108
|
+
// This matches the aggregation logic in local-backend.ts for tool output consistency.
|
|
109
|
+
let aggregatedClusterCount = 0;
|
|
110
|
+
if (pipelineResult.communityResult?.communities) {
|
|
111
|
+
const groups = new Map();
|
|
112
|
+
for (const c of pipelineResult.communityResult.communities) {
|
|
113
|
+
const label = c.heuristicLabel || c.label || 'Unknown';
|
|
114
|
+
groups.set(label, (groups.get(label) || 0) + c.symbolCount);
|
|
115
|
+
}
|
|
116
|
+
aggregatedClusterCount = Array.from(groups.values()).filter(count => count >= 5).length;
|
|
117
|
+
}
|
|
106
118
|
const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
107
119
|
files: pipelineResult.fileContents.size,
|
|
108
120
|
nodes: stats.nodes,
|
|
109
121
|
edges: stats.edges,
|
|
110
122
|
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
123
|
+
clusters: aggregatedClusterCount,
|
|
111
124
|
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
112
125
|
});
|
|
113
126
|
// Close database
|
|
@@ -121,10 +121,16 @@ export const processProcesses = async (knowledgeGraph, memberships, onProgress,
|
|
|
121
121
|
},
|
|
122
122
|
};
|
|
123
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Minimum edge confidence for process tracing.
|
|
126
|
+
* Filters out ambiguous fuzzy-global matches (0.3) that cause
|
|
127
|
+
* traces to jump across unrelated code areas.
|
|
128
|
+
*/
|
|
129
|
+
const MIN_TRACE_CONFIDENCE = 0.5;
|
|
124
130
|
const buildCallsGraph = (graph) => {
|
|
125
131
|
const adj = new Map();
|
|
126
132
|
graph.relationships.forEach(rel => {
|
|
127
|
-
if (rel.type === 'CALLS') {
|
|
133
|
+
if (rel.type === 'CALLS' && rel.confidence >= MIN_TRACE_CONFIDENCE) {
|
|
128
134
|
if (!adj.has(rel.sourceId)) {
|
|
129
135
|
adj.set(rel.sourceId, []);
|
|
130
136
|
}
|
|
@@ -136,7 +142,7 @@ const buildCallsGraph = (graph) => {
|
|
|
136
142
|
const buildReverseCallsGraph = (graph) => {
|
|
137
143
|
const adj = new Map();
|
|
138
144
|
graph.relationships.forEach(rel => {
|
|
139
|
-
if (rel.type === 'CALLS') {
|
|
145
|
+
if (rel.type === 'CALLS' && rel.confidence >= MIN_TRACE_CONFIDENCE) {
|
|
140
146
|
if (!adj.has(rel.targetId)) {
|
|
141
147
|
adj.set(rel.targetId, []);
|
|
142
148
|
}
|
|
@@ -44,25 +44,27 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
|
|
|
44
44
|
* @returns Ranked search results from FTS indexes
|
|
45
45
|
*/
|
|
46
46
|
export const searchFTSFromKuzu = async (query, limit = 20, repoId) => {
|
|
47
|
-
let fileResults, functionResults, classResults, methodResults;
|
|
47
|
+
let fileResults, functionResults, classResults, methodResults, interfaceResults;
|
|
48
48
|
if (repoId) {
|
|
49
49
|
// Use MCP connection pool via dynamic import
|
|
50
50
|
const { executeQuery } = await import('../../mcp/core/kuzu-adapter.js');
|
|
51
51
|
const executor = (cypher) => executeQuery(repoId, cypher);
|
|
52
|
-
[fileResults, functionResults, classResults, methodResults] = await Promise.all([
|
|
52
|
+
[fileResults, functionResults, classResults, methodResults, interfaceResults] = await Promise.all([
|
|
53
53
|
queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit),
|
|
54
54
|
queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit),
|
|
55
55
|
queryFTSViaExecutor(executor, 'Class', 'class_fts', query, limit),
|
|
56
56
|
queryFTSViaExecutor(executor, 'Method', 'method_fts', query, limit),
|
|
57
|
+
queryFTSViaExecutor(executor, 'Interface', 'interface_fts', query, limit),
|
|
57
58
|
]);
|
|
58
59
|
}
|
|
59
60
|
else {
|
|
60
61
|
// Use core kuzu adapter (CLI / pipeline context)
|
|
61
|
-
[fileResults, functionResults, classResults, methodResults] = await Promise.all([
|
|
62
|
+
[fileResults, functionResults, classResults, methodResults, interfaceResults] = await Promise.all([
|
|
62
63
|
queryFTS('File', 'file_fts', query, limit, false).catch(() => []),
|
|
63
64
|
queryFTS('Function', 'function_fts', query, limit, false).catch(() => []),
|
|
64
65
|
queryFTS('Class', 'class_fts', query, limit, false).catch(() => []),
|
|
65
66
|
queryFTS('Method', 'method_fts', query, limit, false).catch(() => []),
|
|
67
|
+
queryFTS('Interface', 'interface_fts', query, limit, false).catch(() => []),
|
|
66
68
|
]);
|
|
67
69
|
}
|
|
68
70
|
// Merge results by filePath, summing scores for same file
|
|
@@ -82,6 +84,7 @@ export const searchFTSFromKuzu = async (query, limit = 20, repoId) => {
|
|
|
82
84
|
addResults(functionResults);
|
|
83
85
|
addResults(classResults);
|
|
84
86
|
addResults(methodResults);
|
|
87
|
+
addResults(interfaceResults);
|
|
85
88
|
// Sort by score descending and add rank
|
|
86
89
|
const sorted = Array.from(merged.values())
|
|
87
90
|
.sort((a, b) => b.score - a.score)
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { initKuzu, executeQuery, closeKuzu, isKuzuReady } from '../core/kuzu-adapter.js';
|
|
10
10
|
import { embedQuery, getEmbeddingDims, disposeEmbedder } from '../core/embedder.js';
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
// git utilities available if needed
|
|
12
|
+
// import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
|
|
13
|
+
import { listRegisteredRepos, } from '../../storage/repo-manager.js';
|
|
14
|
+
// AI context generation is CLI-only (gitnexus analyze)
|
|
15
|
+
// import { generateAIContextFiles } from '../../cli/ai-context.js';
|
|
14
16
|
/**
|
|
15
17
|
* Quick test-file detection for filtering impact results.
|
|
16
18
|
* Matches common test file patterns across all supported languages.
|
|
@@ -123,7 +125,8 @@ export class LocalBackend {
|
|
|
123
125
|
}
|
|
124
126
|
// ─── Lazy KuzuDB Init ────────────────────────────────────────────
|
|
125
127
|
async ensureInitialized(repoId) {
|
|
126
|
-
|
|
128
|
+
// Always check the actual pool — the idle timer may have evicted the connection
|
|
129
|
+
if (this.initializedRepos.has(repoId) && isKuzuReady(repoId))
|
|
127
130
|
return;
|
|
128
131
|
const handle = this.repos.get(repoId);
|
|
129
132
|
if (!handle)
|
|
@@ -174,8 +177,6 @@ export class LocalBackend {
|
|
|
174
177
|
return this.explore(repo, params);
|
|
175
178
|
case 'impact':
|
|
176
179
|
return this.impact(repo, params);
|
|
177
|
-
case 'analyze':
|
|
178
|
-
return this.analyze(repo, params);
|
|
179
180
|
default:
|
|
180
181
|
throw new Error(`Unknown tool: ${method}`);
|
|
181
182
|
}
|
|
@@ -192,11 +193,13 @@ export class LocalBackend {
|
|
|
192
193
|
this.semanticSearch(repo, query, limit * 2),
|
|
193
194
|
]);
|
|
194
195
|
// Merge and deduplicate results using reciprocal rank fusion
|
|
196
|
+
// Key by nodeId (symbol-level) so semantic precision is preserved.
|
|
197
|
+
// Fall back to filePath for File-level results that lack a nodeId.
|
|
195
198
|
const scoreMap = new Map();
|
|
196
199
|
// BM25 results
|
|
197
200
|
for (let i = 0; i < bm25Results.length; i++) {
|
|
198
201
|
const result = bm25Results[i];
|
|
199
|
-
const key = result.filePath;
|
|
202
|
+
const key = result.nodeId || result.filePath;
|
|
200
203
|
const rrfScore = 1 / (60 + i);
|
|
201
204
|
const existing = scoreMap.get(key);
|
|
202
205
|
if (existing) {
|
|
@@ -210,7 +213,7 @@ export class LocalBackend {
|
|
|
210
213
|
// Semantic results
|
|
211
214
|
for (let i = 0; i < semanticResults.length; i++) {
|
|
212
215
|
const result = semanticResults[i];
|
|
213
|
-
const key = result.filePath;
|
|
216
|
+
const key = result.nodeId || result.filePath;
|
|
214
217
|
const rrfScore = 1 / (60 + i);
|
|
215
218
|
const existing = scoreMap.get(key);
|
|
216
219
|
if (existing) {
|
|
@@ -252,10 +255,12 @@ export class LocalBackend {
|
|
|
252
255
|
}
|
|
253
256
|
}
|
|
254
257
|
// Add relationships if depth is 'full' and we have a node ID
|
|
258
|
+
// Only include connections with actual name/path data (skip MEMBER_OF, STEP_IN_PROCESS noise)
|
|
255
259
|
if (depth === 'full' && result.nodeId) {
|
|
256
260
|
try {
|
|
257
261
|
const relQuery = `
|
|
258
262
|
MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[r:CodeRelation]->(m)
|
|
263
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'DEFINES', 'EXTENDS', 'IMPLEMENTS']
|
|
259
264
|
RETURN r.type AS type, m.name AS targetName, m.filePath AS targetPath
|
|
260
265
|
LIMIT 5
|
|
261
266
|
`;
|
|
@@ -282,11 +287,11 @@ export class LocalBackend {
|
|
|
282
287
|
const bm25Results = await searchFTSFromKuzu(query, limit, repo.id);
|
|
283
288
|
const results = [];
|
|
284
289
|
for (const bm25Result of bm25Results) {
|
|
285
|
-
const
|
|
290
|
+
const fullPath = bm25Result.filePath;
|
|
286
291
|
try {
|
|
287
292
|
const symbolQuery = `
|
|
288
293
|
MATCH (n)
|
|
289
|
-
WHERE n.filePath
|
|
294
|
+
WHERE n.filePath = '${fullPath.replace(/'/g, "''")}'
|
|
290
295
|
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
|
|
291
296
|
LIMIT 3
|
|
292
297
|
`;
|
|
@@ -305,6 +310,7 @@ export class LocalBackend {
|
|
|
305
310
|
}
|
|
306
311
|
}
|
|
307
312
|
else {
|
|
313
|
+
const fileName = fullPath.split('/').pop() || fullPath;
|
|
308
314
|
results.push({
|
|
309
315
|
name: fileName,
|
|
310
316
|
type: 'File',
|
|
@@ -314,6 +320,7 @@ export class LocalBackend {
|
|
|
314
320
|
}
|
|
315
321
|
}
|
|
316
322
|
catch {
|
|
323
|
+
const fileName = fullPath.split('/').pop() || fullPath;
|
|
317
324
|
results.push({
|
|
318
325
|
name: fileName,
|
|
319
326
|
type: 'File',
|
|
@@ -485,15 +492,19 @@ export class LocalBackend {
|
|
|
485
492
|
await this.ensureInitialized(repo.id);
|
|
486
493
|
const { name, type } = params;
|
|
487
494
|
if (type === 'symbol') {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
495
|
+
// If name contains a path separator or ':', treat it as a qualified lookup
|
|
496
|
+
const isQualified = name.includes('/') || name.includes(':');
|
|
497
|
+
const symbolQuery = isQualified
|
|
498
|
+
? `MATCH (n) WHERE n.id = '${name.replace(/'/g, "''")}' OR (n.name = '${name.replace(/'/g, "''")}')
|
|
499
|
+
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
|
|
500
|
+
LIMIT 5`
|
|
501
|
+
: `MATCH (n) WHERE n.name = '${name.replace(/'/g, "''")}'
|
|
502
|
+
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
|
|
503
|
+
LIMIT 5`;
|
|
494
504
|
const symbols = await executeQuery(repo.id, symbolQuery);
|
|
495
505
|
if (symbols.length === 0)
|
|
496
506
|
return { error: `Symbol '${name}' not found` };
|
|
507
|
+
// Use the first match for detailed exploration
|
|
497
508
|
const sym = symbols[0];
|
|
498
509
|
const symId = sym.id || sym[0];
|
|
499
510
|
const callersQuery = `
|
|
@@ -514,7 +525,7 @@ export class LocalBackend {
|
|
|
514
525
|
LIMIT 1
|
|
515
526
|
`;
|
|
516
527
|
const communities = await executeQuery(repo.id, communityQuery);
|
|
517
|
-
|
|
528
|
+
const result = {
|
|
518
529
|
symbol: {
|
|
519
530
|
id: symId,
|
|
520
531
|
name: sym.name || sym[1],
|
|
@@ -530,6 +541,16 @@ export class LocalBackend {
|
|
|
530
541
|
heuristicLabel: communities[0].heuristicLabel || communities[0][1],
|
|
531
542
|
} : null,
|
|
532
543
|
};
|
|
544
|
+
// If multiple symbols share the same name, show alternatives so the agent can disambiguate
|
|
545
|
+
if (symbols.length > 1) {
|
|
546
|
+
result.alternatives = symbols.slice(1).map((s) => ({
|
|
547
|
+
id: s.id || s[0],
|
|
548
|
+
type: s.type || s[2],
|
|
549
|
+
filePath: s.filePath || s[3],
|
|
550
|
+
}));
|
|
551
|
+
result.hint = `Multiple symbols named '${name}' found. Showing details for ${result.symbol.filePath}. Use the full node ID to explore a specific alternative.`;
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
533
554
|
}
|
|
534
555
|
if (type === 'cluster') {
|
|
535
556
|
const escaped = name.replace(/'/g, "''");
|
|
@@ -689,121 +710,6 @@ export class LocalBackend {
|
|
|
689
710
|
byDepth: grouped,
|
|
690
711
|
};
|
|
691
712
|
}
|
|
692
|
-
async analyze(repo, params) {
|
|
693
|
-
let repoPath;
|
|
694
|
-
if (params.path) {
|
|
695
|
-
repoPath = path.resolve(params.path);
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
repoPath = repo.repoPath;
|
|
699
|
-
}
|
|
700
|
-
if (!isGitRepo(repoPath)) {
|
|
701
|
-
return { error: 'Not a git repository' };
|
|
702
|
-
}
|
|
703
|
-
const { storagePath, kuzuPath } = getRepoStoragePaths(repoPath);
|
|
704
|
-
const currentCommit = getCurrentCommit(repoPath);
|
|
705
|
-
const existingMeta = await loadRepoMeta(storagePath);
|
|
706
|
-
if (existingMeta && !params.force && existingMeta.lastCommit === currentCommit) {
|
|
707
|
-
return { status: 'up_to_date', message: 'Repository already up to date.' };
|
|
708
|
-
}
|
|
709
|
-
// Close this repo's MCP connection before pipeline takes over
|
|
710
|
-
await closeKuzu(repo.id);
|
|
711
|
-
this.initializedRepos.delete(repo.id);
|
|
712
|
-
try {
|
|
713
|
-
const { runPipelineFromRepo } = await import('../../core/ingestion/pipeline.js');
|
|
714
|
-
const coreKuzu = await import('../../core/kuzu/kuzu-adapter.js');
|
|
715
|
-
console.error('GitNexus: Running indexing pipeline...');
|
|
716
|
-
const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
|
|
717
|
-
if (progress.percent % 20 === 0) {
|
|
718
|
-
console.error(`GitNexus: ${progress.phase} ${progress.percent}%`);
|
|
719
|
-
}
|
|
720
|
-
});
|
|
721
|
-
console.error('GitNexus: Loading graph into KuzuDB...');
|
|
722
|
-
await coreKuzu.initKuzu(kuzuPath);
|
|
723
|
-
await coreKuzu.loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath);
|
|
724
|
-
console.error('GitNexus: Creating FTS indexes...');
|
|
725
|
-
try {
|
|
726
|
-
await coreKuzu.createFTSIndex('File', 'file_fts', ['name', 'content']);
|
|
727
|
-
await coreKuzu.createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
|
728
|
-
await coreKuzu.createFTSIndex('Class', 'class_fts', ['name', 'content']);
|
|
729
|
-
await coreKuzu.createFTSIndex('Method', 'method_fts', ['name', 'content']);
|
|
730
|
-
}
|
|
731
|
-
catch (e) {
|
|
732
|
-
console.error('GitNexus: Some FTS indexes may not have been created:', e.message);
|
|
733
|
-
}
|
|
734
|
-
if (!params.skipEmbeddings) {
|
|
735
|
-
try {
|
|
736
|
-
console.error('GitNexus: Generating embeddings...');
|
|
737
|
-
const { runEmbeddingPipeline } = await import('../../core/embeddings/embedding-pipeline.js');
|
|
738
|
-
await runEmbeddingPipeline(coreKuzu.executeQuery, coreKuzu.executeWithReusedStatement, (progress) => {
|
|
739
|
-
if (progress.percent % 25 === 0) {
|
|
740
|
-
console.error(`GitNexus: Embeddings ${progress.percent}%`);
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
catch (e) {
|
|
745
|
-
console.error('GitNexus: Embedding generation failed (non-fatal):', e.message);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
const stats = await coreKuzu.getKuzuStats();
|
|
749
|
-
const newMeta = {
|
|
750
|
-
repoPath,
|
|
751
|
-
lastCommit: currentCommit,
|
|
752
|
-
indexedAt: new Date().toISOString(),
|
|
753
|
-
stats: {
|
|
754
|
-
files: pipelineResult.fileContents.size,
|
|
755
|
-
nodes: stats.nodes,
|
|
756
|
-
edges: stats.edges,
|
|
757
|
-
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
758
|
-
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
759
|
-
},
|
|
760
|
-
};
|
|
761
|
-
await saveRepoMeta(storagePath, newMeta);
|
|
762
|
-
await addToGitignore(repoPath);
|
|
763
|
-
// Register in global registry
|
|
764
|
-
await registerRepo(repoPath, newMeta);
|
|
765
|
-
const projectName = path.basename(repoPath);
|
|
766
|
-
await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
767
|
-
files: pipelineResult.fileContents.size,
|
|
768
|
-
nodes: stats.nodes,
|
|
769
|
-
edges: stats.edges,
|
|
770
|
-
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
771
|
-
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
772
|
-
});
|
|
773
|
-
await coreKuzu.closeKuzu();
|
|
774
|
-
// Update in-memory state
|
|
775
|
-
const handle = {
|
|
776
|
-
id: repo.id,
|
|
777
|
-
name: projectName,
|
|
778
|
-
repoPath,
|
|
779
|
-
storagePath,
|
|
780
|
-
kuzuPath,
|
|
781
|
-
indexedAt: newMeta.indexedAt,
|
|
782
|
-
lastCommit: newMeta.lastCommit,
|
|
783
|
-
stats: newMeta.stats,
|
|
784
|
-
};
|
|
785
|
-
this.repos.set(repo.id, handle);
|
|
786
|
-
this.contextCache.set(repo.id, {
|
|
787
|
-
projectName,
|
|
788
|
-
stats: {
|
|
789
|
-
fileCount: newMeta.stats.files || 0,
|
|
790
|
-
functionCount: newMeta.stats.nodes || 0,
|
|
791
|
-
communityCount: newMeta.stats.communities || 0,
|
|
792
|
-
processCount: newMeta.stats.processes || 0,
|
|
793
|
-
},
|
|
794
|
-
});
|
|
795
|
-
console.error('GitNexus: Indexing complete!');
|
|
796
|
-
return {
|
|
797
|
-
status: 'success',
|
|
798
|
-
message: `Repository indexed successfully.`,
|
|
799
|
-
stats: newMeta.stats,
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
catch (e) {
|
|
803
|
-
console.error('GitNexus: Indexing failed:', e.message);
|
|
804
|
-
return { error: `Indexing failed: ${e.message}` };
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
713
|
async disconnect() {
|
|
808
714
|
await closeKuzu(); // close all connections
|
|
809
715
|
await disposeEmbedder();
|
package/dist/mcp/resources.js
CHANGED
|
@@ -132,7 +132,6 @@ function getReposResource(backend) {
|
|
|
132
132
|
if (repo.stats) {
|
|
133
133
|
lines.push(` files: ${repo.stats.files || 0}`);
|
|
134
134
|
lines.push(` symbols: ${repo.stats.nodes || 0}`);
|
|
135
|
-
lines.push(` clusters: ${repo.stats.communities || 0}`);
|
|
136
135
|
lines.push(` processes: ${repo.stats.processes || 0}`);
|
|
137
136
|
}
|
|
138
137
|
}
|
|
@@ -158,6 +157,15 @@ async function getContextResource(backend, repoName) {
|
|
|
158
157
|
const repoPath = repo.repoPath;
|
|
159
158
|
const lastCommit = repo.lastCommit || 'HEAD';
|
|
160
159
|
const staleness = repoPath ? checkStaleness(repoPath, lastCommit) : { isStale: false, commitsBehind: 0 };
|
|
160
|
+
// Get aggregated cluster count (matches what overview/clusters resource shows)
|
|
161
|
+
let clusterCount = context.stats.communityCount;
|
|
162
|
+
try {
|
|
163
|
+
const overview = await backend.callTool('overview', { showClusters: true, showProcesses: false, limit: 100, repo: repoName });
|
|
164
|
+
if (overview.clusters) {
|
|
165
|
+
clusterCount = overview.clusters.length;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch { /* fall back to raw count */ }
|
|
161
169
|
const lines = [
|
|
162
170
|
`project: ${context.projectName}`,
|
|
163
171
|
];
|
|
@@ -169,7 +177,7 @@ async function getContextResource(backend, repoName) {
|
|
|
169
177
|
lines.push('stats:');
|
|
170
178
|
lines.push(` files: ${context.stats.fileCount}`);
|
|
171
179
|
lines.push(` symbols: ${context.stats.functionCount}`);
|
|
172
|
-
lines.push(` clusters: ${
|
|
180
|
+
lines.push(` clusters: ${clusterCount}`);
|
|
173
181
|
lines.push(` processes: ${context.stats.processCount}`);
|
|
174
182
|
lines.push('');
|
|
175
183
|
lines.push('tools_available:');
|
|
@@ -179,7 +187,8 @@ async function getContextResource(backend, repoName) {
|
|
|
179
187
|
lines.push(' - impact: Blast radius analysis');
|
|
180
188
|
lines.push(' - overview: List all clusters and processes');
|
|
181
189
|
lines.push(' - cypher: Raw graph queries');
|
|
182
|
-
lines.push('
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push('re_index: Run `npx gitnexus analyze` in terminal if data is stale');
|
|
183
192
|
lines.push('');
|
|
184
193
|
lines.push('resources_available:');
|
|
185
194
|
lines.push(' - gitnexus://repos: All indexed repositories');
|
package/dist/mcp/tools.js
CHANGED
|
@@ -20,37 +20,6 @@ on other tools (search, explore, impact, etc.) to target the correct one.`,
|
|
|
20
20
|
required: [],
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
-
{
|
|
24
|
-
name: 'analyze',
|
|
25
|
-
description: `Index or re-index a repository. Runs the full pipeline in-process.
|
|
26
|
-
|
|
27
|
-
Creates .gitnexus/ in repo root with:
|
|
28
|
-
- Knowledge graph (functions, classes, calls, imports)
|
|
29
|
-
- Full-text search indexes
|
|
30
|
-
- Community detection (Leiden)
|
|
31
|
-
- Process tracing
|
|
32
|
-
- Embeddings for semantic search
|
|
33
|
-
|
|
34
|
-
Also registers the repo in the global registry so the MCP server can serve it.
|
|
35
|
-
|
|
36
|
-
Run this when:
|
|
37
|
-
- First time using GitNexus on a repo
|
|
38
|
-
- After major code changes
|
|
39
|
-
- When staleness warning appears in gitnexus://context
|
|
40
|
-
- When 'not indexed' error appears
|
|
41
|
-
|
|
42
|
-
Note: This may take 30-120 seconds for large repos.`,
|
|
43
|
-
inputSchema: {
|
|
44
|
-
type: 'object',
|
|
45
|
-
properties: {
|
|
46
|
-
path: { type: 'string', description: 'Repo path (default: current directory)' },
|
|
47
|
-
force: { type: 'boolean', description: 'Re-index even if exists', default: false },
|
|
48
|
-
skipEmbeddings: { type: 'boolean', description: 'Skip embedding generation (faster)', default: false },
|
|
49
|
-
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
|
50
|
-
},
|
|
51
|
-
required: [],
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
23
|
{
|
|
55
24
|
name: 'search',
|
|
56
25
|
description: `Hybrid search (keyword + semantic) across the codebase.
|
package/package.json
CHANGED
package/skills/debugging.md
CHANGED
|
@@ -8,7 +8,7 @@ description: Trace bugs through call chains using knowledge graph
|
|
|
8
8
|
## Quick Start
|
|
9
9
|
```
|
|
10
10
|
0. READ gitnexus://repos → Discover indexed repos
|
|
11
|
-
1. If "Index is stale" →
|
|
11
|
+
1. If "Index is stale" → run `npx gitnexus analyze` in terminal
|
|
12
12
|
2. gitnexus_search({query: "...", repo: "my-app"}) → Find code related to error
|
|
13
13
|
3. gitnexus_explore({name, type: "symbol", repo: "my-app"}) → Get callers and callees
|
|
14
14
|
4. READ gitnexus://repo/my-app/process/{name} → Trace execution flow
|
package/skills/exploring.md
CHANGED
|
@@ -8,7 +8,7 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
|
|
|
8
8
|
## Quick Start
|
|
9
9
|
```
|
|
10
10
|
0. READ gitnexus://repos → Discover indexed repos (use repo param if multiple)
|
|
11
|
-
1. If "Index is stale" →
|
|
11
|
+
1. If "Index is stale" → run `npx gitnexus analyze` in terminal
|
|
12
12
|
2. READ gitnexus://repo/{name}/context → Get codebase overview (~150 tokens)
|
|
13
13
|
3. READ gitnexus://repo/{name}/clusters → See all functional clusters
|
|
14
14
|
4. READ gitnexus://repo/{name}/cluster/{name} → Deep dive on specific cluster
|
|
@@ -8,7 +8,7 @@ description: Analyze blast radius before making code changes
|
|
|
8
8
|
## Quick Start
|
|
9
9
|
```
|
|
10
10
|
0. READ gitnexus://repos → Discover indexed repos
|
|
11
|
-
1. If "Index is stale" →
|
|
11
|
+
1. If "Index is stale" → run `npx gitnexus analyze` in terminal
|
|
12
12
|
2. gitnexus_impact({target, direction: "upstream", repo: "my-app"}) → What depends on this
|
|
13
13
|
3. READ gitnexus://repo/my-app/clusters → Check affected areas
|
|
14
14
|
4. READ gitnexus://repo/my-app/processes → Affected execution flows
|
package/skills/refactoring.md
CHANGED
|
@@ -8,7 +8,7 @@ description: Plan safe refactors using blast radius and dependency mapping
|
|
|
8
8
|
## Quick Start
|
|
9
9
|
```
|
|
10
10
|
0. READ gitnexus://repos → Discover indexed repos
|
|
11
|
-
1. If "Index is stale" →
|
|
11
|
+
1. If "Index is stale" → run `npx gitnexus analyze` in terminal
|
|
12
12
|
2. gitnexus_impact({target, direction: "upstream", repo: "my-app"}) → Map all dependents
|
|
13
13
|
3. READ gitnexus://repo/my-app/schema → Understand graph structure
|
|
14
14
|
4. gitnexus_cypher({query: "...", repo: "my-app"}) → Find all references
|