gitnexus 1.1.5 → 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.js +3 -2
- package/dist/mcp/local/local-backend.d.ts +0 -1
- package/dist/mcp/local/local-backend.js +28 -127
- 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.js
CHANGED
|
@@ -33,7 +33,7 @@ This project is indexed as **${projectName}** by GitNexus, providing AI agents w
|
|
|
33
33
|
| Clusters | ${clusterCount} |
|
|
34
34
|
| Processes | ${stats.processes || 0} |
|
|
35
35
|
|
|
36
|
-
> **Staleness:** If the index is out of date, run \`
|
|
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
37
|
|
|
38
38
|
## Quick Start
|
|
39
39
|
|
|
@@ -66,7 +66,8 @@ This project is indexed as **${projectName}** by GitNexus, providing AI agents w
|
|
|
66
66
|
| \`explore\` | Deep dive on symbol/cluster/process | Detailed investigation |
|
|
67
67
|
| \`impact\` | Blast radius analysis | Before making changes |
|
|
68
68
|
| \`cypher\` | Raw graph queries | Complex analysis |
|
|
69
|
-
|
|
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.
|
|
70
71
|
|
|
71
72
|
> **Multi-repo:** When multiple repos are indexed, pass \`repo: "${projectName}"\` to target this project.
|
|
72
73
|
|
|
@@ -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.
|
|
@@ -175,8 +177,6 @@ export class LocalBackend {
|
|
|
175
177
|
return this.explore(repo, params);
|
|
176
178
|
case 'impact':
|
|
177
179
|
return this.impact(repo, params);
|
|
178
|
-
case 'analyze':
|
|
179
|
-
return this.analyze(repo, params);
|
|
180
180
|
default:
|
|
181
181
|
throw new Error(`Unknown tool: ${method}`);
|
|
182
182
|
}
|
|
@@ -255,10 +255,12 @@ export class LocalBackend {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
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)
|
|
258
259
|
if (depth === 'full' && result.nodeId) {
|
|
259
260
|
try {
|
|
260
261
|
const relQuery = `
|
|
261
262
|
MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[r:CodeRelation]->(m)
|
|
263
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'DEFINES', 'EXTENDS', 'IMPLEMENTS']
|
|
262
264
|
RETURN r.type AS type, m.name AS targetName, m.filePath AS targetPath
|
|
263
265
|
LIMIT 5
|
|
264
266
|
`;
|
|
@@ -490,15 +492,19 @@ export class LocalBackend {
|
|
|
490
492
|
await this.ensureInitialized(repo.id);
|
|
491
493
|
const { name, type } = params;
|
|
492
494
|
if (type === 'symbol') {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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`;
|
|
499
504
|
const symbols = await executeQuery(repo.id, symbolQuery);
|
|
500
505
|
if (symbols.length === 0)
|
|
501
506
|
return { error: `Symbol '${name}' not found` };
|
|
507
|
+
// Use the first match for detailed exploration
|
|
502
508
|
const sym = symbols[0];
|
|
503
509
|
const symId = sym.id || sym[0];
|
|
504
510
|
const callersQuery = `
|
|
@@ -519,7 +525,7 @@ export class LocalBackend {
|
|
|
519
525
|
LIMIT 1
|
|
520
526
|
`;
|
|
521
527
|
const communities = await executeQuery(repo.id, communityQuery);
|
|
522
|
-
|
|
528
|
+
const result = {
|
|
523
529
|
symbol: {
|
|
524
530
|
id: symId,
|
|
525
531
|
name: sym.name || sym[1],
|
|
@@ -535,6 +541,16 @@ export class LocalBackend {
|
|
|
535
541
|
heuristicLabel: communities[0].heuristicLabel || communities[0][1],
|
|
536
542
|
} : null,
|
|
537
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;
|
|
538
554
|
}
|
|
539
555
|
if (type === 'cluster') {
|
|
540
556
|
const escaped = name.replace(/'/g, "''");
|
|
@@ -694,121 +710,6 @@ export class LocalBackend {
|
|
|
694
710
|
byDepth: grouped,
|
|
695
711
|
};
|
|
696
712
|
}
|
|
697
|
-
async analyze(repo, params) {
|
|
698
|
-
let repoPath;
|
|
699
|
-
if (params.path) {
|
|
700
|
-
repoPath = path.resolve(params.path);
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
repoPath = repo.repoPath;
|
|
704
|
-
}
|
|
705
|
-
if (!isGitRepo(repoPath)) {
|
|
706
|
-
return { error: 'Not a git repository' };
|
|
707
|
-
}
|
|
708
|
-
const { storagePath, kuzuPath } = getRepoStoragePaths(repoPath);
|
|
709
|
-
const currentCommit = getCurrentCommit(repoPath);
|
|
710
|
-
const existingMeta = await loadRepoMeta(storagePath);
|
|
711
|
-
if (existingMeta && !params.force && existingMeta.lastCommit === currentCommit) {
|
|
712
|
-
return { status: 'up_to_date', message: 'Repository already up to date.' };
|
|
713
|
-
}
|
|
714
|
-
// Close this repo's MCP connection before pipeline takes over
|
|
715
|
-
await closeKuzu(repo.id);
|
|
716
|
-
this.initializedRepos.delete(repo.id);
|
|
717
|
-
try {
|
|
718
|
-
const { runPipelineFromRepo } = await import('../../core/ingestion/pipeline.js');
|
|
719
|
-
const coreKuzu = await import('../../core/kuzu/kuzu-adapter.js');
|
|
720
|
-
console.error('GitNexus: Running indexing pipeline...');
|
|
721
|
-
const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
|
|
722
|
-
if (progress.percent % 20 === 0) {
|
|
723
|
-
console.error(`GitNexus: ${progress.phase} ${progress.percent}%`);
|
|
724
|
-
}
|
|
725
|
-
});
|
|
726
|
-
console.error('GitNexus: Loading graph into KuzuDB...');
|
|
727
|
-
await coreKuzu.initKuzu(kuzuPath);
|
|
728
|
-
await coreKuzu.loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath);
|
|
729
|
-
console.error('GitNexus: Creating FTS indexes...');
|
|
730
|
-
try {
|
|
731
|
-
await coreKuzu.createFTSIndex('File', 'file_fts', ['name', 'content']);
|
|
732
|
-
await coreKuzu.createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
|
733
|
-
await coreKuzu.createFTSIndex('Class', 'class_fts', ['name', 'content']);
|
|
734
|
-
await coreKuzu.createFTSIndex('Method', 'method_fts', ['name', 'content']);
|
|
735
|
-
}
|
|
736
|
-
catch (e) {
|
|
737
|
-
console.error('GitNexus: Some FTS indexes may not have been created:', e.message);
|
|
738
|
-
}
|
|
739
|
-
if (!params.skipEmbeddings) {
|
|
740
|
-
try {
|
|
741
|
-
console.error('GitNexus: Generating embeddings...');
|
|
742
|
-
const { runEmbeddingPipeline } = await import('../../core/embeddings/embedding-pipeline.js');
|
|
743
|
-
await runEmbeddingPipeline(coreKuzu.executeQuery, coreKuzu.executeWithReusedStatement, (progress) => {
|
|
744
|
-
if (progress.percent % 25 === 0) {
|
|
745
|
-
console.error(`GitNexus: Embeddings ${progress.percent}%`);
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
catch (e) {
|
|
750
|
-
console.error('GitNexus: Embedding generation failed (non-fatal):', e.message);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
const stats = await coreKuzu.getKuzuStats();
|
|
754
|
-
const newMeta = {
|
|
755
|
-
repoPath,
|
|
756
|
-
lastCommit: currentCommit,
|
|
757
|
-
indexedAt: new Date().toISOString(),
|
|
758
|
-
stats: {
|
|
759
|
-
files: pipelineResult.fileContents.size,
|
|
760
|
-
nodes: stats.nodes,
|
|
761
|
-
edges: stats.edges,
|
|
762
|
-
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
763
|
-
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
764
|
-
},
|
|
765
|
-
};
|
|
766
|
-
await saveRepoMeta(storagePath, newMeta);
|
|
767
|
-
await addToGitignore(repoPath);
|
|
768
|
-
// Register in global registry
|
|
769
|
-
await registerRepo(repoPath, newMeta);
|
|
770
|
-
const projectName = path.basename(repoPath);
|
|
771
|
-
await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
772
|
-
files: pipelineResult.fileContents.size,
|
|
773
|
-
nodes: stats.nodes,
|
|
774
|
-
edges: stats.edges,
|
|
775
|
-
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
776
|
-
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
777
|
-
});
|
|
778
|
-
await coreKuzu.closeKuzu();
|
|
779
|
-
// Update in-memory state
|
|
780
|
-
const handle = {
|
|
781
|
-
id: repo.id,
|
|
782
|
-
name: projectName,
|
|
783
|
-
repoPath,
|
|
784
|
-
storagePath,
|
|
785
|
-
kuzuPath,
|
|
786
|
-
indexedAt: newMeta.indexedAt,
|
|
787
|
-
lastCommit: newMeta.lastCommit,
|
|
788
|
-
stats: newMeta.stats,
|
|
789
|
-
};
|
|
790
|
-
this.repos.set(repo.id, handle);
|
|
791
|
-
this.contextCache.set(repo.id, {
|
|
792
|
-
projectName,
|
|
793
|
-
stats: {
|
|
794
|
-
fileCount: newMeta.stats.files || 0,
|
|
795
|
-
functionCount: newMeta.stats.nodes || 0,
|
|
796
|
-
communityCount: newMeta.stats.communities || 0,
|
|
797
|
-
processCount: newMeta.stats.processes || 0,
|
|
798
|
-
},
|
|
799
|
-
});
|
|
800
|
-
console.error('GitNexus: Indexing complete!');
|
|
801
|
-
return {
|
|
802
|
-
status: 'success',
|
|
803
|
-
message: `Repository indexed successfully.`,
|
|
804
|
-
stats: newMeta.stats,
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
catch (e) {
|
|
808
|
-
console.error('GitNexus: Indexing failed:', e.message);
|
|
809
|
-
return { error: `Indexing failed: ${e.message}` };
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
713
|
async disconnect() {
|
|
813
714
|
await closeKuzu(); // close all connections
|
|
814
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
|