gitnexus 1.1.3 → 1.1.5

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.
@@ -10,6 +10,7 @@ interface RepoStats {
10
10
  nodes?: number;
11
11
  edges?: number;
12
12
  communities?: number;
13
+ clusters?: number;
13
14
  processes?: number;
14
15
  }
15
16
  /**
@@ -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
- | Communities | ${stats.communities || 0} |
33
+ | Clusters | ${clusterCount} |
33
34
  | Processes | ${stats.processes || 0} |
34
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.
37
+
35
38
  ## Quick Start
36
39
 
37
40
  \`\`\`
@@ -63,6 +66,7 @@ This project is indexed as **${projectName}** by GitNexus, providing AI agents w
63
66
  | \`explore\` | Deep dive on symbol/cluster/process | Detailed investigation |
64
67
  | \`impact\` | Blast radius analysis | Before making changes |
65
68
  | \`cypher\` | Raw graph queries | Complex analysis |
69
+ | \`analyze\` | Re-index repository | When index is stale or after major code changes |
66
70
 
67
71
  > **Multi-repo:** When multiple repos are indexed, pass \`repo: "${projectName}"\` to target this project.
68
72
 
@@ -71,17 +75,17 @@ This project is indexed as **${projectName}** by GitNexus, providing AI agents w
71
75
  ### Exploring the Codebase
72
76
  \`\`\`
73
77
  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/Auth Explore Auth cluster
77
- gitnexus_explore({name: "validateUser", type: "symbol", repo: "${projectName}"})
78
+ READ gitnexus://repo/${projectName}/context → Stats and overview (check for staleness)
79
+ READ gitnexus://repo/${projectName}/clusters → Find relevant cluster by name
80
+ READ gitnexus://repo/${projectName}/cluster/{name} See members of that cluster
81
+ gitnexus_explore({name: "<symbol_name>", type: "symbol", repo: "${projectName}"})
78
82
  \`\`\`
79
83
 
80
84
  ### Planning a Change
81
85
  \`\`\`
82
- gitnexus_impact({target: "UserService", direction: "upstream", repo: "${projectName}"})
83
- READ gitnexus://repo/${projectName}/processes → Check affected flows
84
- gitnexus_explore({name: "LoginFlow", type: "process", repo: "${projectName}"})
86
+ gitnexus_search({query: "<what you want to change>", repo: "${projectName}"})
87
+ gitnexus_impact({target: "<symbol_name>", direction: "upstream", repo: "${projectName}"})
88
+ READ gitnexus://repo/${projectName}/processes → Check affected execution flows
85
89
  \`\`\`
86
90
 
87
91
  ## Graph Schema
@@ -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
package/dist/cli/mcp.js CHANGED
@@ -8,35 +8,7 @@
8
8
  import { startMCPServer } from '../mcp/server.js';
9
9
  import { LocalBackend } from '../mcp/local/local-backend.js';
10
10
  import { listRegisteredRepos } from '../storage/repo-manager.js';
11
- /**
12
- * Protect MCP stdio protocol from library stdout pollution.
13
- *
14
- * Libraries like @huggingface/transformers, ONNX Runtime, and kuzu may
15
- * write progress bars, warnings, or init messages to stdout.
16
- * MCP uses stdout exclusively for JSON-RPC — any foreign output corrupts
17
- * the protocol and causes Cursor to kill the connection.
18
- *
19
- * This intercept redirects all non-JSON-RPC stdout writes to stderr.
20
- */
21
- function installStdoutGuard() {
22
- const origWrite = process.stdout.write.bind(process.stdout);
23
- process.stdout.write = ((chunk, encodingOrCb, cb) => {
24
- const text = typeof chunk === 'string'
25
- ? chunk
26
- : Buffer.isBuffer(chunk)
27
- ? chunk.toString('utf-8')
28
- : '';
29
- // MCP SDK messages always contain "jsonrpc" — let them through
30
- if (text.includes('"jsonrpc"')) {
31
- return origWrite(chunk, encodingOrCb, cb);
32
- }
33
- // Redirect everything else to stderr (library noise)
34
- return process.stderr.write(chunk, encodingOrCb, cb);
35
- });
36
- }
37
11
  export const mcpCommand = async () => {
38
- // Must be first — before any library can pollute stdout
39
- installStdoutGuard();
40
12
  // Load all registered repos
41
13
  const entries = await listRegisteredRepos({ validate: true });
42
14
  if (entries.length === 0) {
@@ -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)
@@ -31,10 +31,19 @@ export const initEmbedder = async () => {
31
31
  const devicesToTry = ['webgpu', 'cpu'];
32
32
  for (const device of devicesToTry) {
33
33
  try {
34
- embedderInstance = await pipeline('feature-extraction', MODEL_ID, {
35
- device: device,
36
- dtype: 'fp32',
37
- });
34
+ // Silence stdout during model load — ONNX Runtime and transformers.js
35
+ // may write progress/init messages to stdout which corrupts MCP stdio protocol.
36
+ const origWrite = process.stdout.write;
37
+ process.stdout.write = (() => true);
38
+ try {
39
+ embedderInstance = await pipeline('feature-extraction', MODEL_ID, {
40
+ device: device,
41
+ dtype: 'fp32',
42
+ });
43
+ }
44
+ finally {
45
+ process.stdout.write = origWrite;
46
+ }
38
47
  console.error(`GitNexus: Embedding model loaded (${device})`);
39
48
  return embedderInstance;
40
49
  }
@@ -82,8 +82,19 @@ export const initKuzu = async (repoId, dbPath) => {
82
82
  throw new Error(`KuzuDB not found at ${dbPath}. Run: gitnexus analyze`);
83
83
  }
84
84
  evictLRU();
85
- const db = new kuzu.Database(dbPath);
86
- const conn = new kuzu.Connection(db);
85
+ // Silence stdout during KuzuDB init — native module may write to stdout
86
+ // which corrupts the MCP stdio protocol.
87
+ const origWrite = process.stdout.write;
88
+ process.stdout.write = (() => true);
89
+ let db;
90
+ let conn;
91
+ try {
92
+ db = new kuzu.Database(dbPath);
93
+ conn = new kuzu.Connection(db);
94
+ }
95
+ finally {
96
+ process.stdout.write = origWrite;
97
+ }
87
98
  pool.set(repoId, { db, conn, lastUsed: Date.now(), dbPath });
88
99
  ensureIdleTimer();
89
100
  };
@@ -123,7 +123,8 @@ export class LocalBackend {
123
123
  }
124
124
  // ─── Lazy KuzuDB Init ────────────────────────────────────────────
125
125
  async ensureInitialized(repoId) {
126
- if (this.initializedRepos.has(repoId))
126
+ // Always check the actual pool — the idle timer may have evicted the connection
127
+ if (this.initializedRepos.has(repoId) && isKuzuReady(repoId))
127
128
  return;
128
129
  const handle = this.repos.get(repoId);
129
130
  if (!handle)
@@ -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) {
@@ -282,11 +285,11 @@ export class LocalBackend {
282
285
  const bm25Results = await searchFTSFromKuzu(query, limit, repo.id);
283
286
  const results = [];
284
287
  for (const bm25Result of bm25Results) {
285
- const fileName = bm25Result.filePath.split('/').pop() || bm25Result.filePath;
288
+ const fullPath = bm25Result.filePath;
286
289
  try {
287
290
  const symbolQuery = `
288
291
  MATCH (n)
289
- WHERE n.filePath CONTAINS '${fileName.replace(/'/g, "''")}'
292
+ WHERE n.filePath = '${fullPath.replace(/'/g, "''")}'
290
293
  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
294
  LIMIT 3
292
295
  `;
@@ -305,6 +308,7 @@ export class LocalBackend {
305
308
  }
306
309
  }
307
310
  else {
311
+ const fileName = fullPath.split('/').pop() || fullPath;
308
312
  results.push({
309
313
  name: fileName,
310
314
  type: 'File',
@@ -314,6 +318,7 @@ export class LocalBackend {
314
318
  }
315
319
  }
316
320
  catch {
321
+ const fileName = fullPath.split('/').pop() || fullPath;
317
322
  results.push({
318
323
  name: fileName,
319
324
  type: 'File',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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",