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.
@@ -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 \`gitnexus_analyze({repo: "${projectName}"})\` to refresh. The \`gitnexus://repo/${projectName}/context\` resource will warn you when the index is stale.
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
- | \`analyze\` | Re-index repository | When index is stale or after major code changes |
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
 
@@ -81,7 +81,6 @@ export declare class LocalBackend {
81
81
  private overview;
82
82
  private explore;
83
83
  private impact;
84
- private analyze;
85
84
  disconnect(): Promise<void>;
86
85
  }
87
86
  export {};
@@ -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
- import { isGitRepo, getCurrentCommit } from '../../storage/git.js';
12
- import { getStoragePaths as getRepoStoragePaths, saveMeta as saveRepoMeta, loadMeta as loadRepoMeta, addToGitignore, listRegisteredRepos, registerRepo, } from '../../storage/repo-manager.js';
13
- import { generateAIContextFiles } from '../../cli/ai-context.js';
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
- const symbolQuery = `
494
- MATCH (n)
495
- WHERE n.name = '${name.replace(/'/g, "''")}'
496
- 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
497
- LIMIT 1
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
- return {
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();
@@ -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: ${context.stats.communityCount}`);
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(' - analyze: Re-index to update stale data');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -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" → gitnexus_analyze({repo: "my-app"})
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
@@ -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" → gitnexus_analyze({repo: "my-app"})
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" → gitnexus_analyze({repo: "my-app"})
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
@@ -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" → gitnexus_analyze({repo: "my-app"})
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