mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
feat(gitnexus): v1.2.9 — impact enrichment, cypher markdown, Windows setup fix
- Impact tool now returns risk score, affected processes/modules, and summary - Cypher tool formats results as markdown tables for LLM readability - Context tool includes module (functional area) field - Semantic search skips model init when embeddings are disabled - Setup: wrap npx in cmd /c on Windows for .cmd script compatibility - Embedder: silence stderr during ONNX model load to protect MCP stdio - API: use executeCypher directly to avoid double formatting - Add community integrations section to READMEs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6be54ce9d3
commit
91289404c2
11 changed files with 166 additions and 25 deletions
|
|
@ -3,7 +3,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus MCP
|
||||
|
||||
This project is indexed by GitNexus as **GitnexusV2** (1309 symbols, 3350 relationships, 101 execution flows).
|
||||
This project is indexed by GitNexus as **GitnexusV2** (1348 symbols, 3469 relationships, 104 execution flows).
|
||||
|
||||
GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus MCP
|
||||
|
||||
This project is indexed by GitNexus as **GitnexusV2** (1309 symbols, 3350 relationships, 101 execution flows).
|
||||
This project is indexed by GitNexus as **GitnexusV2** (1348 symbols, 3469 relationships, 104 execution flows).
|
||||
|
||||
GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
|
|||
|
||||
> **Claude Code** gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context.
|
||||
|
||||
### Community Integrations
|
||||
|
||||
| Agent | Install | Source |
|
||||
|-------|---------|--------|
|
||||
| [pi](https://pi.dev) | `pi install npm:pi-gitnexus` | [pi-gitnexus](https://github.com/tintinweb/pi-gitnexus) |
|
||||
|
||||
If you prefer manual configuration:
|
||||
|
||||
**Claude Code** (full support — MCP + skills + hooks):
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
|
|||
|
||||
> **Claude Code** gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context.
|
||||
|
||||
### Community Integrations
|
||||
|
||||
| Agent | Install | Source |
|
||||
|-------|---------|--------|
|
||||
| [pi](https://pi.dev) | `pi install npm:pi-gitnexus` | [pi-gitnexus](https://github.com/tintinweb/pi-gitnexus) |
|
||||
|
||||
## MCP Setup (manual)
|
||||
|
||||
If you prefer to configure manually instead of using `gitnexus setup`:
|
||||
|
|
|
|||
4
gitnexus/package-lock.json
generated
4
gitnexus/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.1.9",
|
||||
"version": "1.2.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitnexus",
|
||||
"version": "1.1.9",
|
||||
"version": "1.2.9",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"dependencies": {
|
||||
"@huggingface/transformers": "^3.0.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.2.8",
|
||||
"version": "1.2.9",
|
||||
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
||||
"author": "Abhigyan Patwari",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
|
|
|
|||
|
|
@ -22,9 +22,16 @@ interface SetupResult {
|
|||
}
|
||||
|
||||
/**
|
||||
* The MCP server entry for all editors
|
||||
* The MCP server entry for all editors.
|
||||
* On Windows, npx must be invoked via cmd /c since it's a .cmd script.
|
||||
*/
|
||||
function getMcpEntry() {
|
||||
if (process.platform === 'win32') {
|
||||
return {
|
||||
command: 'cmd',
|
||||
args: ['/c', 'npx', '-y', 'gitnexus@latest', 'mcp'],
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', 'gitnexus@latest', 'mcp'],
|
||||
|
|
|
|||
|
|
@ -43,10 +43,13 @@ export const initEmbedder = async (): Promise<FeatureExtractionPipeline> => {
|
|||
|
||||
for (const device of devicesToTry) {
|
||||
try {
|
||||
// Silence stdout during model load — ONNX Runtime and transformers.js
|
||||
// may write progress/init messages to stdout which corrupts MCP stdio protocol.
|
||||
const origWrite = process.stdout.write;
|
||||
// Silence stdout and stderr during model load — ONNX Runtime and transformers.js
|
||||
// may write progress/init messages that corrupt MCP stdio protocol or produce
|
||||
// noisy warnings (e.g. node assignment to execution providers).
|
||||
const origStdout = process.stdout.write;
|
||||
const origStderr = process.stderr.write;
|
||||
process.stdout.write = (() => true) as any;
|
||||
process.stderr.write = (() => true) as any;
|
||||
try {
|
||||
embedderInstance = await (pipeline as any)(
|
||||
'feature-extraction',
|
||||
|
|
@ -57,7 +60,8 @@ export const initEmbedder = async (): Promise<FeatureExtractionPipeline> => {
|
|||
}
|
||||
);
|
||||
} finally {
|
||||
process.stdout.write = origWrite;
|
||||
process.stdout.write = origStdout;
|
||||
process.stderr.write = origStderr;
|
||||
}
|
||||
console.error(`GitNexus: Embedding model loaded (${device})`);
|
||||
return embedderInstance!;
|
||||
|
|
|
|||
|
|
@ -279,8 +279,10 @@ export class LocalBackend {
|
|||
switch (method) {
|
||||
case 'query':
|
||||
return this.query(repo, params);
|
||||
case 'cypher':
|
||||
return this.cypher(repo, params);
|
||||
case 'cypher': {
|
||||
const raw = await this.cypher(repo, params);
|
||||
return this.formatCypherAsMarkdown(raw);
|
||||
}
|
||||
case 'context':
|
||||
return this.context(repo, params);
|
||||
case 'impact':
|
||||
|
|
@ -395,16 +397,18 @@ export class LocalBackend {
|
|||
`);
|
||||
} catch { /* symbol might not be in any process */ }
|
||||
|
||||
// Get cluster cohesion as internal ranking signal (never exposed)
|
||||
// Get cluster membership + cohesion (cohesion used as internal ranking signal)
|
||||
let cohesion = 0;
|
||||
let module: string | undefined;
|
||||
try {
|
||||
const cohesionRows = await executeQuery(repo.id, `
|
||||
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
RETURN c.cohesion AS cohesion
|
||||
RETURN c.cohesion AS cohesion, c.heuristicLabel AS module
|
||||
LIMIT 1
|
||||
`);
|
||||
if (cohesionRows.length > 0) {
|
||||
cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0;
|
||||
module = cohesionRows[0].module ?? cohesionRows[0][1];
|
||||
}
|
||||
} catch { /* no cluster info */ }
|
||||
|
||||
|
|
@ -429,6 +433,7 @@ export class LocalBackend {
|
|||
filePath: sym.filePath,
|
||||
startLine: sym.startLine,
|
||||
endLine: sym.endLine,
|
||||
...(module ? { module } : {}),
|
||||
...(includeContent && content ? { content } : {}),
|
||||
};
|
||||
|
||||
|
|
@ -577,6 +582,10 @@ export class LocalBackend {
|
|||
*/
|
||||
private async semanticSearch(repo: RepoHandle, query: string, limit: number): Promise<any[]> {
|
||||
try {
|
||||
// Check if embedding table exists before loading the model (avoids heavy model init when embeddings are off)
|
||||
const tableCheck = await executeQuery(repo.id, `MATCH (e:CodeEmbedding) RETURN COUNT(*) AS cnt LIMIT 1`);
|
||||
if (!tableCheck.length || (tableCheck[0].cnt ?? tableCheck[0][0]) === 0) return [];
|
||||
|
||||
const queryVec = await embedQuery(query);
|
||||
const dims = getEmbeddingDims();
|
||||
const queryVecStr = `[${queryVec.join(',')}]`;
|
||||
|
|
@ -630,8 +639,8 @@ export class LocalBackend {
|
|||
}
|
||||
|
||||
return results;
|
||||
} catch (err: any) {
|
||||
console.error('GitNexus: Semantic search unavailable -', err.message);
|
||||
} catch {
|
||||
// Expected when embeddings are disabled — silently fall back to BM25-only
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -643,11 +652,11 @@ export class LocalBackend {
|
|||
|
||||
private async cypher(repo: RepoHandle, params: { query: string }): Promise<any> {
|
||||
await this.ensureInitialized(repo.id);
|
||||
|
||||
|
||||
if (!isKuzuReady(repo.id)) {
|
||||
return { error: 'KuzuDB not ready. Index may be corrupted.' };
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const result = await executeQuery(repo.id, params.query);
|
||||
return result;
|
||||
|
|
@ -656,6 +665,36 @@ export class LocalBackend {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format raw Cypher result rows as a markdown table for LLM readability.
|
||||
* Falls back to raw result if rows aren't tabular objects.
|
||||
*/
|
||||
private formatCypherAsMarkdown(result: any): any {
|
||||
if (!Array.isArray(result) || result.length === 0) return result;
|
||||
|
||||
const firstRow = result[0];
|
||||
if (typeof firstRow !== 'object' || firstRow === null) return result;
|
||||
|
||||
const keys = Object.keys(firstRow);
|
||||
if (keys.length === 0) return result;
|
||||
|
||||
const header = '| ' + keys.join(' | ') + ' |';
|
||||
const separator = '| ' + keys.map(() => '---').join(' | ') + ' |';
|
||||
const dataRows = result.map((row: any) =>
|
||||
'| ' + keys.map(k => {
|
||||
const v = row[k];
|
||||
if (v === null || v === undefined) return '';
|
||||
if (typeof v === 'object') return JSON.stringify(v);
|
||||
return String(v);
|
||||
}).join(' | ') + ' |'
|
||||
);
|
||||
|
||||
return {
|
||||
markdown: [header, separator, ...dataRows].join('\n'),
|
||||
row_count: result.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate same-named clusters: group by heuristicLabel, sum symbols,
|
||||
* weighted-average cohesion, filter out tiny clusters (<5 symbols).
|
||||
|
|
@ -1318,7 +1357,69 @@ export class LocalBackend {
|
|||
if (!grouped[item.depth]) grouped[item.depth] = [];
|
||||
grouped[item.depth].push(item);
|
||||
}
|
||||
|
||||
|
||||
// ── Enrichment: affected processes, modules, risk ──────────────
|
||||
const directCount = (grouped[1] || []).length;
|
||||
let affectedProcesses: any[] = [];
|
||||
let affectedModules: any[] = [];
|
||||
|
||||
if (impacted.length > 0) {
|
||||
const allIds = impacted.map(i => `'${i.id.replace(/'/g, "''")}'`).join(', ');
|
||||
const d1Ids = (grouped[1] || []).map((i: any) => `'${i.id.replace(/'/g, "''")}'`).join(', ');
|
||||
|
||||
// Affected processes: which execution flows are broken and at which step
|
||||
const [processRows, moduleRows, directModuleRows] = await Promise.all([
|
||||
executeQuery(repo.id, `
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
WHERE s.id IN [${allIds}]
|
||||
RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount
|
||||
ORDER BY hits DESC
|
||||
LIMIT 20
|
||||
`).catch(() => []),
|
||||
executeQuery(repo.id, `
|
||||
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
WHERE s.id IN [${allIds}]
|
||||
RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
|
||||
ORDER BY hits DESC
|
||||
LIMIT 20
|
||||
`).catch(() => []),
|
||||
d1Ids ? executeQuery(repo.id, `
|
||||
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
WHERE s.id IN [${d1Ids}]
|
||||
RETURN DISTINCT c.heuristicLabel AS name
|
||||
`).catch(() => []) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
affectedProcesses = processRows.map((r: any) => ({
|
||||
name: r.name || r[0],
|
||||
hits: r.hits || r[1],
|
||||
broken_at_step: r.minStep ?? r[2],
|
||||
step_count: r.stepCount ?? r[3],
|
||||
}));
|
||||
|
||||
const directModuleSet = new Set(directModuleRows.map((r: any) => r.name || r[0]));
|
||||
affectedModules = moduleRows.map((r: any) => {
|
||||
const name = r.name || r[0];
|
||||
return {
|
||||
name,
|
||||
hits: r.hits || r[1],
|
||||
impact: directModuleSet.has(name) ? 'direct' : 'indirect',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Risk scoring
|
||||
const processCount = affectedProcesses.length;
|
||||
const moduleCount = affectedModules.length;
|
||||
let risk = 'LOW';
|
||||
if (directCount >= 30 || processCount >= 5 || moduleCount >= 5 || impacted.length >= 200) {
|
||||
risk = 'CRITICAL';
|
||||
} else if (directCount >= 15 || processCount >= 3 || moduleCount >= 3 || impacted.length >= 100) {
|
||||
risk = 'HIGH';
|
||||
} else if (directCount >= 5 || impacted.length >= 30) {
|
||||
risk = 'MEDIUM';
|
||||
}
|
||||
|
||||
return {
|
||||
target: {
|
||||
id: symId,
|
||||
|
|
@ -1328,6 +1429,14 @@ export class LocalBackend {
|
|||
},
|
||||
direction,
|
||||
impactedCount: impacted.length,
|
||||
risk,
|
||||
summary: {
|
||||
direct: directCount,
|
||||
processes_affected: processCount,
|
||||
modules_affected: moduleCount,
|
||||
},
|
||||
affected_processes: affectedProcesses,
|
||||
affected_modules: affectedModules,
|
||||
byDepth: grouped,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, cal
|
|||
|
||||
Returns results grouped by process (execution flow):
|
||||
- processes: ranked execution flows with relevance priority
|
||||
- process_symbols: all symbols in those flows with file locations
|
||||
- process_symbols: all symbols in those flows with file locations and module (functional area)
|
||||
- definitions: standalone types/interfaces not in any process
|
||||
|
||||
Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.`,
|
||||
|
|
@ -91,6 +91,8 @@ EXAMPLES:
|
|||
• Trace a process:
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step
|
||||
|
||||
OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown table for easy reading.
|
||||
|
||||
TIPS:
|
||||
- All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc.
|
||||
- Community = auto-detected functional area (Leiden algorithm)
|
||||
|
|
@ -172,10 +174,17 @@ Each edit is tagged with confidence:
|
|||
{
|
||||
name: 'impact',
|
||||
description: `Analyze the blast radius of changing a code symbol.
|
||||
Returns all symbols affected by modifying the target, grouped by depth with edge types and confidence.
|
||||
Returns affected symbols grouped by depth, plus risk assessment, affected execution flows, and affected modules.
|
||||
|
||||
WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
|
||||
AFTER THIS: Review d=1 items (WILL BREAK). READ gitnexus://repo/{name}/processes to check affected execution flows.
|
||||
AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
|
||||
|
||||
Output includes:
|
||||
- risk: LOW / MEDIUM / HIGH / CRITICAL
|
||||
- summary: direct callers, processes affected, modules affected
|
||||
- affected_processes: which execution flows break and at which step
|
||||
- affected_modules: which functional areas are hit (direct vs indirect)
|
||||
- byDepth: all affected symbols grouped by traversal depth
|
||||
|
||||
Depth groups:
|
||||
- d=1: WILL BREAK (direct callers/importers)
|
||||
|
|
|
|||
|
|
@ -191,9 +191,9 @@ export const createServer = async (port: number) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const result = await backend.callTool('cypher', { repo: repoName, query: cypher });
|
||||
if (result && !Array.isArray(result) && result.error) {
|
||||
res.status(500).json({ error: result.error });
|
||||
const result = await backend.executeCypher(repoName, cypher);
|
||||
if (result && !Array.isArray(result) && (result as any).error) {
|
||||
res.status(500).json({ error: (result as any).error });
|
||||
return;
|
||||
}
|
||||
res.json({ result });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue