mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
MCP and cli fixes
This commit is contained in:
parent
0943df9feb
commit
99766d9cd2
33 changed files with 2643 additions and 1317 deletions
|
|
@ -7,10 +7,11 @@ description: Trace bugs through call chains using knowledge graph
|
|||
|
||||
## Quick Start
|
||||
```
|
||||
0. If "Index is stale" → gitnexus_analyze({})
|
||||
1. gitnexus_search({query}) → Find code related to error
|
||||
2. gitnexus_explore({name, type: "symbol"}) → Get callers and callees
|
||||
3. READ gitnexus://process/{name} → Trace execution flow
|
||||
0. READ gitnexus://repos → Discover indexed repos
|
||||
1. If "Index is stale" → gitnexus_analyze({repo: "my-app"})
|
||||
2. gitnexus_search({query: "...", repo: "my-app"}) → Find code related to error
|
||||
3. gitnexus_explore({name, type: "symbol", repo: "my-app"}) → Get callers and callees
|
||||
4. READ gitnexus://repo/my-app/process/{name} → Trace execution flow
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
|
@ -22,18 +23,19 @@ description: Trace bugs through call chains using knowledge graph
|
|||
## Workflow Checklist
|
||||
```
|
||||
Bug Investigation:
|
||||
- [ ] READ gitnexus://repos to find the right repo
|
||||
- [ ] Understand the symptom (error message, behavior)
|
||||
- [ ] gitnexus_search to find related code
|
||||
- [ ] Identify the suspect function
|
||||
- [ ] gitnexus_explore to see callers/callees
|
||||
- [ ] READ gitnexus://process/{name} if suspect is in a process
|
||||
- [ ] READ gitnexus://schema for Cypher query help
|
||||
- [ ] READ gitnexus://repo/{name}/process/{name} if suspect is in a process
|
||||
- [ ] READ gitnexus://repo/{name}/schema for Cypher query help
|
||||
- [ ] gitnexus_cypher for custom traces
|
||||
```
|
||||
|
||||
## Resource Reference
|
||||
|
||||
### gitnexus://schema
|
||||
### gitnexus://repo/{name}/schema
|
||||
Graph schema for writing Cypher queries:
|
||||
```yaml
|
||||
nodes: [Function, Class, Method, File, Community, Process]
|
||||
|
|
@ -44,7 +46,7 @@ example_queries:
|
|||
RETURN caller.name
|
||||
```
|
||||
|
||||
### gitnexus://process/{name}
|
||||
### gitnexus://repo/{name}/process/{processName}
|
||||
Trace execution flow to find where bug might occur:
|
||||
```yaml
|
||||
name: CheckoutFlow
|
||||
|
|
@ -60,13 +62,13 @@ trace:
|
|||
### gitnexus_search
|
||||
Find code related to error or symptom:
|
||||
```
|
||||
gitnexus_search({query: "payment validation error", depth: "full"})
|
||||
gitnexus_search({query: "payment validation error", depth: "full", repo: "my-app"})
|
||||
```
|
||||
|
||||
### gitnexus_explore
|
||||
Get symbol context:
|
||||
```
|
||||
gitnexus_explore({name: "validatePayment", type: "symbol"})
|
||||
gitnexus_explore({name: "validatePayment", type: "symbol", repo: "my-app"})
|
||||
→ Callers: processCheckout, webhookHandler
|
||||
→ Callees: verifyCard, fetchRates
|
||||
```
|
||||
|
|
@ -82,13 +84,13 @@ RETURN [n IN nodes(path) | n.name] AS chain
|
|||
## Example: "Payment endpoint returns 500 intermittently"
|
||||
|
||||
```
|
||||
1. gitnexus_search({query: "payment error handling"})
|
||||
1. gitnexus_search({query: "payment error handling", repo: "my-app"})
|
||||
→ validatePayment, handlePaymentError, PaymentException
|
||||
|
||||
2. gitnexus_explore({name: "validatePayment", type: "symbol"})
|
||||
2. gitnexus_explore({name: "validatePayment", type: "symbol", repo: "my-app"})
|
||||
→ Callees: verifyCard, fetchRates (external API!)
|
||||
|
||||
3. READ gitnexus://process/CheckoutFlow
|
||||
3. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
→ Step 3: validatePayment → calls external API
|
||||
|
||||
4. Root cause: fetchRates calls external API without proper timeout
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
|
|||
|
||||
## Quick Start
|
||||
```
|
||||
0. If "Index is stale" → gitnexus_analyze({})
|
||||
1. READ gitnexus://context → Get codebase overview (~150 tokens)
|
||||
2. READ gitnexus://clusters → See all functional clusters
|
||||
3. READ gitnexus://cluster/{name} → Deep dive on specific cluster
|
||||
0. READ gitnexus://repos → Discover indexed repos (use repo param if multiple)
|
||||
1. If "Index is stale" → gitnexus_analyze({repo: "my-app"})
|
||||
2. READ gitnexus://repo/{name}/context → Get codebase overview (~150 tokens)
|
||||
3. READ gitnexus://repo/{name}/clusters → See all functional clusters
|
||||
4. READ gitnexus://repo/{name}/cluster/{name} → Deep dive on specific cluster
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
|
@ -22,17 +23,28 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
|
|||
## Workflow Checklist
|
||||
```
|
||||
Exploration Progress:
|
||||
- [ ] READ gitnexus://context for codebase overview
|
||||
- [ ] READ gitnexus://clusters to list all clusters
|
||||
- [ ] READ gitnexus://repos to discover available repos
|
||||
- [ ] READ gitnexus://repo/{name}/context for codebase overview
|
||||
- [ ] READ gitnexus://repo/{name}/clusters to list all clusters
|
||||
- [ ] Identify the relevant cluster by name
|
||||
- [ ] READ gitnexus://cluster/{name} for cluster details
|
||||
- [ ] READ gitnexus://repo/{name}/cluster/{name} for cluster details
|
||||
- [ ] Use gitnexus_explore for specific symbols
|
||||
```
|
||||
|
||||
## Resource Reference
|
||||
|
||||
### gitnexus://context
|
||||
Codebase overview. **Read first.**
|
||||
### gitnexus://repos
|
||||
Discover all indexed repositories. **Read first.**
|
||||
```yaml
|
||||
repos:
|
||||
- name: "my-app"
|
||||
path: "/home/user/my-app"
|
||||
files: 42
|
||||
symbols: 918
|
||||
```
|
||||
|
||||
### gitnexus://repo/{name}/context
|
||||
Codebase overview for a specific repo.
|
||||
```yaml
|
||||
project: my-app
|
||||
stats:
|
||||
|
|
@ -40,11 +52,10 @@ stats:
|
|||
symbols: 918
|
||||
clusters: 12
|
||||
processes: 45
|
||||
tools_available: [search, explore, impact, overview, cypher]
|
||||
resources_available: [clusters, processes, cluster/{name}, process/{name}]
|
||||
tools_available: [list_repos, search, explore, impact, overview, cypher]
|
||||
```
|
||||
|
||||
### gitnexus://clusters
|
||||
### gitnexus://repo/{name}/clusters
|
||||
All functional clusters with cohesion scores.
|
||||
```yaml
|
||||
clusters:
|
||||
|
|
@ -56,7 +67,7 @@ clusters:
|
|||
cohesion: 88%
|
||||
```
|
||||
|
||||
### gitnexus://cluster/{name}
|
||||
### gitnexus://repo/{name}/cluster/{clusterName}
|
||||
Members of a specific cluster.
|
||||
```yaml
|
||||
name: Auth
|
||||
|
|
@ -68,7 +79,7 @@ members:
|
|||
file: src/auth/validator.ts
|
||||
```
|
||||
|
||||
### gitnexus://process/{name}
|
||||
### gitnexus://repo/{name}/process/{processName}
|
||||
Full execution trace.
|
||||
```yaml
|
||||
name: LoginFlow
|
||||
|
|
@ -84,7 +95,7 @@ steps:
|
|||
### gitnexus_explore
|
||||
For detailed symbol context with callers/callees:
|
||||
```
|
||||
gitnexus_explore({name: "validateUser", type: "symbol"})
|
||||
gitnexus_explore({name: "validateUser", type: "symbol", repo: "my-app"})
|
||||
→ Callers: loginHandler, apiMiddleware
|
||||
→ Callees: checkToken, getUserById
|
||||
```
|
||||
|
|
@ -92,21 +103,24 @@ gitnexus_explore({name: "validateUser", type: "symbol"})
|
|||
### gitnexus_search
|
||||
For finding code by query:
|
||||
```
|
||||
gitnexus_search({query: "payment validation", depth: "full"})
|
||||
gitnexus_search({query: "payment validation", depth: "full", repo: "my-app"})
|
||||
```
|
||||
|
||||
## Example: "How does payment processing work?"
|
||||
|
||||
```
|
||||
1. READ gitnexus://context
|
||||
1. READ gitnexus://repos
|
||||
→ Repos: my-app (918 symbols)
|
||||
|
||||
2. READ gitnexus://repo/my-app/context
|
||||
→ 918 symbols, 12 clusters
|
||||
|
||||
2. READ gitnexus://clusters
|
||||
3. READ gitnexus://repo/my-app/clusters
|
||||
→ Clusters: Auth, Payment, Database, API...
|
||||
|
||||
3. READ gitnexus://cluster/Payment
|
||||
4. READ gitnexus://repo/my-app/cluster/Payment
|
||||
→ Members: processPayment, validateCard, PaymentService
|
||||
|
||||
4. READ gitnexus://process/CheckoutFlow
|
||||
5. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
→ handleCheckout → validateCart → processPayment → sendConfirmation
|
||||
```
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ description: Analyze blast radius before making code changes
|
|||
|
||||
## Quick Start
|
||||
```
|
||||
0. If "Index is stale" → gitnexus_analyze({})
|
||||
1. gitnexus_impact({target, direction: "upstream"}) → What depends on this
|
||||
2. READ gitnexus://clusters → Check affected areas
|
||||
3. READ gitnexus://processes → Affected execution flows
|
||||
0. READ gitnexus://repos → Discover indexed repos
|
||||
1. If "Index is stale" → gitnexus_analyze({repo: "my-app"})
|
||||
2. gitnexus_impact({target, direction: "upstream", repo: "my-app"}) → What depends on this
|
||||
3. READ gitnexus://repo/my-app/clusters → Check affected areas
|
||||
4. READ gitnexus://repo/my-app/processes → Affected execution flows
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
|
@ -30,8 +31,9 @@ description: Analyze blast radius before making code changes
|
|||
## Workflow Checklist
|
||||
```
|
||||
Impact Analysis:
|
||||
- [ ] gitnexus_impact(target, "upstream") to find dependents
|
||||
- [ ] READ gitnexus://clusters to understand affected areas
|
||||
- [ ] READ gitnexus://repos to find the right repo
|
||||
- [ ] gitnexus_impact(target, "upstream", repo) to find dependents
|
||||
- [ ] READ gitnexus://repo/{name}/clusters to understand affected areas
|
||||
- [ ] Check high-confidence (>0.8) dependencies first
|
||||
- [ ] Count affected clusters (cross-cutting = higher risk)
|
||||
- [ ] If >10 processes affected, consider splitting change
|
||||
|
|
@ -39,7 +41,7 @@ Impact Analysis:
|
|||
|
||||
## Resource Reference
|
||||
|
||||
### gitnexus://clusters
|
||||
### gitnexus://repo/{name}/clusters
|
||||
Check which clusters might be affected:
|
||||
```yaml
|
||||
clusters:
|
||||
|
|
@ -49,7 +51,7 @@ clusters:
|
|||
symbols: 32
|
||||
```
|
||||
|
||||
### gitnexus://processes
|
||||
### gitnexus://repo/{name}/processes
|
||||
Find which processes touch the target:
|
||||
```yaml
|
||||
processes:
|
||||
|
|
@ -67,7 +69,8 @@ gitnexus_impact({
|
|||
target: "validateUser",
|
||||
direction: "upstream",
|
||||
minConfidence: 0.8,
|
||||
maxDepth: 3
|
||||
maxDepth: 3,
|
||||
repo: "my-app"
|
||||
})
|
||||
|
||||
→ d=1 (WILL BREAK):
|
||||
|
|
@ -103,11 +106,11 @@ Before Committing:
|
|||
## Example: "What breaks if I change validateUser?"
|
||||
|
||||
```
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream", repo: "my-app"})
|
||||
→ d=1: loginHandler, apiMiddleware
|
||||
→ d=2: authRouter, sessionManager
|
||||
|
||||
2. READ gitnexus://clusters
|
||||
2. READ gitnexus://repo/my-app/clusters
|
||||
→ Auth and API clusters affected
|
||||
|
||||
3. Decision: 2 direct callers, 2 clusters = MEDIUM risk
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ description: Plan safe refactors using blast radius and dependency mapping
|
|||
|
||||
## Quick Start
|
||||
```
|
||||
0. If "Index is stale" → gitnexus_analyze({})
|
||||
1. gitnexus_impact({target, direction: "upstream"}) → Map all dependents
|
||||
2. READ gitnexus://schema → Understand graph structure
|
||||
3. gitnexus_cypher → Find all references
|
||||
0. READ gitnexus://repos → Discover indexed repos
|
||||
1. If "Index is stale" → gitnexus_analyze({repo: "my-app"})
|
||||
2. gitnexus_impact({target, direction: "upstream", repo: "my-app"}) → Map all dependents
|
||||
3. READ gitnexus://repo/my-app/schema → Understand graph structure
|
||||
4. gitnexus_cypher({query: "...", repo: "my-app"}) → Find all references
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
|
@ -24,8 +25,8 @@ description: Plan safe refactors using blast radius and dependency mapping
|
|||
### Rename Symbol
|
||||
```
|
||||
Rename Refactoring:
|
||||
- [ ] gitnexus_impact(oldName, "upstream") — find all callers
|
||||
- [ ] gitnexus_search(oldName) — find string literals
|
||||
- [ ] gitnexus_impact({target: oldName, direction: "upstream", repo: "my-app"}) — find all callers
|
||||
- [ ] gitnexus_search({query: oldName, repo: "my-app"}) — find string literals
|
||||
- [ ] Check for reflection/dynamic references
|
||||
- [ ] Update in order: interface → implementation → usages
|
||||
- [ ] Run tests for affected processes
|
||||
|
|
@ -34,9 +35,9 @@ Rename Refactoring:
|
|||
### Extract Module
|
||||
```
|
||||
Extract Module:
|
||||
- [ ] gitnexus_explore(target, "symbol") — map dependencies
|
||||
- [ ] gitnexus_impact(target, "upstream") — find callers
|
||||
- [ ] READ gitnexus://cluster/{name} — check cohesion
|
||||
- [ ] gitnexus_explore({name: target, type: "symbol", repo: "my-app"}) — map dependencies
|
||||
- [ ] gitnexus_impact({target, direction: "upstream", repo: "my-app"}) — find callers
|
||||
- [ ] READ gitnexus://repo/my-app/cluster/{name} — check cohesion
|
||||
- [ ] Define new module interface
|
||||
- [ ] Update imports across affected files
|
||||
```
|
||||
|
|
@ -44,7 +45,7 @@ Extract Module:
|
|||
### Split Function
|
||||
```
|
||||
Split Function:
|
||||
- [ ] gitnexus_explore(target, "symbol") — understand callees
|
||||
- [ ] gitnexus_explore({name: target, type: "symbol", repo: "my-app"}) — understand callees
|
||||
- [ ] Group related logic
|
||||
- [ ] gitnexus_impact — verify callers won't break
|
||||
- [ ] Create new functions
|
||||
|
|
@ -53,7 +54,7 @@ Split Function:
|
|||
|
||||
## Resource Reference
|
||||
|
||||
### gitnexus://schema
|
||||
### gitnexus://repo/{name}/schema
|
||||
Graph structure for Cypher queries:
|
||||
```yaml
|
||||
nodes: [Function, Class, Method, Community, Process]
|
||||
|
|
@ -65,7 +66,7 @@ example_queries:
|
|||
RETURN caller.name
|
||||
```
|
||||
|
||||
### gitnexus://cluster/{name}
|
||||
### gitnexus://repo/{name}/cluster/{clusterName}
|
||||
Check if extraction preserves cohesion:
|
||||
```yaml
|
||||
name: Payment
|
||||
|
|
@ -91,13 +92,13 @@ RETURN importer.name, importer.filePath
|
|||
## Example: Safely Rename `validateUser` to `authenticateUser`
|
||||
|
||||
```
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream", repo: "my-app"})
|
||||
→ loginHandler, apiMiddleware, testUtils
|
||||
|
||||
2. gitnexus_search({query: "validateUser"})
|
||||
2. gitnexus_search({query: "validateUser", repo: "my-app"})
|
||||
→ Found in: config.json (dynamic reference!)
|
||||
|
||||
3. READ gitnexus://processes
|
||||
3. READ gitnexus://repo/my-app/processes
|
||||
→ LoginFlow, TokenRefresh, APIGateway
|
||||
|
||||
4. Plan update order:
|
||||
|
|
|
|||
54
AGENTS.md
54
AGENTS.md
|
|
@ -7,63 +7,71 @@ This project uses GitNexus MCP for code intelligence. See .gitnexus/RULES.md for
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus MCP
|
||||
|
||||
This project is indexed by GitNexus, providing AI agents with deep code intelligence.
|
||||
This project is indexed as **GitnexusV2** by GitNexus, providing AI agents with deep code intelligence.
|
||||
|
||||
## Project: GitnexusV2
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Files | 146 |
|
||||
| Symbols | 907 |
|
||||
| Relationships | 2306 |
|
||||
| Communities | 278 |
|
||||
| Processes | 75 |
|
||||
| Files | 147 |
|
||||
| Symbols | 940 |
|
||||
| Relationships | 2298 |
|
||||
| Clusters | 13 |
|
||||
| Processes | 67 |
|
||||
|
||||
> **Staleness:** If the index is out of date, run `gitnexus_analyze({repo: "GitnexusV2"})` to refresh. The `gitnexus://repo/GitnexusV2/context` resource will warn you when the index is stale.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```
|
||||
1. READ gitnexus://context → Get codebase overview (~150 tokens)
|
||||
2. READ gitnexus://clusters → See all functional clusters
|
||||
3. READ gitnexus://cluster/{name} → Deep dive on specific cluster
|
||||
4. gitnexus_search(query) → Find code by query
|
||||
1. READ gitnexus://repos → Discover all indexed repos
|
||||
2. READ gitnexus://repo/GitnexusV2/context → Get codebase overview (~150 tokens)
|
||||
3. READ gitnexus://repo/GitnexusV2/clusters → See all functional clusters
|
||||
4. gitnexus_search({query: "...", repo: "GitnexusV2"}) → Find code by query
|
||||
```
|
||||
|
||||
## Available Resources
|
||||
|
||||
| Resource | Purpose |
|
||||
|----------|---------|
|
||||
| `gitnexus://context` | Codebase stats, tools, and resources overview |
|
||||
| `gitnexus://clusters` | All clusters with symbol counts and cohesion |
|
||||
| `gitnexus://cluster/{name}` | Cluster members and details |
|
||||
| `gitnexus://processes` | All execution flows with types |
|
||||
| `gitnexus://process/{name}` | Full process trace with steps |
|
||||
| `gitnexus://schema` | Graph schema for Cypher queries |
|
||||
| `gitnexus://repos` | List all indexed repositories |
|
||||
| `gitnexus://repo/GitnexusV2/context` | Codebase stats, tools, and resources overview |
|
||||
| `gitnexus://repo/GitnexusV2/clusters` | All clusters with symbol counts and cohesion |
|
||||
| `gitnexus://repo/GitnexusV2/cluster/{name}` | Cluster members and details |
|
||||
| `gitnexus://repo/GitnexusV2/processes` | All execution flows with types |
|
||||
| `gitnexus://repo/GitnexusV2/process/{name}` | Full process trace with steps |
|
||||
| `gitnexus://repo/GitnexusV2/schema` | Graph schema for Cypher queries |
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Purpose | When to Use |
|
||||
|------|---------|-------------|
|
||||
| `list_repos` | Discover indexed repos | First step with multiple repos |
|
||||
| `search` | Semantic + keyword search | Finding code by query |
|
||||
| `overview` | List clusters & processes | Understanding architecture |
|
||||
| `explore` | Deep dive on symbol/cluster/process | Detailed investigation |
|
||||
| `impact` | Blast radius analysis | Before making changes |
|
||||
| `cypher` | Raw graph queries | Complex analysis |
|
||||
| `analyze` | Re-index repository | When index is stale or after major code changes |
|
||||
|
||||
> **Multi-repo:** When multiple repos are indexed, pass `repo: "GitnexusV2"` to target this project.
|
||||
|
||||
## Workflow Examples
|
||||
|
||||
### Exploring the Codebase
|
||||
```
|
||||
READ gitnexus://context → Stats and overview
|
||||
READ gitnexus://clusters → Find relevant cluster
|
||||
READ gitnexus://cluster/Auth → Explore Auth cluster
|
||||
gitnexus_explore("validateUser", "symbol") → Detailed symbol info
|
||||
READ gitnexus://repos → Discover repos
|
||||
READ gitnexus://repo/GitnexusV2/context → Stats and overview (check for staleness)
|
||||
READ gitnexus://repo/GitnexusV2/clusters → Find relevant cluster by name
|
||||
READ gitnexus://repo/GitnexusV2/cluster/{name} → See members of that cluster
|
||||
gitnexus_explore({name: "<symbol_name>", type: "symbol", repo: "GitnexusV2"})
|
||||
```
|
||||
|
||||
### Planning a Change
|
||||
```
|
||||
gitnexus_impact("UserService", "upstream") → See what breaks
|
||||
READ gitnexus://processes → Check affected flows
|
||||
gitnexus_explore("LoginFlow", "process") → Trace execution
|
||||
gitnexus_search({query: "<what you want to change>", repo: "GitnexusV2"})
|
||||
gitnexus_impact({target: "<symbol_name>", direction: "upstream", repo: "GitnexusV2"})
|
||||
READ gitnexus://repo/GitnexusV2/processes → Check affected execution flows
|
||||
```
|
||||
|
||||
## Graph Schema
|
||||
|
|
|
|||
54
CLAUDE.md
54
CLAUDE.md
|
|
@ -1,63 +1,71 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus MCP
|
||||
|
||||
This project is indexed by GitNexus, providing AI agents with deep code intelligence.
|
||||
This project is indexed as **GitnexusV2** by GitNexus, providing AI agents with deep code intelligence.
|
||||
|
||||
## Project: GitnexusV2
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Files | 146 |
|
||||
| Symbols | 907 |
|
||||
| Relationships | 2306 |
|
||||
| Communities | 278 |
|
||||
| Processes | 75 |
|
||||
| Files | 147 |
|
||||
| Symbols | 940 |
|
||||
| Relationships | 2298 |
|
||||
| Clusters | 13 |
|
||||
| Processes | 67 |
|
||||
|
||||
> **Staleness:** If the index is out of date, run `gitnexus_analyze({repo: "GitnexusV2"})` to refresh. The `gitnexus://repo/GitnexusV2/context` resource will warn you when the index is stale.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```
|
||||
1. READ gitnexus://context → Get codebase overview (~150 tokens)
|
||||
2. READ gitnexus://clusters → See all functional clusters
|
||||
3. READ gitnexus://cluster/{name} → Deep dive on specific cluster
|
||||
4. gitnexus_search(query) → Find code by query
|
||||
1. READ gitnexus://repos → Discover all indexed repos
|
||||
2. READ gitnexus://repo/GitnexusV2/context → Get codebase overview (~150 tokens)
|
||||
3. READ gitnexus://repo/GitnexusV2/clusters → See all functional clusters
|
||||
4. gitnexus_search({query: "...", repo: "GitnexusV2"}) → Find code by query
|
||||
```
|
||||
|
||||
## Available Resources
|
||||
|
||||
| Resource | Purpose |
|
||||
|----------|---------|
|
||||
| `gitnexus://context` | Codebase stats, tools, and resources overview |
|
||||
| `gitnexus://clusters` | All clusters with symbol counts and cohesion |
|
||||
| `gitnexus://cluster/{name}` | Cluster members and details |
|
||||
| `gitnexus://processes` | All execution flows with types |
|
||||
| `gitnexus://process/{name}` | Full process trace with steps |
|
||||
| `gitnexus://schema` | Graph schema for Cypher queries |
|
||||
| `gitnexus://repos` | List all indexed repositories |
|
||||
| `gitnexus://repo/GitnexusV2/context` | Codebase stats, tools, and resources overview |
|
||||
| `gitnexus://repo/GitnexusV2/clusters` | All clusters with symbol counts and cohesion |
|
||||
| `gitnexus://repo/GitnexusV2/cluster/{name}` | Cluster members and details |
|
||||
| `gitnexus://repo/GitnexusV2/processes` | All execution flows with types |
|
||||
| `gitnexus://repo/GitnexusV2/process/{name}` | Full process trace with steps |
|
||||
| `gitnexus://repo/GitnexusV2/schema` | Graph schema for Cypher queries |
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Purpose | When to Use |
|
||||
|------|---------|-------------|
|
||||
| `list_repos` | Discover indexed repos | First step with multiple repos |
|
||||
| `search` | Semantic + keyword search | Finding code by query |
|
||||
| `overview` | List clusters & processes | Understanding architecture |
|
||||
| `explore` | Deep dive on symbol/cluster/process | Detailed investigation |
|
||||
| `impact` | Blast radius analysis | Before making changes |
|
||||
| `cypher` | Raw graph queries | Complex analysis |
|
||||
| `analyze` | Re-index repository | When index is stale or after major code changes |
|
||||
|
||||
> **Multi-repo:** When multiple repos are indexed, pass `repo: "GitnexusV2"` to target this project.
|
||||
|
||||
## Workflow Examples
|
||||
|
||||
### Exploring the Codebase
|
||||
```
|
||||
READ gitnexus://context → Stats and overview
|
||||
READ gitnexus://clusters → Find relevant cluster
|
||||
READ gitnexus://cluster/Auth → Explore Auth cluster
|
||||
gitnexus_explore("validateUser", "symbol") → Detailed symbol info
|
||||
READ gitnexus://repos → Discover repos
|
||||
READ gitnexus://repo/GitnexusV2/context → Stats and overview (check for staleness)
|
||||
READ gitnexus://repo/GitnexusV2/clusters → Find relevant cluster by name
|
||||
READ gitnexus://repo/GitnexusV2/cluster/{name} → See members of that cluster
|
||||
gitnexus_explore({name: "<symbol_name>", type: "symbol", repo: "GitnexusV2"})
|
||||
```
|
||||
|
||||
### Planning a Change
|
||||
```
|
||||
gitnexus_impact("UserService", "upstream") → See what breaks
|
||||
READ gitnexus://processes → Check affected flows
|
||||
gitnexus_explore("LoginFlow", "process") → Trace execution
|
||||
gitnexus_search({query: "<what you want to change>", repo: "GitnexusV2"})
|
||||
gitnexus_impact({target: "<symbol_name>", direction: "upstream", repo: "GitnexusV2"})
|
||||
READ gitnexus://repo/GitnexusV2/processes → Check affected execution flows
|
||||
```
|
||||
|
||||
## Graph Schema
|
||||
|
|
|
|||
764
README.md
764
README.md
|
|
@ -1,183 +1,361 @@
|
|||
# GitNexus V2
|
||||
# GitNexus
|
||||
|
||||
**Zero-Server, Graph-Based Code Intelligence Engine**
|
||||
Works fully in-browser through WebAssembly. (DB engine, Embeddings model, AST parsing, all happens inside browser)
|
||||
**Building git for agent context.**
|
||||
|
||||
Indexes any codebase into a knowledge graph — every dependency, call chain, cluster, and execution flow — then exposes it through smart tools so AI agents never miss code.
|
||||
|
||||
[](https://www.npmjs.com/package/gitnexus)
|
||||
[](https://polyformproject.org/licenses/noncommercial/1.0.0/)
|
||||
|
||||
https://github.com/user-attachments/assets/abfd0300-0aae-4296-b8d3-8b72ed882433
|
||||
|
||||
https://gitnexus.vercel.app
|
||||
Being client sided, it costs me zero to deploy, so you can use it for free :-) (would love a ⭐ though)
|
||||
> *Like DeepWiki, but deeper.* DeepWiki helps you *understand* code. GitNexus lets you *analyze* it — because a knowledge graph tracks every relationship, not just descriptions.
|
||||
|
||||
> *Like DeepWiki, but deeper.* 😉
|
||||
|
||||
DeepWiki helps you *understand* code. GitNexus lets you *analyze* it—because a knowledge graph tracks every dependency, call chain, and relationship.
|
||||
|
||||
That's the difference between:
|
||||
- "What does this function do?" → *understanding*
|
||||
- "What breaks if I change this function?" → *analysis*
|
||||
|
||||
**Core Innovation: Precomputed Relational Intelligence**
|
||||
|
||||
Most AI coding tools give the LLM raw data and hope it figures out relationships. GitNexus **precomputes structure at index time**—clustering related code, tracing execution flows, scoring edge confidence—so tools return *decision-ready context*. This means:
|
||||
- 🎯 **Reliability**: LLM can't miss context—it's already in the tool response
|
||||
- ⚡ **Token efficiency**: No 10-query chains to understand one function
|
||||
- 🤖 **Model democratization**: Smaller LLMs work because tools do the heavy lifting
|
||||
|
||||
**Quick tech jargon:**
|
||||
- **Smart Tools**: 7 graph-aware tools with built-in cluster/process context
|
||||
- **Leiden Clustering**: Automatic detection of functional code communities
|
||||
- **Process Detection**: Entry point tracing via BFS with framework-aware scoring
|
||||
- **Confidence Scoring**: Every CALLS edge rated 0-1 (import-resolved vs fuzzy guess)
|
||||
- **Hybrid Search**: BM25 + Semantic + 1-hop graph expansion via Cypher
|
||||
- **Full WASM Stack**: Tree-sitter parsing + KuzuDB graph database, all in-browser
|
||||
- **9 Languages**: TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust
|
||||
|
||||
**What you can do:**
|
||||
|
||||
| Capability | Description |
|
||||
|------------|-------------|
|
||||
| **Codebase-wide audits** | Find layer violations, forbidden dependencies |
|
||||
| **Blast radius analysis** | See every function affected by a change (with confidence) |
|
||||
| **Dead code detection** | Identify orphaned nodes with zero incoming calls |
|
||||
| **Dependency tracing** | Follow import chains across the entire codebase |
|
||||
| **Process exploration** | Trace execution flows from API handlers to data layer |
|
||||
| **Cluster navigation** | Explore code by functional area, not just file structure |
|
||||
| **AI analyses with citations** | Ask questions, analyze, get answers with `[[file:line]]` proof |
|
||||
|
||||
**100% client-side.** Your code never leaves your browser.
|
||||
|
||||
<img width="2550" height="1343" alt="gitnexus_img" src="https://github.com/user-attachments/assets/cc5d637d-e0e5-48e6-93ff-5bcfdb929285" />
|
||||
**TL;DR:** The **Web UI** is a quick way to chat with any repo. The **CLI + MCP** is how you make your AI agent actually reliable — it gives Cursor, Claude Code, and friends a deep architectural view of your codebase so they stop missing dependencies, breaking call chains, and shipping blind edits. Even smaller models get full architectural clarity, making it compete with goliath models.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 The Problem with AI Coding Tools
|
||||
## Two Ways to Use GitNexus
|
||||
|
||||
Tools like **Cursor**, **Claude Code**, **Cline**, **Roo Code**, and **Windsurf** are powerful—but they share a fundamental limitation: **they don't truly know your codebase structure**.
|
||||
| | **CLI + MCP** | **Web UI** |
|
||||
|---|---|---|
|
||||
| **What** | Index repos locally, connect AI agents via MCP | Visual graph explorer + AI chat in browser |
|
||||
| **For** | Daily development with Cursor, Claude Code, Windsurf, OpenCode | Quick exploration, demos, one-off analysis |
|
||||
| **Scale** | Full repos, any size | Limited by browser memory (~5k files) |
|
||||
| **Install** | `npm install -g gitnexus` | No install — [gitnexus.vercel.app](https://gitnexus.vercel.app) |
|
||||
| **Storage** | KuzuDB native (fast, persistent) | KuzuDB WASM (in-memory, per session) |
|
||||
| **Parsing** | Tree-sitter native bindings | Tree-sitter WASM |
|
||||
| **Privacy** | Everything local, no network | Everything in-browser, no server |
|
||||
|
||||
---
|
||||
|
||||
## CLI + MCP (recommended)
|
||||
|
||||
The CLI indexes your repository and runs an MCP server that gives AI agents deep codebase awareness.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Install
|
||||
npm install -g gitnexus
|
||||
|
||||
# One-time setup: configures MCP for Cursor, Claude Code, OpenCode
|
||||
gitnexus setup
|
||||
|
||||
# Index your repo (run from repo root)
|
||||
gitnexus analyze
|
||||
|
||||
# That's it! Open your editor — the MCP server starts automatically.
|
||||
```
|
||||
|
||||
Or without installing globally:
|
||||
|
||||
```bash
|
||||
npx gitnexus setup # one-time
|
||||
npx gitnexus analyze # per repo
|
||||
```
|
||||
|
||||
### MCP Setup
|
||||
|
||||
The `gitnexus setup` command auto-detects your editors and writes the correct MCP config. You only need to run it once.
|
||||
|
||||
If you prefer manual configuration:
|
||||
|
||||
**Cursor** (`~/.cursor/mcp.json` — global, works for all projects):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitnexus": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "gitnexus@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Claude Code:**
|
||||
|
||||
```bash
|
||||
claude mcp add gitnexus -- npx -y gitnexus@latest mcp
|
||||
```
|
||||
|
||||
**OpenCode** (`~/.config/opencode/config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"gitnexus": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "gitnexus@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
gitnexus setup # Configure MCP for your editors (one-time)
|
||||
gitnexus analyze [path] # Index a repository (or update stale index)
|
||||
gitnexus analyze --force # Force full re-index
|
||||
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
|
||||
gitnexus serve # Start HTTP server for web UI connection
|
||||
gitnexus list # List all indexed repositories
|
||||
gitnexus status # Show index status for current repo
|
||||
gitnexus clean # Delete index for current repo
|
||||
gitnexus clean --all # Delete all indexes
|
||||
```
|
||||
|
||||
### What Your AI Agent Gets
|
||||
|
||||
**7 tools** exposed via MCP:
|
||||
|
||||
| Tool | What It Does | `repo` Param |
|
||||
|------|-------------|--------------|
|
||||
| `list_repos` | Discover all indexed repositories | — |
|
||||
| `search` | Hybrid search (BM25 + semantic) with cluster context | Optional |
|
||||
| `overview` | List all clusters and processes | Optional |
|
||||
| `explore` | Deep dive on a symbol, cluster, or process | Optional |
|
||||
| `impact` | Blast radius analysis with confidence filtering | Optional |
|
||||
| `cypher` | Raw Cypher graph queries | Optional |
|
||||
| `analyze` | Index or re-index a repository | Optional |
|
||||
|
||||
> When only one repo is indexed, the `repo` parameter is optional. With multiple repos, specify which one: `search({query: "auth", repo: "my-app"})`.
|
||||
|
||||
**Resources** for instant context:
|
||||
|
||||
| Resource | Purpose |
|
||||
|----------|---------|
|
||||
| `gitnexus://repos` | List all indexed repositories (read this first) |
|
||||
| `gitnexus://repo/{name}/context` | Codebase stats and overview |
|
||||
| `gitnexus://repo/{name}/clusters` | All functional clusters with cohesion scores |
|
||||
| `gitnexus://repo/{name}/cluster/{name}` | Cluster members and details |
|
||||
| `gitnexus://repo/{name}/processes` | All execution flows |
|
||||
| `gitnexus://repo/{name}/process/{name}` | Full process trace with steps |
|
||||
| `gitnexus://repo/{name}/schema` | Graph schema for Cypher queries |
|
||||
|
||||
**4 agent skills** installed to `.claude/skills/` automatically:
|
||||
|
||||
- **Exploring** — Navigate unfamiliar code using the knowledge graph
|
||||
- **Debugging** — Trace bugs through call chains
|
||||
- **Impact Analysis** — Analyze blast radius before changes
|
||||
- **Refactoring** — Plan safe refactors using dependency mapping
|
||||
|
||||
---
|
||||
|
||||
## Multi-Repo MCP Architecture
|
||||
|
||||
GitNexus uses a **global registry** so one MCP server can serve multiple indexed repos. No per-project MCP config needed — set it up once and it works everywhere.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph CLI [CLI Commands]
|
||||
Setup["gitnexus setup"]
|
||||
Analyze["gitnexus analyze"]
|
||||
Clean["gitnexus clean"]
|
||||
List["gitnexus list"]
|
||||
end
|
||||
|
||||
subgraph Registry ["~/.gitnexus/"]
|
||||
RegFile["registry.json"]
|
||||
end
|
||||
|
||||
subgraph Repos [Project Repos]
|
||||
RepoA[".gitnexus/ in repo A"]
|
||||
RepoB[".gitnexus/ in repo B"]
|
||||
end
|
||||
|
||||
subgraph MCP [MCP Server]
|
||||
Server["server.ts"]
|
||||
Backend["LocalBackend"]
|
||||
Pool["Connection Pool"]
|
||||
ConnA["KuzuDB conn A"]
|
||||
ConnB["KuzuDB conn B"]
|
||||
end
|
||||
|
||||
Setup -->|"writes global MCP config"| CursorConfig["~/.cursor/mcp.json"]
|
||||
Analyze -->|"registers repo"| RegFile
|
||||
Analyze -->|"stores index"| RepoA
|
||||
Clean -->|"unregisters repo"| RegFile
|
||||
List -->|"reads"| RegFile
|
||||
Server -->|"reads registry"| RegFile
|
||||
Server --> Backend
|
||||
Backend --> Pool
|
||||
Pool -->|"lazy open"| ConnA
|
||||
Pool -->|"lazy open"| ConnB
|
||||
ConnA -->|"queries"| RepoA
|
||||
ConnB -->|"queries"| RepoB
|
||||
```
|
||||
|
||||
**How it works:** Each `gitnexus analyze` stores the index in `.gitnexus/` inside the repo (portable, gitignored) and registers a pointer in `~/.gitnexus/registry.json`. When an AI agent starts, the MCP server reads the registry and can serve any indexed repo. KuzuDB connections are opened lazily on first query and evicted after 5 minutes of inactivity (max 5 concurrent). If only one repo is indexed, the `repo` parameter is optional on all tools — agents don't need to change anything.
|
||||
|
||||
---
|
||||
|
||||
## Web UI (browser-based)
|
||||
|
||||
A fully client-side graph explorer and AI chat. No server, no install — your code never leaves the browser.
|
||||
|
||||
**Try it now:** [gitnexus.vercel.app](https://gitnexus.vercel.app) — drag & drop a ZIP and start exploring.
|
||||
|
||||
<img width="2550" height="1343" alt="gitnexus_img" src="https://github.com/user-attachments/assets/cc5d637d-e0e5-48e6-93ff-5bcfdb929285" />
|
||||
|
||||
Or run locally:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/abhigyanpatwari/gitnexus.git
|
||||
cd gitnexus/gitnexus-web
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAssembly (Tree-sitter WASM, KuzuDB WASM, in-browser embeddings). It's great for quick exploration but limited by browser memory for larger repos.
|
||||
|
||||
---
|
||||
|
||||
## The Problem GitNexus Solves
|
||||
|
||||
Tools like **Cursor**, **Claude Code**, **Cline**, **Roo Code**, and **Windsurf** are powerful — but they don't truly know your codebase structure.
|
||||
|
||||
**What happens:**
|
||||
1. AI edits `UserService.validate()`
|
||||
1. AI edits `UserService.validate()`
|
||||
2. Doesn't know 47 functions depend on its return type
|
||||
3. **Breaking changes ship** 💥
|
||||
3. **Breaking changes ship**
|
||||
|
||||
### The Solution: Precomputed Graph Intelligence
|
||||
### Traditional Graph RAG vs GitNexus
|
||||
|
||||
Traditional Graph RAG gives the LLM raw edges and hopes it explores enough. GitNexus precomputes structure so tools return complete context in one call:
|
||||
Traditional approaches give the LLM raw graph edges and hope it explores enough. GitNexus **precomputes structure at index time** — clustering, tracing, scoring — so tools return complete context in one call:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Traditional["❌ Traditional Graph RAG"]
|
||||
subgraph Traditional["Traditional Graph RAG"]
|
||||
direction TB
|
||||
U1["User: What depends on UserService?"]
|
||||
U1 --> LLM1["LLM receives raw graph"]
|
||||
LLM1 --> Q1["Query 1: Find callers"]
|
||||
Q1 --> R1["47 node IDs returned"]
|
||||
R1 --> Q2["Query 2: What files are these?"]
|
||||
Q2 --> R2["12 file paths"]
|
||||
R2 --> Q3["Query 3: Filter out tests?"]
|
||||
Q3 --> R3["8 production files"]
|
||||
R3 --> Q4["Query 4: Which are high-risk?"]
|
||||
Q4 --> THINK["LLM interprets..."]
|
||||
THINK --> OUT1["Answer after 4+ queries"]
|
||||
Q1 --> Q2["Query 2: What files?"]
|
||||
Q2 --> Q3["Query 3: Filter tests?"]
|
||||
Q3 --> Q4["Query 4: High-risk?"]
|
||||
Q4 --> OUT1["Answer after 4+ queries"]
|
||||
end
|
||||
|
||||
subgraph GitNexus["✅ GitNexus Smart Tools"]
|
||||
subgraph GN["GitNexus Smart Tools"]
|
||||
direction TB
|
||||
U2["User: What depends on UserService?"]
|
||||
U2 --> TOOL["impact UserService upstream"]
|
||||
TOOL --> PRECOMP["Pre-structured response:
|
||||
• 8 production callers
|
||||
• Grouped: Auth 3, Payment 2, API 3
|
||||
• All 90%+ confidence
|
||||
• 5 in LoginFlow process"]
|
||||
8 callers, 3 clusters, all 90%+ confidence"]
|
||||
PRECOMP --> OUT2["Complete answer, 1 query"]
|
||||
end
|
||||
```
|
||||
|
||||
**Current state:** GitNexus is a standalone tool—a better DeepWiki that's 100% client-side with graph-powered analysis.
|
||||
**Core innovation: Precomputed Relational Intelligence**
|
||||
|
||||
**MCP Integration:** GitNexus also runs as an MCP server (`gitnexus-mcp`) so tools like Cursor and Claude Code can query it for accurate context.
|
||||
|
||||
git clone https://github.com/abhigyanpatwari/gitnexus.git
|
||||
cd gitnexus
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
Open http://localhost:5173, drag & drop a ZIP of your codebase, and start exploring.
|
||||
- **Reliability** — LLM can't miss context, it's already in the tool response
|
||||
- **Token efficiency** — No 10-query chains to understand one function
|
||||
- **Model democratization** — Smaller LLMs work because tools do the heavy lifting
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Indexing Architecture
|
||||
## How Indexing Works
|
||||
|
||||
Seven-phase indexing: **Structure** → **Parse** → **Imports** → **Calls** → **Heritage** → **Communities** → **Processes**.
|
||||
Seven-phase pipeline that builds a complete knowledge graph:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph P1["Phase 1: Extract (0-15%)"]
|
||||
E1[Decompress ZIP] --> E2[Collect file paths]
|
||||
subgraph P1["Phase 1: Structure (0-15%)"]
|
||||
S1[Walk file tree] --> S2[Create CONTAINS edges]
|
||||
end
|
||||
|
||||
subgraph P2["Phase 2: Structure (15-30%)"]
|
||||
S1[Build folder tree] --> S2[Create CONTAINS edges]
|
||||
end
|
||||
|
||||
subgraph P3["Phase 3: Parse (30-55%)"]
|
||||
PA1[Load Tree-sitter WASM] --> PA2[Generate ASTs]
|
||||
PA2 --> PA3[Extract symbols]
|
||||
|
||||
subgraph P2["Phase 2: Parse (15-40%)"]
|
||||
PA1[Load Tree-sitter parsers] --> PA2[Generate ASTs]
|
||||
PA2 --> PA3[Extract functions, classes, methods]
|
||||
PA3 --> PA4[Populate Symbol Table]
|
||||
end
|
||||
|
||||
subgraph P4["Phase 4: Imports (55-65%)"]
|
||||
I1[Find import statements] --> I2[Resolve paths]
|
||||
|
||||
subgraph P3["Phase 3: Imports (40-55%)"]
|
||||
I1[Find import statements] --> I2[Language-aware resolution]
|
||||
I2 --> I3[Create IMPORTS edges]
|
||||
end
|
||||
|
||||
subgraph P5["Phase 5: Calls + Heritage (65-80%)"]
|
||||
|
||||
subgraph P4["Phase 4: Calls + Heritage (55-75%)"]
|
||||
C1[Find function calls] --> C2[Resolve via Symbol Table]
|
||||
C2 --> C3[Create CALLS edges with confidence]
|
||||
C3 --> H1[Find extends/implements]
|
||||
H1 --> H2[Create EXTENDS/IMPLEMENTS edges]
|
||||
end
|
||||
|
||||
subgraph P6["Phase 6: Communities (80-90%)"]
|
||||
|
||||
subgraph P5["Phase 5: Communities (75-85%)"]
|
||||
CM1[Build CALLS graph] --> CM2[Run Leiden algorithm]
|
||||
CM2 --> CM3[Calculate cohesion scores]
|
||||
CM3 --> CM4[Generate heuristic labels]
|
||||
CM4 --> CM5[Create MEMBER_OF edges]
|
||||
end
|
||||
|
||||
subgraph P7["Phase 7: Processes (90-100%)"]
|
||||
|
||||
subgraph P6["Phase 6: Processes (85-95%)"]
|
||||
PR1[Score entry points] --> PR2[BFS trace via CALLS]
|
||||
PR2 --> PR3[Detect cross-community flows]
|
||||
PR3 --> PR4[Create STEP_IN_PROCESS edges]
|
||||
end
|
||||
|
||||
|
||||
subgraph P7["Phase 7: Embeddings (95-100%)"]
|
||||
EM1[Generate embeddings] --> EM2[Build HNSW vector index]
|
||||
EM2 --> EM3[Build BM25 full-text index]
|
||||
end
|
||||
|
||||
P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
|
||||
P7 --> DB[(KuzuDB WASM)]
|
||||
DB --> READY[Graph Ready!]
|
||||
P7 --> DB[(KuzuDB)]
|
||||
DB --> READY[Graph Ready]
|
||||
```
|
||||
|
||||
### Symbol Table: Dual HashMap
|
||||
### Supported Languages
|
||||
|
||||
Resolution strategy for function calls (produces **confidence scores**):
|
||||
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust
|
||||
|
||||
### Language-Aware Import Resolution
|
||||
|
||||
GitNexus doesn't just string-match import paths. It understands language-specific module systems:
|
||||
|
||||
| Language | What's Resolved |
|
||||
|----------|----------------|
|
||||
| **TypeScript** | Path aliases from `tsconfig.json` (e.g. `@/lib/auth` → `src/lib/auth`) |
|
||||
| **Rust** | Module paths (`crate::auth::validate`, `super::utils`, `self::handler`) |
|
||||
| **Java** | Wildcard imports (`com.example.*`) and static imports |
|
||||
| **Go** | Module paths via `go.mod`, internal package resolution |
|
||||
| **C/C++** | Relative includes, system include detection |
|
||||
|
||||
### Confidence Scoring on CALLS
|
||||
|
||||
Every function call edge includes a trust score:
|
||||
|
||||
| Confidence | Reason | Meaning |
|
||||
|------------|--------|---------|
|
||||
| 0.90 | `import-resolved` | Target found in imported file |
|
||||
| 0.85 | `same-file` | Target defined in same file |
|
||||
| 0.50 | `fuzzy-global` (1 match) | Single global match by name |
|
||||
| 0.30 | `fuzzy-global` (N matches) | Multiple matches, first picked |
|
||||
|
||||
The `impact` tool uses `minConfidence` to filter out guesses and return only reliable results.
|
||||
|
||||
### Symbol Table: Dual HashMap
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CALL["Found call: validateUser"] --> CHECK1{"In Import Map?"}
|
||||
CHECK1 -->|Yes| FOUND1["✅ Import-resolved (90%)"]
|
||||
CHECK1 -->|Yes| FOUND1["Import-resolved (90%)"]
|
||||
CHECK1 -->|No| CHECK2{"In Current File?"}
|
||||
CHECK2 -->|Yes| FOUND2["✅ Same-file (85%)"]
|
||||
CHECK2 -->|Yes| FOUND2["Same-file (85%)"]
|
||||
CHECK2 -->|No| CHECK3{"Global Search"}
|
||||
CHECK3 -->|1 match| FOUND3["⚠️ Fuzzy single (50%)"]
|
||||
CHECK3 -->|N matches| FOUND4["⚠️ Fuzzy multiple (30%)"]
|
||||
CHECK3 -->|1 match| FOUND3["Fuzzy single (50%)"]
|
||||
CHECK3 -->|N matches| FOUND4["Fuzzy multiple (30%)"]
|
||||
CHECK3 -->|Not Found| SKIP["Skip - unresolved"]
|
||||
|
||||
|
||||
FOUND1 & FOUND2 & FOUND3 & FOUND4 --> EDGE["Create CALLS edge with confidence"]
|
||||
```
|
||||
|
||||
### Community Detection (Leiden Algorithm)
|
||||
|
||||
Groups related code by analyzing CALLS edge density:
|
||||
Groups related code into functional clusters by analyzing CALLS edge density:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
|
@ -191,7 +369,7 @@ flowchart LR
|
|||
COHESION --> MEMBER["MEMBER_OF edges"]
|
||||
```
|
||||
|
||||
**Why it matters:** Instead of "this function is in `/src/auth/validate.ts`", the agent knows "this function is in the **Authentication** cluster with 23 other related symbols."
|
||||
Instead of "this function is in `/src/auth/validate.ts`", the agent knows "this function is in the **Authentication** cluster with 23 other related symbols."
|
||||
|
||||
### Process Detection (Entry Point Tracing)
|
||||
|
||||
|
|
@ -200,14 +378,14 @@ Finds execution flows by tracing from entry points:
|
|||
```mermaid
|
||||
flowchart TD
|
||||
FUNCS[All Functions/Methods] --> SCORE["Score entry point likelihood"]
|
||||
|
||||
|
||||
subgraph Scoring["Entry Point Scoring"]
|
||||
BASE["Call ratio: callees/(callers+1)"]
|
||||
EXPORT["× 2.0 if exported"]
|
||||
NAME["× 1.5 if handle*/on*/Controller"]
|
||||
FW["× 3.0 if in /routes/ or /handlers/"]
|
||||
EXPORT["x 2.0 if exported"]
|
||||
NAME["x 1.5 if handle*/on*/Controller"]
|
||||
FW["x 3.0 if in /routes/ or /handlers/"]
|
||||
end
|
||||
|
||||
|
||||
SCORE --> Scoring
|
||||
Scoring --> TOP["Top candidates"]
|
||||
TOP --> BFS["BFS trace via CALLS (max 10 hops)"]
|
||||
|
|
@ -222,40 +400,25 @@ flowchart TD
|
|||
- Spring: `/controllers/`, `*Controller.java`
|
||||
- And more for Go, Rust, C#...
|
||||
|
||||
### Background Embeddings
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph BG["Background (Non-blocking)"]
|
||||
M1[Load snowflake-arctic-embed-xs] --> M2[Initialize WebGPU/WASM]
|
||||
M2 --> E1[Batch embed nodes]
|
||||
E1 --> E2[INSERT into CodeEmbedding table]
|
||||
E2 --> V1[Create HNSW Vector Index]
|
||||
V1 --> B1[Build BM25 Index]
|
||||
end
|
||||
|
||||
BG --> AI[AI Search Ready!]
|
||||
```
|
||||
|
||||
User can explore the graph during embedding. AI features unlock when complete.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Graph Schema
|
||||
## Graph Schema
|
||||
|
||||
### Node Types
|
||||
|
||||
| Label | Description | Key Properties |
|
||||
|-------|-------------|----------------|
|
||||
| `Folder` | Directory | `name`, `filePath` |
|
||||
| `File` | Source file | `name`, `filePath`, `language` |
|
||||
| `File` | Source file | `name`, `filePath`, `language`, `content` |
|
||||
| `Function` | Function def | `name`, `filePath`, `startLine`, `endLine`, `isExported` |
|
||||
| `Class` | Class def | `name`, `filePath`, `startLine`, `endLine` |
|
||||
| `Interface` | Interface def | `name`, `filePath`, `startLine`, `endLine` |
|
||||
| `Method` | Class method | `name`, `filePath`, `startLine`, `endLine` |
|
||||
| `Community` | Functional cluster | `label`, `cohesion`, `symbolCount`, `description` |
|
||||
| `Class` | Class def | `name`, `filePath`, `startLine`, `endLine`, `isExported` |
|
||||
| `Interface` | Interface def | `name`, `filePath`, `startLine`, `endLine`, `isExported` |
|
||||
| `Method` | Class method | `name`, `filePath`, `startLine`, `endLine`, `isExported` |
|
||||
| `Community` | Functional cluster | `label`, `heuristicLabel`, `cohesion`, `symbolCount` |
|
||||
| `Process` | Execution flow | `label`, `processType`, `stepCount`, `entryPointId` |
|
||||
|
||||
Plus language-specific nodes: `Struct`, `Enum`, `Trait`, `Impl`, `TypeAlias`, `Namespace`, `Record`, `Delegate`, `Annotation`, `Constructor`, `Template`, `Module` and more.
|
||||
|
||||
### Relationship Table: `CodeRelation`
|
||||
|
||||
Single edge table with `type` property:
|
||||
|
|
@ -269,60 +432,74 @@ Single edge table with `type` property:
|
|||
| `EXTENDS` | Class | Class | — |
|
||||
| `IMPLEMENTS` | Class | Interface | — |
|
||||
| `MEMBER_OF` | Symbol | Community | — |
|
||||
| `STEP_IN_PROCESS` | Symbol | Process | `step` (1-indexed position) |
|
||||
|
||||
### Confidence Scores on CALLS
|
||||
|
||||
Every CALLS edge includes trust metadata:
|
||||
|
||||
| Confidence | Reason | Meaning |
|
||||
|------------|--------|---------|
|
||||
| 0.90 | `import-resolved` | Target found in imported file |
|
||||
| 0.85 | `same-file` | Target defined in same file |
|
||||
| 0.50 | `fuzzy-global` (1 match) | Single global match by name |
|
||||
| 0.30 | `fuzzy-global` (N matches) | Multiple matches, first picked |
|
||||
|
||||
**Why it matters:** The `impact` tool filters by `minConfidence` (default 0.7) to exclude guesses.
|
||||
| `STEP_IN_PROCESS` | Symbol | Process | `step` (1-indexed) |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Agent Tools Architecture
|
||||
## Tool Examples
|
||||
|
||||
The LangChain ReAct agent has **7 tools** for code exploration. These tools **use precomputed structure** (clusters, processes, confidence) to return rich context.
|
||||
### Impact Analysis
|
||||
|
||||
### Tool 1: `search` — Hybrid Search with Process Grouping
|
||||
```
|
||||
gitnexus_impact({target: "UserService", direction: "upstream", minConfidence: 0.8, repo: "my-app"})
|
||||
|
||||
Combines **BM25** (keyword) + **Semantic** (vector) + **1-hop expansion** + **process context**:
|
||||
TARGET: Class UserService (src/services/user.ts)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Q["Query: auth middleware"] --> HYBRID["Hybrid Search (BM25 + Semantic)"]
|
||||
HYBRID --> RRF["Reciprocal Rank Fusion"]
|
||||
RRF --> TOP["Top K Results"]
|
||||
|
||||
TOP --> ENRICH["For each result:"]
|
||||
ENRICH --> HOP["1-hop connections + confidence"]
|
||||
ENRICH --> CLUSTER["Cluster membership"]
|
||||
ENRICH --> PROC["Process participation"]
|
||||
|
||||
HOP & CLUSTER & PROC --> GROUP["Group by process"]
|
||||
GROUP --> OUT["Structured output:
|
||||
PROCESS: LoginFlow (3 matches)
|
||||
[1] Function: validateUser (step 2/7)
|
||||
Cluster: Authentication
|
||||
Connections: ←[CALLS 90%] handleLogin"]
|
||||
UPSTREAM (what depends on this):
|
||||
Depth 1 (direct callers):
|
||||
handleLogin [CALLS 90%] → src/api/auth.ts:45
|
||||
handleRegister [CALLS 90%] → src/api/auth.ts:78
|
||||
UserController [CALLS 85%] → src/controllers/user.ts:12
|
||||
Depth 2:
|
||||
authRouter [IMPORTS] → src/routes/auth.ts
|
||||
|
||||
8 files affected, 3 clusters touched
|
||||
```
|
||||
|
||||
Each result includes not just *what matches*, but *where it fits* in the codebase structure.
|
||||
Options: `maxDepth`, `minConfidence`, `relationTypes` (`CALLS`, `IMPORTS`, `EXTENDS`, `IMPLEMENTS`), `includeTests`
|
||||
|
||||
---
|
||||
### Search with Cluster Context
|
||||
|
||||
### Tool 2: `cypher` — Raw Graph Queries
|
||||
```
|
||||
gitnexus_search({query: "authentication middleware", repo: "my-app"})
|
||||
|
||||
Execute Cypher directly. Supports `{{QUERY_VECTOR}}` auto-embedding:
|
||||
Results:
|
||||
[1] Function: validateUser
|
||||
File: src/auth/validate.ts
|
||||
Cluster: Authentication
|
||||
Score: 0.042 (hybrid: bm25 + semantic)
|
||||
[2] Function: checkSession
|
||||
File: src/auth/session.ts
|
||||
Cluster: Authentication
|
||||
Score: 0.038
|
||||
```
|
||||
|
||||
Each result includes the functional cluster it belongs to and optional relationship connections with `depth=full`.
|
||||
|
||||
### Explore a Process
|
||||
|
||||
```
|
||||
gitnexus_explore({name: "LoginFlow", type: "process", repo: "my-app"})
|
||||
|
||||
PROCESS: LoginFlow
|
||||
Type: cross_community | Steps: 7
|
||||
|
||||
TRACE:
|
||||
1. handleLogin (API)
|
||||
2. validateUser (Authentication)
|
||||
3. checkRateLimit (RateLimiting)
|
||||
4. hashPassword (Authentication)
|
||||
5. createSession (Authentication)
|
||||
6. storeSession (Database)
|
||||
7. generateToken (Authentication)
|
||||
|
||||
CLUSTERS TOUCHED: API, Authentication, RateLimiting, Database
|
||||
```
|
||||
|
||||
### Cypher Queries
|
||||
|
||||
```cypher
|
||||
-- Find what calls auth functions in the Authentication cluster
|
||||
-- Find what calls auth functions with high confidence
|
||||
MATCH (c:Community {label: 'Authentication'})<-[:CodeRelation {type: 'MEMBER_OF'}]-(fn)
|
||||
MATCH (caller)-[r:CodeRelation {type: 'CALLS'}]->(fn)
|
||||
WHERE r.confidence > 0.8
|
||||
|
|
@ -332,216 +509,63 @@ ORDER BY r.confidence DESC
|
|||
|
||||
---
|
||||
|
||||
### Tool 3: `grep` — Regex Pattern Matching
|
||||
## Tech Stack
|
||||
|
||||
For exact strings, error codes, TODOs:
|
||||
|
||||
```
|
||||
grep TODO|FIXME --fileFilter=.ts
|
||||
→ src/auth/validate.ts:42: // TODO: Add rate limiting
|
||||
```
|
||||
| Layer | CLI | Web |
|
||||
|-------|-----|-----|
|
||||
| **Runtime** | Node.js (native) | Browser (WASM) |
|
||||
| **Parsing** | Tree-sitter native bindings | Tree-sitter WASM |
|
||||
| **Database** | KuzuDB native | KuzuDB WASM |
|
||||
| **Embeddings** | HuggingFace transformers | transformers.js (WebGPU/WASM) |
|
||||
| **Search** | BM25 + semantic + RRF | BM25 + semantic + RRF |
|
||||
| **Agent Interface** | MCP (stdio) | LangChain ReAct agent |
|
||||
| **Visualization** | — | Sigma.js + Graphology (WebGL) |
|
||||
| **Frontend** | — | React 18, TypeScript, Vite, Tailwind v4 |
|
||||
| **Clustering** | Graphology + Leiden | Graphology + Leiden |
|
||||
| **Concurrency** | Node.js async | Web Workers + Comlink |
|
||||
|
||||
---
|
||||
|
||||
### Tool 4: `read` — Smart File Reader
|
||||
|
||||
Fuzzy path matching with suggestions if not found.
|
||||
|
||||
---
|
||||
|
||||
### Tool 5: `overview` — Codebase Map
|
||||
|
||||
Returns the full structural overview in one call:
|
||||
|
||||
```
|
||||
CLUSTERS (12 total):
|
||||
| Cluster | Symbols | Cohesion | Description |
|
||||
| Authentication| 23 | 0.82 | Login, session, JWT handling |
|
||||
| Database | 18 | 0.76 | Query builders, connection pool |
|
||||
...
|
||||
|
||||
PROCESSES (8 total):
|
||||
| Process | Steps | Type | Clusters |
|
||||
| LoginFlow | 7 | cross_community | 3 |
|
||||
| PaymentProcessing | 5 | intra_community | 1 |
|
||||
...
|
||||
|
||||
CLUSTER DEPENDENCIES:
|
||||
- Authentication -> Database (12 calls)
|
||||
- API -> Authentication (8 calls)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Tool 6: `explore` — Deep Dive
|
||||
|
||||
Accepts a **symbol**, **cluster**, or **process** name and returns detailed info:
|
||||
|
||||
**For a symbol:**
|
||||
```
|
||||
SYMBOL: Function validateUser
|
||||
File: src/auth/validate.ts
|
||||
Cluster: Authentication — Login and session management
|
||||
|
||||
PROCESSES:
|
||||
- LoginFlow (step 2/7)
|
||||
- SessionRefresh (step 1/4)
|
||||
|
||||
CONNECTIONS:
|
||||
-[CALLS 90%]-> hashPassword
|
||||
-[CALLS 85%]-> checkRateLimit
|
||||
<-[CALLS 90%]- handleLogin
|
||||
<-[CALLS 85%]- refreshSession
|
||||
```
|
||||
|
||||
**For a process:**
|
||||
```
|
||||
PROCESS: LoginFlow
|
||||
Type: cross_community
|
||||
Steps: 7
|
||||
|
||||
TRACE:
|
||||
1. handleLogin (API)
|
||||
2. validateUser (Authentication)
|
||||
3. checkRateLimit (RateLimiting)
|
||||
4. hashPassword (Authentication)
|
||||
5. createSession (Authentication)
|
||||
6. storeSession (Database)
|
||||
7. generateToken (Authentication)
|
||||
|
||||
CLUSTERS TOUCHED: API, Authentication, RateLimiting, Database
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Tool 7: `impact` — Blast Radius Analysis
|
||||
|
||||
Answers "what breaks if I change X?" or "what does X depend on?":
|
||||
|
||||
```
|
||||
impact UserService upstream --maxDepth=3 --minConfidence=0.8
|
||||
|
||||
TARGET: Class UserService (src/services/user.ts)
|
||||
|
||||
UPSTREAM (what depends on this):
|
||||
Depth 1 (direct callers):
|
||||
• handleLogin [CALLS 90%] → src/api/auth.ts:45
|
||||
• handleRegister [CALLS 90%] → src/api/auth.ts:78
|
||||
• UserController [CALLS 85%] → src/controllers/user.ts:12
|
||||
|
||||
Depth 2:
|
||||
• authRouter [IMPORTS] → src/routes/auth.ts
|
||||
• (3 more...)
|
||||
|
||||
Summary: 8 production files affected, 3 clusters touched
|
||||
```
|
||||
|
||||
**Key features:**
|
||||
- `upstream` = what calls this (breakage risk)
|
||||
- `downstream` = what this depends on
|
||||
- `minConfidence` = filter out fuzzy matches (default 0.7)
|
||||
- `includeTests` = false by default
|
||||
|
||||
---
|
||||
|
||||
## 💡 Key Discovery: Unified Vector + Graph
|
||||
|
||||
KuzuDB supports **native vector indexing (HNSW)**, so we do semantic + graph in **one Cypher query**:
|
||||
|
||||
```cypher
|
||||
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', $queryVector, 20)
|
||||
YIELD node AS emb, distance
|
||||
WITH emb, distance WHERE distance < 0.4
|
||||
MATCH (n:Function {id: emb.nodeId})<-[:CodeRelation {type: 'CALLS'}]-(caller)
|
||||
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
RETURN n.name, caller.name, c.label, distance
|
||||
ORDER BY distance
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- 🎯 **Single query execution** — No round-trips between systems
|
||||
- 📊 **Built-in relevance ranking** — Distance IS the score
|
||||
- ⚡ **No separate vector DB** — One database, one query language
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Technical Improvements
|
||||
|
||||
### Sigma.js + WebGL
|
||||
- V1: D3.js, choked at ~3k nodes
|
||||
- V2: Sigma.js + GPU rendering, smooth at 10k+
|
||||
|
||||
### Dual HashMap Symbol Table
|
||||
- V1: Trie (prefix tree) - clever but slow
|
||||
- V2: File-scoped + Global hashmaps - **~2x speedup**
|
||||
|
||||
### LRU AST Cache
|
||||
- Tree-sitter ASTs live in WASM memory
|
||||
- LRU cache (50 slots) with `tree.delete()` for cleanup
|
||||
|
||||
### ForceAtlas2 in Web Worker
|
||||
- Layout algorithm runs off main thread
|
||||
- UI stays responsive during graph positioning
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Roadmap
|
||||
## Roadmap
|
||||
|
||||
### Actively Building
|
||||
|
||||
- [ ] **LLM Cluster Enrichment** - Semantic names via LLM API
|
||||
- [ ] **AST Decorator Detection** - Parse @Controller, @Get, etc.
|
||||
- [ ] **Multi-Repo Support** - Analyze multiple repos together
|
||||
- [ ] **External Neo4j Connection** - Use hosted graph DB
|
||||
- [ ] **LLM Cluster Enrichment** — Semantic cluster names via LLM API
|
||||
- [ ] **AST Decorator Detection** — Parse @Controller, @Get, etc.
|
||||
- [ ] **Diff-Aware Impact** — Show blast radius for uncommitted changes
|
||||
|
||||
### Recently Completed ✅
|
||||
### Recently Completed
|
||||
|
||||
- [x] **MCP Support** - `gitnexus-mcp` package for tool integration
|
||||
- [x] **Community Detection** - Leiden algorithm for functional clustering
|
||||
- [x] **Process Detection** - Entry point tracing with framework awareness
|
||||
- [x] **9 Language Support** - Java, C, C++, C#, Go, Rust added
|
||||
- [x] **Confidence Scoring** - Trust levels on CALLS edges
|
||||
- [x] **7 Smart Tools** - overview, explore, impact added
|
||||
- [x] **Ollama Support** - Local LLM integration
|
||||
- [x] **Blast Radius Tool** - `impact` for dependency analysis
|
||||
- [x] Graph RAG Agent with streaming
|
||||
- [x] Browser embeddings (snowflake-arctic-embed-xs, 22M params)
|
||||
- [x] Vector index with HNSW in KuzuDB
|
||||
- [x] Hybrid search (BM25 + semantic + RRF)
|
||||
- [x] Grounded citations (`[[file:line]]` format)
|
||||
- [x] Multiple LLM providers (OpenAI, Azure, Gemini, Anthropic, Ollama)
|
||||
- [x] **Multi-Repo MCP** — Global registry + lazy connection pool, one MCP server for all repos
|
||||
- [x] **Zero-Config Setup** — `gitnexus setup` auto-configures Cursor, Claude Code, OpenCode
|
||||
- [x] **Unified CLI + MCP** — `npm install -g gitnexus` for indexing and MCP server
|
||||
- [x] **Language-Aware Imports** — TS path aliases, Rust modules, Java wildcards, Go packages
|
||||
- [x] **Community Detection** — Leiden algorithm for functional clustering
|
||||
- [x] **Process Detection** — Entry point tracing with framework awareness
|
||||
- [x] **9 Language Support** — TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust
|
||||
- [x] **Confidence Scoring** — Trust levels on CALLS edges (0.3-0.9)
|
||||
- [x] **Blast Radius Tool** — `impact` with minConfidence, relationTypes, includeTests
|
||||
- [x] **Hybrid Search** — BM25 + semantic + Reciprocal Rank Fusion + cluster context
|
||||
- [x] **Browser Embeddings** — snowflake-arctic-embed-xs (22M params)
|
||||
- [x] **Vector Index** — HNSW in KuzuDB for semantic search
|
||||
- [x] **Grounded Citations** — `[[file:line]]` format in AI responses
|
||||
- [x] **Multiple LLM Providers** — OpenAI, Azure, Gemini, Anthropic, Ollama
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Tech Stack
|
||||
## Security & Privacy
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| **Frontend** | React 18, TypeScript, Vite, Tailwind v4 |
|
||||
| **Visualization** | Sigma.js, Graphology, ForceAtlas2 (WebGL) |
|
||||
| **Parsing** | Tree-sitter WASM (9 languages) |
|
||||
| **Database** | KuzuDB WASM (graph + vector HNSW) |
|
||||
| **Clustering** | Graphology + Leiden algorithm |
|
||||
| **Embeddings** | transformers.js, snowflake-arctic-embed-xs (22M) |
|
||||
| **AI** | LangChain ReAct agent, streaming |
|
||||
| **Concurrency** | Web Workers + Comlink |
|
||||
- **CLI**: Everything runs locally on your machine. No network calls. Index stored in `.gitnexus/` (gitignored). Global registry at `~/.gitnexus/` stores only paths and metadata.
|
||||
- **Web**: Everything runs in your browser. No code uploaded to any server. API keys stored in localStorage only.
|
||||
- Open source — audit the code yourself.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security & Privacy
|
||||
## Acknowledgments
|
||||
|
||||
- All processing happens in your browser
|
||||
- No code uploaded to any server
|
||||
- API keys stored in localStorage only
|
||||
- Open source—audit the code yourself
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- [Tree-sitter](https://tree-sitter.github.io/) - AST parsing
|
||||
- [KuzuDB](https://kuzudb.com/) - Embedded graph database with vector support
|
||||
- [Sigma.js](https://www.sigmajs.org/) - WebGL graph rendering
|
||||
- [transformers.js](https://huggingface.co/docs/transformers.js) - Browser ML
|
||||
- [LangChain](https://langchain.com/) - Agent orchestration
|
||||
- [Graphology](https://graphology.github.io/) - Graph data structures + Leiden
|
||||
- [Tree-sitter](https://tree-sitter.github.io/) — AST parsing
|
||||
- [KuzuDB](https://kuzudb.com/) — Embedded graph database with vector support
|
||||
- [Sigma.js](https://www.sigmajs.org/) — WebGL graph rendering
|
||||
- [transformers.js](https://huggingface.co/docs/transformers.js) — Browser ML
|
||||
- [Graphology](https://graphology.github.io/) — Graph data structures + Leiden
|
||||
- [MCP](https://modelcontextprotocol.io/) — Model Context Protocol
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Heart } from 'lucide-react';
|
||||
import { useAppState } from '../hooks/useAppState';
|
||||
|
||||
export const StatusBar = () => {
|
||||
|
|
@ -44,6 +45,20 @@ export const StatusBar = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Center - Sponsor */}
|
||||
<a
|
||||
href="https://github.com/sponsors/abhigyanpatwari"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center gap-2 px-3 py-1 rounded-full bg-pink-500/10 border border-pink-500/20 hover:bg-pink-500/20 hover:border-pink-500/40 hover:scale-[1.02] transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<Heart className="w-3.5 h-3.5 text-pink-500 fill-pink-500/40 group-hover:fill-pink-500 group-hover:scale-110 transition-all duration-200 animate-pulse" />
|
||||
<span className="text-[11px] font-medium text-pink-400 group-hover:text-pink-300 transition-colors">Sponsor</span>
|
||||
<span className="text-[10px] text-pink-300/50 group-hover:text-pink-300/80 italic hidden md:inline transition-colors">
|
||||
need to buy some API credits to run SWE-bench 😅
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{/* Right - Stats */}
|
||||
<div className="flex items-center gap-3">
|
||||
{graph && (
|
||||
|
|
|
|||
17
gitnexus/.npmignore
Normal file
17
gitnexus/.npmignore
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Source (dist/ is the compiled output)
|
||||
src/
|
||||
tsconfig.json
|
||||
|
||||
# Dev files
|
||||
*.ts
|
||||
!dist/**/*.d.ts
|
||||
.git/
|
||||
.gitignore
|
||||
node_modules/
|
||||
|
||||
# Package lock (consumers use their own)
|
||||
package-lock.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
196
gitnexus/README.md
Normal file
196
gitnexus/README.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# GitNexus
|
||||
|
||||
**Graph-powered code intelligence for AI agents.** Index any codebase into a knowledge graph, then query it via MCP or CLI.
|
||||
|
||||
Works with **Cursor**, **Claude Code**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
|
||||
|
||||
[](https://www.npmjs.com/package/gitnexus)
|
||||
[](https://polyformproject.org/licenses/noncommercial/1.0.0/)
|
||||
|
||||
---
|
||||
|
||||
## Why?
|
||||
|
||||
AI coding tools don't understand your codebase structure. They edit a function without knowing 47 other functions depend on it. GitNexus fixes this by **precomputing every dependency, call chain, and relationship** into a queryable graph.
|
||||
|
||||
**Three commands to give your AI agent full codebase awareness.**
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install
|
||||
npm install -g gitnexus
|
||||
|
||||
# One-time: configure MCP for your editors
|
||||
gitnexus setup
|
||||
|
||||
# Index your repository (run from repo root)
|
||||
gitnexus analyze
|
||||
|
||||
# Done! Open your editor — MCP connects automatically.
|
||||
```
|
||||
|
||||
Or without installing globally:
|
||||
|
||||
```bash
|
||||
npx gitnexus setup # one-time
|
||||
npx gitnexus analyze # per repo
|
||||
```
|
||||
|
||||
The `setup` command auto-detects Cursor, Claude Code, and OpenCode, then writes the correct global MCP config. You only run it once.
|
||||
|
||||
## MCP Setup (manual)
|
||||
|
||||
If you prefer to configure manually instead of using `gitnexus setup`:
|
||||
|
||||
### Cursor / Windsurf
|
||||
|
||||
Add to `~/.cursor/mcp.json` (global — works for all projects):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitnexus": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "gitnexus@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Code
|
||||
|
||||
```bash
|
||||
claude mcp add gitnexus -- npx -y gitnexus@latest mcp
|
||||
```
|
||||
|
||||
### OpenCode
|
||||
|
||||
Add to `~/.config/opencode/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"gitnexus": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "gitnexus@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
GitNexus indexes your codebase through 7 phases:
|
||||
|
||||
1. **Structure** — File/folder tree
|
||||
2. **Parse** — AST extraction via Tree-sitter (9 languages)
|
||||
3. **Imports** — Resolve import paths (including TS path aliases, Rust modules, Java wildcards, Go packages)
|
||||
4. **Calls** — Function call resolution with confidence scoring (0.3-0.9)
|
||||
5. **Heritage** — Class extends/implements chains
|
||||
6. **Communities** — Leiden algorithm clusters related code into functional groups
|
||||
7. **Processes** — Entry point detection and execution flow tracing
|
||||
|
||||
The result is a **KuzuDB graph database** stored locally in `.gitnexus/` with full-text search and semantic embeddings.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
Your AI agent gets these tools automatically:
|
||||
|
||||
| Tool | What It Does | `repo` Param |
|
||||
|------|-------------|--------------|
|
||||
| `list_repos` | Discover all indexed repositories | — |
|
||||
| `search` | Hybrid search (BM25 + semantic) with cluster context | Optional |
|
||||
| `overview` | List all clusters and processes | Optional |
|
||||
| `explore` | Deep dive on a symbol, cluster, or process | Optional |
|
||||
| `impact` | Blast radius analysis | Optional |
|
||||
| `cypher` | Raw Cypher graph queries | Optional |
|
||||
| `analyze` | Index or re-index a repository | Optional |
|
||||
|
||||
> With one indexed repo, the `repo` param is optional. With multiple, specify which: `search({query: "auth", repo: "my-app"})`.
|
||||
|
||||
## MCP Resources
|
||||
|
||||
| Resource | Purpose |
|
||||
|----------|---------|
|
||||
| `gitnexus://repos` | List all indexed repositories (read first) |
|
||||
| `gitnexus://repo/{name}/context` | Codebase stats and overview |
|
||||
| `gitnexus://repo/{name}/clusters` | All functional clusters |
|
||||
| `gitnexus://repo/{name}/cluster/{name}` | Cluster members and details |
|
||||
| `gitnexus://repo/{name}/processes` | All execution flows |
|
||||
| `gitnexus://repo/{name}/process/{name}` | Full process trace |
|
||||
| `gitnexus://repo/{name}/schema` | Graph schema for Cypher queries |
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
gitnexus setup # Configure MCP for your editors (one-time)
|
||||
gitnexus analyze [path] # Index a repository (or update stale index)
|
||||
gitnexus analyze --force # Force full re-index
|
||||
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
|
||||
gitnexus serve # Start HTTP server for web UI
|
||||
gitnexus list # List all indexed repositories
|
||||
gitnexus status # Show index status for current repo
|
||||
gitnexus clean # Delete index for current repo
|
||||
gitnexus clean --all # Delete all indexes
|
||||
```
|
||||
|
||||
## Multi-Repo Support
|
||||
|
||||
GitNexus supports indexing multiple repositories. Each `gitnexus analyze` registers the repo in a global registry (`~/.gitnexus/registry.json`). The MCP server serves all indexed repos automatically with lazy KuzuDB connections.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust
|
||||
|
||||
## How Impact Analysis Works
|
||||
|
||||
```
|
||||
gitnexus_impact({target: "UserService", direction: "upstream", repo: "my-app"})
|
||||
|
||||
TARGET: Class UserService (src/services/user.ts)
|
||||
|
||||
UPSTREAM (what depends on this):
|
||||
Depth 1 (direct callers):
|
||||
handleLogin [CALLS 90%] → src/api/auth.ts:45
|
||||
handleRegister [CALLS 90%] → src/api/auth.ts:78
|
||||
Depth 2:
|
||||
authRouter [IMPORTS] → src/routes/auth.ts
|
||||
|
||||
8 files affected, 3 clusters touched
|
||||
```
|
||||
|
||||
Options: `maxDepth`, `minConfidence`, `relationTypes`, `includeTests`
|
||||
|
||||
## Agent Skills
|
||||
|
||||
GitNexus ships with skill files that teach AI agents how to use the tools effectively:
|
||||
|
||||
- **Exploring** — Navigate unfamiliar code using the knowledge graph
|
||||
- **Debugging** — Trace bugs through call chains
|
||||
- **Impact Analysis** — Analyze blast radius before changes
|
||||
- **Refactoring** — Plan safe refactors using dependency mapping
|
||||
|
||||
These are installed automatically to `.claude/skills/` when you run `gitnexus analyze`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js >= 18
|
||||
- Git repository (uses git for commit tracking)
|
||||
|
||||
## Privacy
|
||||
|
||||
- All processing happens locally on your machine
|
||||
- No code is sent to any server
|
||||
- Index stored in `.gitnexus/` inside your repo (gitignored)
|
||||
- Global registry at `~/.gitnexus/` stores only paths and metadata
|
||||
|
||||
## Web UI
|
||||
|
||||
GitNexus also has a browser-based UI at [gitnexus.vercel.app](https://gitnexus.vercel.app) — 100% client-side, your code never leaves the browser.
|
||||
|
||||
## License
|
||||
|
||||
[PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/)
|
||||
|
||||
Free for non-commercial use. Contact for commercial licensing.
|
||||
|
|
@ -1,12 +1,33 @@
|
|||
{
|
||||
"name": "gitnexus",
|
||||
"version": "0.1.0",
|
||||
"description": "GitNexus local CLI and MCP server",
|
||||
"version": "1.1.6",
|
||||
"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",
|
||||
"homepage": "https://github.com/abhigyanpatwari/GitNexus#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/abhigyanpatwari/GitNexus.git",
|
||||
"directory": "gitnexus"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/abhigyanpatwari/GitNexus/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"code-intelligence",
|
||||
"knowledge-graph",
|
||||
"cursor",
|
||||
"claude",
|
||||
"ai-agent",
|
||||
"gitnexus",
|
||||
"static-analysis",
|
||||
"codebase-indexing"
|
||||
],
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"gitnexus": "./dist/cli/index.js"
|
||||
"gitnexus": "dist/cli/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ description: Trace bugs through call chains using knowledge graph
|
|||
|
||||
## Quick Start
|
||||
```
|
||||
0. If "Index is stale" → gitnexus_analyze({})
|
||||
1. gitnexus_search({query}) → Find code related to error
|
||||
2. gitnexus_explore({name, type: "symbol"}) → Get callers and callees
|
||||
3. READ gitnexus://process/{name} → Trace execution flow
|
||||
0. READ gitnexus://repos → Discover indexed repos
|
||||
1. If "Index is stale" → run `npx gitnexus analyze` in terminal
|
||||
2. gitnexus_search({query: "...", repo: "my-app"}) → Find code related to error
|
||||
3. gitnexus_explore({name, type: "symbol", repo: "my-app"}) → Get callers and callees
|
||||
4. READ gitnexus://repo/my-app/process/{name} → Trace execution flow
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
|
@ -22,18 +23,19 @@ description: Trace bugs through call chains using knowledge graph
|
|||
## Workflow Checklist
|
||||
```
|
||||
Bug Investigation:
|
||||
- [ ] READ gitnexus://repos to find the right repo
|
||||
- [ ] Understand the symptom (error message, behavior)
|
||||
- [ ] gitnexus_search to find related code
|
||||
- [ ] Identify the suspect function
|
||||
- [ ] gitnexus_explore to see callers/callees
|
||||
- [ ] READ gitnexus://process/{name} if suspect is in a process
|
||||
- [ ] READ gitnexus://schema for Cypher query help
|
||||
- [ ] READ gitnexus://repo/{name}/process/{name} if suspect is in a process
|
||||
- [ ] READ gitnexus://repo/{name}/schema for Cypher query help
|
||||
- [ ] gitnexus_cypher for custom traces
|
||||
```
|
||||
|
||||
## Resource Reference
|
||||
|
||||
### gitnexus://schema
|
||||
### gitnexus://repo/{name}/schema
|
||||
Graph schema for writing Cypher queries:
|
||||
```yaml
|
||||
nodes: [Function, Class, Method, File, Community, Process]
|
||||
|
|
@ -44,7 +46,7 @@ example_queries:
|
|||
RETURN caller.name
|
||||
```
|
||||
|
||||
### gitnexus://process/{name}
|
||||
### gitnexus://repo/{name}/process/{processName}
|
||||
Trace execution flow to find where bug might occur:
|
||||
```yaml
|
||||
name: CheckoutFlow
|
||||
|
|
@ -60,13 +62,13 @@ trace:
|
|||
### gitnexus_search
|
||||
Find code related to error or symptom:
|
||||
```
|
||||
gitnexus_search({query: "payment validation error", depth: "full"})
|
||||
gitnexus_search({query: "payment validation error", depth: "full", repo: "my-app"})
|
||||
```
|
||||
|
||||
### gitnexus_explore
|
||||
Get symbol context:
|
||||
```
|
||||
gitnexus_explore({name: "validatePayment", type: "symbol"})
|
||||
gitnexus_explore({name: "validatePayment", type: "symbol", repo: "my-app"})
|
||||
→ Callers: processCheckout, webhookHandler
|
||||
→ Callees: verifyCard, fetchRates
|
||||
```
|
||||
|
|
@ -82,13 +84,13 @@ RETURN [n IN nodes(path) | n.name] AS chain
|
|||
## Example: "Payment endpoint returns 500 intermittently"
|
||||
|
||||
```
|
||||
1. gitnexus_search({query: "payment error handling"})
|
||||
1. gitnexus_search({query: "payment error handling", repo: "my-app"})
|
||||
→ validatePayment, handlePaymentError, PaymentException
|
||||
|
||||
2. gitnexus_explore({name: "validatePayment", type: "symbol"})
|
||||
2. gitnexus_explore({name: "validatePayment", type: "symbol", repo: "my-app"})
|
||||
→ Callees: verifyCard, fetchRates (external API!)
|
||||
|
||||
3. READ gitnexus://process/CheckoutFlow
|
||||
3. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
→ Step 3: validatePayment → calls external API
|
||||
|
||||
4. Root cause: fetchRates calls external API without proper timeout
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
|
|||
|
||||
## Quick Start
|
||||
```
|
||||
0. If "Index is stale" → gitnexus_analyze({})
|
||||
1. READ gitnexus://context → Get codebase overview (~150 tokens)
|
||||
2. READ gitnexus://clusters → See all functional clusters
|
||||
3. READ gitnexus://cluster/{name} → Deep dive on specific cluster
|
||||
0. READ gitnexus://repos → Discover indexed repos (use repo param if multiple)
|
||||
1. If "Index is stale" → run `npx gitnexus analyze` in terminal
|
||||
2. READ gitnexus://repo/{name}/context → Get codebase overview (~150 tokens)
|
||||
3. READ gitnexus://repo/{name}/clusters → See all functional clusters
|
||||
4. READ gitnexus://repo/{name}/cluster/{name} → Deep dive on specific cluster
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
|
@ -22,17 +23,28 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
|
|||
## Workflow Checklist
|
||||
```
|
||||
Exploration Progress:
|
||||
- [ ] READ gitnexus://context for codebase overview
|
||||
- [ ] READ gitnexus://clusters to list all clusters
|
||||
- [ ] READ gitnexus://repos to discover available repos
|
||||
- [ ] READ gitnexus://repo/{name}/context for codebase overview
|
||||
- [ ] READ gitnexus://repo/{name}/clusters to list all clusters
|
||||
- [ ] Identify the relevant cluster by name
|
||||
- [ ] READ gitnexus://cluster/{name} for cluster details
|
||||
- [ ] READ gitnexus://repo/{name}/cluster/{name} for cluster details
|
||||
- [ ] Use gitnexus_explore for specific symbols
|
||||
```
|
||||
|
||||
## Resource Reference
|
||||
|
||||
### gitnexus://context
|
||||
Codebase overview. **Read first.**
|
||||
### gitnexus://repos
|
||||
Discover all indexed repositories. **Read first.**
|
||||
```yaml
|
||||
repos:
|
||||
- name: "my-app"
|
||||
path: "/home/user/my-app"
|
||||
files: 42
|
||||
symbols: 918
|
||||
```
|
||||
|
||||
### gitnexus://repo/{name}/context
|
||||
Codebase overview for a specific repo.
|
||||
```yaml
|
||||
project: my-app
|
||||
stats:
|
||||
|
|
@ -40,11 +52,10 @@ stats:
|
|||
symbols: 918
|
||||
clusters: 12
|
||||
processes: 45
|
||||
tools_available: [search, explore, impact, overview, cypher]
|
||||
resources_available: [clusters, processes, cluster/{name}, process/{name}]
|
||||
tools_available: [list_repos, search, explore, impact, overview, cypher]
|
||||
```
|
||||
|
||||
### gitnexus://clusters
|
||||
### gitnexus://repo/{name}/clusters
|
||||
All functional clusters with cohesion scores.
|
||||
```yaml
|
||||
clusters:
|
||||
|
|
@ -56,7 +67,7 @@ clusters:
|
|||
cohesion: 88%
|
||||
```
|
||||
|
||||
### gitnexus://cluster/{name}
|
||||
### gitnexus://repo/{name}/cluster/{clusterName}
|
||||
Members of a specific cluster.
|
||||
```yaml
|
||||
name: Auth
|
||||
|
|
@ -68,7 +79,7 @@ members:
|
|||
file: src/auth/validator.ts
|
||||
```
|
||||
|
||||
### gitnexus://process/{name}
|
||||
### gitnexus://repo/{name}/process/{processName}
|
||||
Full execution trace.
|
||||
```yaml
|
||||
name: LoginFlow
|
||||
|
|
@ -84,7 +95,7 @@ steps:
|
|||
### gitnexus_explore
|
||||
For detailed symbol context with callers/callees:
|
||||
```
|
||||
gitnexus_explore({name: "validateUser", type: "symbol"})
|
||||
gitnexus_explore({name: "validateUser", type: "symbol", repo: "my-app"})
|
||||
→ Callers: loginHandler, apiMiddleware
|
||||
→ Callees: checkToken, getUserById
|
||||
```
|
||||
|
|
@ -92,21 +103,24 @@ gitnexus_explore({name: "validateUser", type: "symbol"})
|
|||
### gitnexus_search
|
||||
For finding code by query:
|
||||
```
|
||||
gitnexus_search({query: "payment validation", depth: "full"})
|
||||
gitnexus_search({query: "payment validation", depth: "full", repo: "my-app"})
|
||||
```
|
||||
|
||||
## Example: "How does payment processing work?"
|
||||
|
||||
```
|
||||
1. READ gitnexus://context
|
||||
1. READ gitnexus://repos
|
||||
→ Repos: my-app (918 symbols)
|
||||
|
||||
2. READ gitnexus://repo/my-app/context
|
||||
→ 918 symbols, 12 clusters
|
||||
|
||||
2. READ gitnexus://clusters
|
||||
3. READ gitnexus://repo/my-app/clusters
|
||||
→ Clusters: Auth, Payment, Database, API...
|
||||
|
||||
3. READ gitnexus://cluster/Payment
|
||||
4. READ gitnexus://repo/my-app/cluster/Payment
|
||||
→ Members: processPayment, validateCard, PaymentService
|
||||
|
||||
4. READ gitnexus://process/CheckoutFlow
|
||||
5. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
→ handleCheckout → validateCart → processPayment → sendConfirmation
|
||||
```
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ description: Analyze blast radius before making code changes
|
|||
|
||||
## Quick Start
|
||||
```
|
||||
0. If "Index is stale" → gitnexus_analyze({})
|
||||
1. gitnexus_impact({target, direction: "upstream"}) → What depends on this
|
||||
2. READ gitnexus://clusters → Check affected areas
|
||||
3. READ gitnexus://processes → Affected execution flows
|
||||
0. READ gitnexus://repos → Discover indexed repos
|
||||
1. If "Index is stale" → run `npx gitnexus analyze` in terminal
|
||||
2. gitnexus_impact({target, direction: "upstream", repo: "my-app"}) → What depends on this
|
||||
3. READ gitnexus://repo/my-app/clusters → Check affected areas
|
||||
4. READ gitnexus://repo/my-app/processes → Affected execution flows
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
|
@ -30,8 +31,9 @@ description: Analyze blast radius before making code changes
|
|||
## Workflow Checklist
|
||||
```
|
||||
Impact Analysis:
|
||||
- [ ] gitnexus_impact(target, "upstream") to find dependents
|
||||
- [ ] READ gitnexus://clusters to understand affected areas
|
||||
- [ ] READ gitnexus://repos to find the right repo
|
||||
- [ ] gitnexus_impact(target, "upstream", repo) to find dependents
|
||||
- [ ] READ gitnexus://repo/{name}/clusters to understand affected areas
|
||||
- [ ] Check high-confidence (>0.8) dependencies first
|
||||
- [ ] Count affected clusters (cross-cutting = higher risk)
|
||||
- [ ] If >10 processes affected, consider splitting change
|
||||
|
|
@ -39,7 +41,7 @@ Impact Analysis:
|
|||
|
||||
## Resource Reference
|
||||
|
||||
### gitnexus://clusters
|
||||
### gitnexus://repo/{name}/clusters
|
||||
Check which clusters might be affected:
|
||||
```yaml
|
||||
clusters:
|
||||
|
|
@ -49,7 +51,7 @@ clusters:
|
|||
symbols: 32
|
||||
```
|
||||
|
||||
### gitnexus://processes
|
||||
### gitnexus://repo/{name}/processes
|
||||
Find which processes touch the target:
|
||||
```yaml
|
||||
processes:
|
||||
|
|
@ -67,7 +69,8 @@ gitnexus_impact({
|
|||
target: "validateUser",
|
||||
direction: "upstream",
|
||||
minConfidence: 0.8,
|
||||
maxDepth: 3
|
||||
maxDepth: 3,
|
||||
repo: "my-app"
|
||||
})
|
||||
|
||||
→ d=1 (WILL BREAK):
|
||||
|
|
@ -103,11 +106,11 @@ Before Committing:
|
|||
## Example: "What breaks if I change validateUser?"
|
||||
|
||||
```
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream", repo: "my-app"})
|
||||
→ d=1: loginHandler, apiMiddleware
|
||||
→ d=2: authRouter, sessionManager
|
||||
|
||||
2. READ gitnexus://clusters
|
||||
2. READ gitnexus://repo/my-app/clusters
|
||||
→ Auth and API clusters affected
|
||||
|
||||
3. Decision: 2 direct callers, 2 clusters = MEDIUM risk
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ description: Plan safe refactors using blast radius and dependency mapping
|
|||
|
||||
## Quick Start
|
||||
```
|
||||
0. If "Index is stale" → gitnexus_analyze({})
|
||||
1. gitnexus_impact({target, direction: "upstream"}) → Map all dependents
|
||||
2. READ gitnexus://schema → Understand graph structure
|
||||
3. gitnexus_cypher → Find all references
|
||||
0. READ gitnexus://repos → Discover indexed repos
|
||||
1. If "Index is stale" → run `npx gitnexus analyze` in terminal
|
||||
2. gitnexus_impact({target, direction: "upstream", repo: "my-app"}) → Map all dependents
|
||||
3. READ gitnexus://repo/my-app/schema → Understand graph structure
|
||||
4. gitnexus_cypher({query: "...", repo: "my-app"}) → Find all references
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
|
@ -24,8 +25,8 @@ description: Plan safe refactors using blast radius and dependency mapping
|
|||
### Rename Symbol
|
||||
```
|
||||
Rename Refactoring:
|
||||
- [ ] gitnexus_impact(oldName, "upstream") — find all callers
|
||||
- [ ] gitnexus_search(oldName) — find string literals
|
||||
- [ ] gitnexus_impact({target: oldName, direction: "upstream", repo: "my-app"}) — find all callers
|
||||
- [ ] gitnexus_search({query: oldName, repo: "my-app"}) — find string literals
|
||||
- [ ] Check for reflection/dynamic references
|
||||
- [ ] Update in order: interface → implementation → usages
|
||||
- [ ] Run tests for affected processes
|
||||
|
|
@ -34,9 +35,9 @@ Rename Refactoring:
|
|||
### Extract Module
|
||||
```
|
||||
Extract Module:
|
||||
- [ ] gitnexus_explore(target, "symbol") — map dependencies
|
||||
- [ ] gitnexus_impact(target, "upstream") — find callers
|
||||
- [ ] READ gitnexus://cluster/{name} — check cohesion
|
||||
- [ ] gitnexus_explore({name: target, type: "symbol", repo: "my-app"}) — map dependencies
|
||||
- [ ] gitnexus_impact({target, direction: "upstream", repo: "my-app"}) — find callers
|
||||
- [ ] READ gitnexus://repo/my-app/cluster/{name} — check cohesion
|
||||
- [ ] Define new module interface
|
||||
- [ ] Update imports across affected files
|
||||
```
|
||||
|
|
@ -44,7 +45,7 @@ Extract Module:
|
|||
### Split Function
|
||||
```
|
||||
Split Function:
|
||||
- [ ] gitnexus_explore(target, "symbol") — understand callees
|
||||
- [ ] gitnexus_explore({name: target, type: "symbol", repo: "my-app"}) — understand callees
|
||||
- [ ] Group related logic
|
||||
- [ ] gitnexus_impact — verify callers won't break
|
||||
- [ ] Create new functions
|
||||
|
|
@ -53,7 +54,7 @@ Split Function:
|
|||
|
||||
## Resource Reference
|
||||
|
||||
### gitnexus://schema
|
||||
### gitnexus://repo/{name}/schema
|
||||
Graph structure for Cypher queries:
|
||||
```yaml
|
||||
nodes: [Function, Class, Method, Community, Process]
|
||||
|
|
@ -65,7 +66,7 @@ example_queries:
|
|||
RETURN caller.name
|
||||
```
|
||||
|
||||
### gitnexus://cluster/{name}
|
||||
### gitnexus://repo/{name}/cluster/{clusterName}
|
||||
Check if extraction preserves cohesion:
|
||||
```yaml
|
||||
name: Payment
|
||||
|
|
@ -91,13 +92,13 @@ RETURN importer.name, importer.filePath
|
|||
## Example: Safely Rename `validateUser` to `authenticateUser`
|
||||
|
||||
```
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream", repo: "my-app"})
|
||||
→ loginHandler, apiMiddleware, testUtils
|
||||
|
||||
2. gitnexus_search({query: "validateUser"})
|
||||
2. gitnexus_search({query: "validateUser", repo: "my-app"})
|
||||
→ Found in: config.json (dynamic reference!)
|
||||
|
||||
3. READ gitnexus://processes
|
||||
3. READ gitnexus://repo/my-app/processes
|
||||
→ LoginFlow, TokenRefresh, APIGateway
|
||||
|
||||
4. Plan update order:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ interface RepoStats {
|
|||
nodes?: number;
|
||||
edges?: number;
|
||||
communities?: number;
|
||||
clusters?: number; // Aggregated cluster count (what tools show)
|
||||
processes?: number;
|
||||
}
|
||||
|
||||
|
|
@ -29,10 +30,11 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
|
|||
* Generate the full GitNexus context content (resources-first approach)
|
||||
*/
|
||||
function generateGitNexusContent(projectName: string, stats: RepoStats): string {
|
||||
const clusterCount = stats.clusters || stats.communities || 0;
|
||||
return `${GITNEXUS_START_MARKER}
|
||||
# GitNexus MCP
|
||||
|
||||
This project is indexed by GitNexus, providing AI agents with deep code intelligence.
|
||||
This project is indexed as **${projectName}** by GitNexus, providing AI agents with deep code intelligence.
|
||||
|
||||
## Project: ${projectName}
|
||||
|
||||
|
|
@ -41,54 +43,63 @@ This project is indexed by GitNexus, providing AI agents with deep code intellig
|
|||
| Files | ${stats.files || 0} |
|
||||
| Symbols | ${stats.nodes || 0} |
|
||||
| Relationships | ${stats.edges || 0} |
|
||||
| Communities | ${stats.communities || 0} |
|
||||
| Clusters | ${clusterCount} |
|
||||
| Processes | ${stats.processes || 0} |
|
||||
|
||||
> **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.
|
||||
|
||||
## Quick Start
|
||||
|
||||
\`\`\`
|
||||
1. READ gitnexus://context → Get codebase overview (~150 tokens)
|
||||
2. READ gitnexus://clusters → See all functional clusters
|
||||
3. READ gitnexus://cluster/{name} → Deep dive on specific cluster
|
||||
4. gitnexus_search(query) → Find code by query
|
||||
1. READ gitnexus://repos → Discover all indexed repos
|
||||
2. READ gitnexus://repo/${projectName}/context → Get codebase overview (~150 tokens)
|
||||
3. READ gitnexus://repo/${projectName}/clusters → See all functional clusters
|
||||
4. gitnexus_search({query: "...", repo: "${projectName}"}) → Find code by query
|
||||
\`\`\`
|
||||
|
||||
## Available Resources
|
||||
|
||||
| Resource | Purpose |
|
||||
|----------|---------|
|
||||
| \`gitnexus://context\` | Codebase stats, tools, and resources overview |
|
||||
| \`gitnexus://clusters\` | All clusters with symbol counts and cohesion |
|
||||
| \`gitnexus://cluster/{name}\` | Cluster members and details |
|
||||
| \`gitnexus://processes\` | All execution flows with types |
|
||||
| \`gitnexus://process/{name}\` | Full process trace with steps |
|
||||
| \`gitnexus://schema\` | Graph schema for Cypher queries |
|
||||
| \`gitnexus://repos\` | List all indexed repositories |
|
||||
| \`gitnexus://repo/${projectName}/context\` | Codebase stats, tools, and resources overview |
|
||||
| \`gitnexus://repo/${projectName}/clusters\` | All clusters with symbol counts and cohesion |
|
||||
| \`gitnexus://repo/${projectName}/cluster/{name}\` | Cluster members and details |
|
||||
| \`gitnexus://repo/${projectName}/processes\` | All execution flows with types |
|
||||
| \`gitnexus://repo/${projectName}/process/{name}\` | Full process trace with steps |
|
||||
| \`gitnexus://repo/${projectName}/schema\` | Graph schema for Cypher queries |
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Purpose | When to Use |
|
||||
|------|---------|-------------|
|
||||
| \`list_repos\` | Discover indexed repos | First step with multiple repos |
|
||||
| \`search\` | Semantic + keyword search | Finding code by query |
|
||||
| \`overview\` | List clusters & processes | Understanding architecture |
|
||||
| \`explore\` | Deep dive on symbol/cluster/process | Detailed investigation |
|
||||
| \`impact\` | Blast radius analysis | Before making changes |
|
||||
| \`cypher\` | Raw graph queries | Complex analysis |
|
||||
|
||||
> **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.
|
||||
|
||||
> **Multi-repo:** When multiple repos are indexed, pass \`repo: "${projectName}"\` to target this project.
|
||||
|
||||
## Workflow Examples
|
||||
|
||||
### Exploring the Codebase
|
||||
\`\`\`
|
||||
READ gitnexus://context → Stats and overview
|
||||
READ gitnexus://clusters → Find relevant cluster
|
||||
READ gitnexus://cluster/Auth → Explore Auth cluster
|
||||
gitnexus_explore("validateUser", "symbol") → Detailed symbol info
|
||||
READ gitnexus://repos → Discover repos
|
||||
READ gitnexus://repo/${projectName}/context → Stats and overview (check for staleness)
|
||||
READ gitnexus://repo/${projectName}/clusters → Find relevant cluster by name
|
||||
READ gitnexus://repo/${projectName}/cluster/{name} → See members of that cluster
|
||||
gitnexus_explore({name: "<symbol_name>", type: "symbol", repo: "${projectName}"})
|
||||
\`\`\`
|
||||
|
||||
### Planning a Change
|
||||
\`\`\`
|
||||
gitnexus_impact("UserService", "upstream") → See what breaks
|
||||
READ gitnexus://processes → Check affected flows
|
||||
gitnexus_explore("LoginFlow", "process") → Trace execution
|
||||
gitnexus_search({query: "<what you want to change>", repo: "${projectName}"})
|
||||
gitnexus_impact({target: "<symbol_name>", direction: "upstream", repo: "${projectName}"})
|
||||
READ gitnexus://repo/${projectName}/processes → Check affected execution flows
|
||||
\`\`\`
|
||||
|
||||
## Graph Schema
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ import ora from 'ora';
|
|||
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
||||
import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex } from '../core/kuzu/kuzu-adapter.js';
|
||||
import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js';
|
||||
import { getStoragePaths, saveMeta, loadMeta, addToGitignore } from '../storage/repo-manager.js';
|
||||
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
|
||||
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
||||
import { generateAIContextFiles } from './ai-context.js';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
export interface AnalyzeOptions {
|
||||
force?: boolean;
|
||||
|
|
@ -84,6 +85,7 @@ export const analyzeCommand = async (
|
|||
await createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
||||
await createFTSIndex('Class', 'class_fts', ['name', 'content']);
|
||||
await createFTSIndex('Method', 'method_fts', ['name', 'content']);
|
||||
await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
|
||||
} catch (e: any) {
|
||||
// FTS index creation may fail if tables are empty (no data for that type)
|
||||
console.error('Note: Some FTS indexes may not have been created:', e.message);
|
||||
|
|
@ -103,7 +105,7 @@ export const analyzeCommand = async (
|
|||
|
||||
// Save metadata
|
||||
const stats = await getKuzuStats();
|
||||
await saveMeta(storagePath, {
|
||||
const meta = {
|
||||
repoPath,
|
||||
lastCommit: currentCommit,
|
||||
indexedAt: new Date().toISOString(),
|
||||
|
|
@ -114,18 +116,35 @@ export const analyzeCommand = async (
|
|||
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
||||
processes: pipelineResult.processResult?.stats.totalProcesses,
|
||||
},
|
||||
});
|
||||
};
|
||||
await saveMeta(storagePath, meta);
|
||||
|
||||
// Register in global registry
|
||||
await registerRepo(repoPath, meta);
|
||||
|
||||
// Add .gitnexus to .gitignore
|
||||
await addToGitignore(repoPath);
|
||||
|
||||
// Generate AI context files
|
||||
const projectName = path.basename(repoPath);
|
||||
// Compute aggregated cluster count (grouped by heuristicLabel, >=5 symbols)
|
||||
// This matches the aggregation logic in local-backend.ts for tool output consistency.
|
||||
let aggregatedClusterCount = 0;
|
||||
if (pipelineResult.communityResult?.communities) {
|
||||
const groups = new Map<string, number>();
|
||||
for (const c of pipelineResult.communityResult.communities) {
|
||||
const label = c.heuristicLabel || c.label || 'Unknown';
|
||||
groups.set(label, (groups.get(label) || 0) + c.symbolCount);
|
||||
}
|
||||
aggregatedClusterCount = Array.from(groups.values()).filter(count => count >= 5).length;
|
||||
}
|
||||
|
||||
const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
|
||||
files: pipelineResult.fileContents.size,
|
||||
nodes: stats.nodes,
|
||||
edges: stats.edges,
|
||||
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
||||
clusters: aggregatedClusterCount,
|
||||
processes: pipelineResult.processResult?.stats.totalProcesses,
|
||||
});
|
||||
|
||||
|
|
@ -140,4 +159,13 @@ export const analyzeCommand = async (
|
|||
if (aiContext.files.length > 0) {
|
||||
console.log(` AI Context: ${aiContext.files.join(', ')}`);
|
||||
}
|
||||
|
||||
// Hint about setup if it hasn't been run
|
||||
try {
|
||||
await fs.access(getGlobalRegistryPath());
|
||||
} catch {
|
||||
// Registry didn't exist before this run — suggest setup
|
||||
console.log('');
|
||||
console.log(' Tip: Run `gitnexus setup` to configure MCP for your editor.');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,12 +2,43 @@
|
|||
* Clean Command
|
||||
*
|
||||
* Removes the .gitnexus index from the current repository.
|
||||
* Also unregisters it from the global registry.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import { findRepo, getStoragePath } from '../storage/repo-manager.js';
|
||||
import { findRepo, unregisterRepo, listRegisteredRepos } from '../storage/repo-manager.js';
|
||||
|
||||
export const cleanCommand = async (options?: { force?: boolean }) => {
|
||||
export const cleanCommand = async (options?: { force?: boolean; all?: boolean }) => {
|
||||
// --all flag: clean all indexed repos
|
||||
if (options?.all) {
|
||||
if (!options?.force) {
|
||||
const entries = await listRegisteredRepos();
|
||||
if (entries.length === 0) {
|
||||
console.log('No indexed repositories found.');
|
||||
return;
|
||||
}
|
||||
console.log(`This will delete GitNexus indexes for ${entries.length} repo(s):`);
|
||||
for (const entry of entries) {
|
||||
console.log(` - ${entry.name} (${entry.path})`);
|
||||
}
|
||||
console.log('\nRun with --force to confirm deletion.');
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await listRegisteredRepos();
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
await fs.rm(entry.storagePath, { recursive: true, force: true });
|
||||
await unregisterRepo(entry.path);
|
||||
console.log(`Deleted: ${entry.name} (${entry.storagePath})`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete ${entry.name}:`, err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: clean current repo
|
||||
const cwd = process.cwd();
|
||||
const repo = await findRepo(cwd);
|
||||
|
||||
|
|
@ -19,7 +50,7 @@ export const cleanCommand = async (options?: { force?: boolean }) => {
|
|||
const repoName = repo.repoPath.split(/[/\\]/).pop() || repo.repoPath;
|
||||
|
||||
if (!options?.force) {
|
||||
console.log(`⚠️ This will delete the GitNexus index for: ${repoName}`);
|
||||
console.log(`This will delete the GitNexus index for: ${repoName}`);
|
||||
console.log(` Path: ${repo.storagePath}`);
|
||||
console.log('\nRun with --force to confirm deletion.');
|
||||
return;
|
||||
|
|
@ -27,7 +58,8 @@ export const cleanCommand = async (options?: { force?: boolean }) => {
|
|||
|
||||
try {
|
||||
await fs.rm(repo.storagePath, { recursive: true, force: true });
|
||||
console.log(`🗑️ Deleted: ${repo.storagePath}`);
|
||||
await unregisterRepo(repo.repoPath);
|
||||
console.log(`Deleted: ${repo.storagePath}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to delete:', err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,18 @@ import { listCommand } from './list.js';
|
|||
import { statusCommand } from './status.js';
|
||||
import { mcpCommand } from './mcp.js';
|
||||
import { cleanCommand } from './clean.js';
|
||||
import { setupCommand } from './setup.js';
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('gitnexus')
|
||||
.description('GitNexus local CLI and MCP server')
|
||||
.version('0.1.0');
|
||||
.version('1.1.1');
|
||||
|
||||
program
|
||||
.command('setup')
|
||||
.description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode')
|
||||
.action(setupCommand);
|
||||
|
||||
program
|
||||
.command('analyze [path]')
|
||||
|
|
@ -28,12 +34,12 @@ program
|
|||
|
||||
program
|
||||
.command('mcp')
|
||||
.description('Start MCP server (stdio)')
|
||||
.description('Start MCP server (stdio) — serves all indexed repos')
|
||||
.action(mcpCommand);
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('List indexed repositories')
|
||||
.description('List all indexed repositories')
|
||||
.action(listCommand);
|
||||
|
||||
program
|
||||
|
|
@ -45,9 +51,7 @@ program
|
|||
.command('clean')
|
||||
.description('Delete GitNexus index for current repo')
|
||||
.option('-f, --force', 'Skip confirmation prompt')
|
||||
.option('--all', 'Clean all indexed repos')
|
||||
.action(cleanCommand);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,34 @@
|
|||
/**
|
||||
* List Command
|
||||
*
|
||||
* Shows info about the indexed repo in the current directory.
|
||||
* Shows all indexed repositories from the global registry.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { findRepo } from '../storage/repo-manager.js';
|
||||
import { listRegisteredRepos } from '../storage/repo-manager.js';
|
||||
|
||||
export const listCommand = async () => {
|
||||
const cwd = process.cwd();
|
||||
const repo = await findRepo(cwd);
|
||||
const entries = await listRegisteredRepos({ validate: true });
|
||||
|
||||
if (!repo) {
|
||||
console.log('No indexed repository found in this directory.');
|
||||
console.log('Run `gitnexus analyze` to index your codebase.');
|
||||
if (entries.length === 0) {
|
||||
console.log('No indexed repositories found.');
|
||||
console.log('Run `gitnexus analyze` in a git repo to index it.');
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = repo.meta.stats || {};
|
||||
const repoName = repo.repoPath.split(/[/\\]/).pop() || repo.repoPath;
|
||||
const indexedDate = new Date(repo.meta.indexedAt).toLocaleString();
|
||||
console.log(`\n Indexed Repositories (${entries.length})\n`);
|
||||
|
||||
console.log(`\n📁 ${repoName}`);
|
||||
console.log(` Path: ${repo.repoPath}`);
|
||||
console.log(` Indexed: ${indexedDate}`);
|
||||
console.log(` Stats: ${stats.files ?? 0} files, ${stats.nodes ?? 0} nodes, ${stats.edges ?? 0} edges`);
|
||||
console.log(` Commit: ${repo.meta.lastCommit?.slice(0, 7) || 'unknown'}`);
|
||||
if (stats.communities) console.log(` Communities: ${stats.communities}`);
|
||||
if (stats.processes) console.log(` Processes: ${stats.processes}`);
|
||||
for (const entry of entries) {
|
||||
const indexedDate = new Date(entry.indexedAt).toLocaleString();
|
||||
const stats = entry.stats || {};
|
||||
const commitShort = entry.lastCommit?.slice(0, 7) || 'unknown';
|
||||
|
||||
console.log(` ${entry.name}`);
|
||||
console.log(` Path: ${entry.path}`);
|
||||
console.log(` Indexed: ${indexedDate}`);
|
||||
console.log(` Commit: ${commitShort}`);
|
||||
console.log(` Stats: ${stats.files ?? 0} files, ${stats.nodes ?? 0} symbols, ${stats.edges ?? 0} edges`);
|
||||
if (stats.communities) console.log(` Clusters: ${stats.communities}`);
|
||||
if (stats.processes) console.log(` Processes: ${stats.processes}`);
|
||||
console.log('');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,96 +1,43 @@
|
|||
/**
|
||||
* MCP Command
|
||||
*
|
||||
* Starts the MCP server in standalone mode using local .gitnexus/ index.
|
||||
* Auto-detects repository by searching for .gitnexus/ folder.
|
||||
* Starts the MCP server in standalone mode.
|
||||
* Loads all indexed repos from the global registry.
|
||||
* No longer depends on cwd — works from any directory.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { startMCPServer } from '../mcp/server.js';
|
||||
import { LocalBackend, findRepo } from '../mcp/local/local-backend.js';
|
||||
|
||||
/**
|
||||
* Get candidate paths to search for .gitnexus/ folder
|
||||
*/
|
||||
function getCandidatePaths(): string[] {
|
||||
const candidates: string[] = [];
|
||||
|
||||
// 1. Explicit override (highest priority)
|
||||
if (process.env.GITNEXUS_CWD) {
|
||||
candidates.push(process.env.GITNEXUS_CWD);
|
||||
}
|
||||
|
||||
// 2. Current working directory
|
||||
candidates.push(process.cwd());
|
||||
|
||||
// 3. VS Code workspace folders (if available via env)
|
||||
if (process.env.VSCODE_WORKSPACE_FOLDER) {
|
||||
candidates.push(process.env.VSCODE_WORKSPACE_FOLDER);
|
||||
}
|
||||
|
||||
// Deduplicate while preserving order
|
||||
return [...new Set(candidates.map(p => path.resolve(p)))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a git repository root by walking up the directory tree
|
||||
*/
|
||||
async function findGitRoot(startPath: string): Promise<string | null> {
|
||||
let current = path.resolve(startPath);
|
||||
const root = path.parse(current).root;
|
||||
|
||||
while (current !== root) {
|
||||
try {
|
||||
const gitPath = path.join(current, '.git');
|
||||
const stat = await fs.stat(gitPath);
|
||||
if (stat.isDirectory()) return current;
|
||||
} catch {}
|
||||
current = path.dirname(current);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
import { LocalBackend } from '../mcp/local/local-backend.js';
|
||||
import { listRegisteredRepos } from '../storage/repo-manager.js';
|
||||
|
||||
export const mcpCommand = async () => {
|
||||
// Try multiple candidate paths to find .gitnexus/
|
||||
const candidates = getCandidatePaths();
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const repo = await findRepo(candidate);
|
||||
if (repo) {
|
||||
const local = new LocalBackend();
|
||||
await local.init(candidate);
|
||||
console.error(`GitNexus: Found index at ${repo.storagePath}`);
|
||||
|
||||
// Start MCP server (no watcher daemon - staleness hints guide LLM to call analyze)
|
||||
await startMCPServer(local);
|
||||
return;
|
||||
}
|
||||
// Load all registered repos
|
||||
const entries = await listRegisteredRepos({ validate: true });
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.error('');
|
||||
console.error(' GitNexus: No indexed repositories found.');
|
||||
console.error('');
|
||||
console.error(' To get started:');
|
||||
console.error(' 1. cd into a git repository');
|
||||
console.error(' 2. Run: gitnexus analyze');
|
||||
console.error(' 3. Restart your editor');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// No index found - give helpful error message
|
||||
for (const candidate of candidates) {
|
||||
const gitRoot = await findGitRoot(candidate);
|
||||
if (gitRoot) {
|
||||
console.error('');
|
||||
console.error('╔════════════════════════════════════════════════════╗');
|
||||
console.error('║ GitNexus: Repository Not Indexed ║');
|
||||
console.error('╠════════════════════════════════════════════════════╣');
|
||||
console.error(`║ Found git repo: ${gitRoot.slice(0, 35).padEnd(35)} ║`);
|
||||
console.error('║ ║');
|
||||
console.error('║ To enable AI code understanding, run: ║');
|
||||
console.error('║ ║');
|
||||
console.error('║ npx gitnexus analyze ║');
|
||||
console.error('║ ║');
|
||||
console.error('║ Then restart your IDE. ║');
|
||||
console.error('╚════════════════════════════════════════════════════╝');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize multi-repo backend from registry
|
||||
const backend = new LocalBackend();
|
||||
const ok = await backend.init();
|
||||
|
||||
if (!ok) {
|
||||
console.error('GitNexus: Failed to initialize backend from registry.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// No git repo found
|
||||
console.error('GitNexus: No git repository found.');
|
||||
console.error(`Searched: ${candidates.join(', ')}`);
|
||||
process.exit(1);
|
||||
|
||||
const repoNames = backend.listRepos().map(r => r.name);
|
||||
console.error(`GitNexus: MCP server starting with ${repoNames.length} repo(s): ${repoNames.join(', ')}`);
|
||||
|
||||
// Start MCP server (serves all repos)
|
||||
await startMCPServer(backend);
|
||||
};
|
||||
|
|
|
|||
190
gitnexus/src/cli/setup.ts
Normal file
190
gitnexus/src/cli/setup.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/**
|
||||
* Setup Command
|
||||
*
|
||||
* One-time global MCP configuration writer.
|
||||
* Detects installed AI editors and writes the appropriate MCP config
|
||||
* so the GitNexus MCP server is available in all projects.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { getGlobalDir } from '../storage/repo-manager.js';
|
||||
|
||||
interface SetupResult {
|
||||
configured: string[];
|
||||
skipped: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The MCP server entry for all editors
|
||||
*/
|
||||
function getMcpEntry() {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', 'gitnexus@latest', 'mcp'],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge gitnexus entry into an existing MCP config JSON object.
|
||||
* Returns the updated config.
|
||||
*/
|
||||
function mergeMcpConfig(existing: any): any {
|
||||
if (!existing || typeof existing !== 'object') {
|
||||
existing = {};
|
||||
}
|
||||
if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
|
||||
existing.mcpServers = {};
|
||||
}
|
||||
existing.mcpServers.gitnexus = getMcpEntry();
|
||||
return existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to read a JSON file, returning null if it doesn't exist or is invalid.
|
||||
*/
|
||||
async function readJsonFile(filePath: string): Promise<any | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write JSON to a file, creating parent directories if needed.
|
||||
*/
|
||||
async function writeJsonFile(filePath: string, data: any): Promise<void> {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directory exists
|
||||
*/
|
||||
async function dirExists(dirPath: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await fs.stat(dirPath);
|
||||
return stat.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Editor-specific setup ─────────────────────────────────────────
|
||||
|
||||
async function setupCursor(result: SetupResult): Promise<void> {
|
||||
const cursorDir = path.join(os.homedir(), '.cursor');
|
||||
if (!(await dirExists(cursorDir))) {
|
||||
result.skipped.push('Cursor (not installed)');
|
||||
return;
|
||||
}
|
||||
|
||||
const mcpPath = path.join(cursorDir, 'mcp.json');
|
||||
try {
|
||||
const existing = await readJsonFile(mcpPath);
|
||||
const updated = mergeMcpConfig(existing);
|
||||
await writeJsonFile(mcpPath, updated);
|
||||
result.configured.push('Cursor');
|
||||
} catch (err: any) {
|
||||
result.errors.push(`Cursor: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupClaudeCode(result: SetupResult): Promise<void> {
|
||||
// Claude Code uses `claude mcp add` — we just print the command
|
||||
// Check for common Claude Code indicators
|
||||
const claudeDir = path.join(os.homedir(), '.claude');
|
||||
const hasClaude = await dirExists(claudeDir);
|
||||
|
||||
if (!hasClaude) {
|
||||
result.skipped.push('Claude Code (not installed)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Claude Code uses a JSON settings file at ~/.claude.json or claude mcp add
|
||||
console.log('');
|
||||
console.log(' Claude Code detected. Run this command to add GitNexus:');
|
||||
console.log('');
|
||||
console.log(' claude mcp add gitnexus -- npx -y gitnexus mcp');
|
||||
console.log('');
|
||||
result.configured.push('Claude Code (manual step printed)');
|
||||
}
|
||||
|
||||
async function setupOpenCode(result: SetupResult): Promise<void> {
|
||||
const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
|
||||
if (!(await dirExists(opencodeDir))) {
|
||||
result.skipped.push('OpenCode (not installed)');
|
||||
return;
|
||||
}
|
||||
|
||||
const configPath = path.join(opencodeDir, 'config.json');
|
||||
try {
|
||||
const existing = await readJsonFile(configPath);
|
||||
const config = existing || {};
|
||||
if (!config.mcp) config.mcp = {};
|
||||
config.mcp.gitnexus = getMcpEntry();
|
||||
await writeJsonFile(configPath, config);
|
||||
result.configured.push('OpenCode');
|
||||
} catch (err: any) {
|
||||
result.errors.push(`OpenCode: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main command ──────────────────────────────────────────────────
|
||||
|
||||
export const setupCommand = async () => {
|
||||
console.log('');
|
||||
console.log(' GitNexus Setup');
|
||||
console.log(' ==============');
|
||||
console.log('');
|
||||
|
||||
// Ensure global directory exists
|
||||
const globalDir = getGlobalDir();
|
||||
await fs.mkdir(globalDir, { recursive: true });
|
||||
|
||||
const result: SetupResult = {
|
||||
configured: [],
|
||||
skipped: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
// Detect and configure each editor
|
||||
await setupCursor(result);
|
||||
await setupClaudeCode(result);
|
||||
await setupOpenCode(result);
|
||||
|
||||
// Print results
|
||||
if (result.configured.length > 0) {
|
||||
console.log(' Configured:');
|
||||
for (const name of result.configured) {
|
||||
console.log(` + ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.skipped.length > 0) {
|
||||
console.log('');
|
||||
console.log(' Skipped:');
|
||||
for (const name of result.skipped) {
|
||||
console.log(` - ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
console.log('');
|
||||
console.log(' Errors:');
|
||||
for (const err of result.errors) {
|
||||
console.log(` ! ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(' Next steps:');
|
||||
console.log(' 1. cd into any git repo');
|
||||
console.log(' 2. Run: gitnexus analyze');
|
||||
console.log(' 3. Open the repo in your editor — MCP is ready!');
|
||||
console.log('');
|
||||
};
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { KnowledgeGraph } from '../graph/types.js';
|
||||
import { ASTCache } from './ast-cache.js';
|
||||
import Parser from 'tree-sitter';
|
||||
|
|
@ -5,6 +7,7 @@ import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|||
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
||||
import { generateId } from '../../lib/utils.js';
|
||||
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
|
||||
import { SupportedLanguages } from '../../config/supported-languages.js';
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
|
|
@ -14,21 +17,217 @@ export type ImportMap = Map<string, Set<string>>;
|
|||
|
||||
export const createImportMap = (): ImportMap => new Map();
|
||||
|
||||
// Helper: Resolve import paths (relative and absolute/package-style)
|
||||
// ============================================================================
|
||||
// LANGUAGE-SPECIFIC CONFIG
|
||||
// ============================================================================
|
||||
|
||||
/** TypeScript path alias config parsed from tsconfig.json */
|
||||
interface TsconfigPaths {
|
||||
/** Map of alias prefix -> target prefix (e.g., "@/" -> "src/") */
|
||||
aliases: Map<string, string>;
|
||||
/** Base URL for path resolution (relative to repo root) */
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
/** Go module config parsed from go.mod */
|
||||
interface GoModuleConfig {
|
||||
/** Module path (e.g., "github.com/user/repo") */
|
||||
modulePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tsconfig.json to extract path aliases.
|
||||
* Tries tsconfig.json, tsconfig.app.json, tsconfig.base.json in order.
|
||||
*/
|
||||
async function loadTsconfigPaths(repoRoot: string): Promise<TsconfigPaths | null> {
|
||||
const candidates = ['tsconfig.json', 'tsconfig.app.json', 'tsconfig.base.json'];
|
||||
|
||||
for (const filename of candidates) {
|
||||
try {
|
||||
const tsconfigPath = path.join(repoRoot, filename);
|
||||
const raw = await fs.readFile(tsconfigPath, 'utf-8');
|
||||
// Strip JSON comments (// and /* */ style) for robustness
|
||||
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
const tsconfig = JSON.parse(stripped);
|
||||
const compilerOptions = tsconfig.compilerOptions;
|
||||
if (!compilerOptions?.paths) continue;
|
||||
|
||||
const baseUrl = compilerOptions.baseUrl || '.';
|
||||
const aliases = new Map<string, string>();
|
||||
|
||||
for (const [pattern, targets] of Object.entries(compilerOptions.paths)) {
|
||||
if (!Array.isArray(targets) || targets.length === 0) continue;
|
||||
const target = targets[0] as string;
|
||||
|
||||
// Convert glob patterns: "@/*" -> "@/", "src/*" -> "src/"
|
||||
const aliasPrefix = pattern.endsWith('/*') ? pattern.slice(0, -1) : pattern;
|
||||
const targetPrefix = target.endsWith('/*') ? target.slice(0, -1) : target;
|
||||
|
||||
aliases.set(aliasPrefix, targetPrefix);
|
||||
}
|
||||
|
||||
if (aliases.size > 0) {
|
||||
if (isDev) {
|
||||
console.log(`📦 Loaded ${aliases.size} path aliases from ${filename}`);
|
||||
}
|
||||
return { aliases, baseUrl };
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or isn't valid JSON - try next
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse go.mod to extract module path.
|
||||
*/
|
||||
async function loadGoModulePath(repoRoot: string): Promise<GoModuleConfig | null> {
|
||||
try {
|
||||
const goModPath = path.join(repoRoot, 'go.mod');
|
||||
const content = await fs.readFile(goModPath, 'utf-8');
|
||||
const match = content.match(/^module\s+(\S+)/m);
|
||||
if (match) {
|
||||
if (isDev) {
|
||||
console.log(`📦 Loaded Go module path: ${match[1]}`);
|
||||
}
|
||||
return { modulePath: match[1] };
|
||||
}
|
||||
} catch {
|
||||
// No go.mod
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IMPORT PATH RESOLUTION
|
||||
// ============================================================================
|
||||
|
||||
/** All file extensions to try during resolution */
|
||||
const EXTENSIONS = [
|
||||
'',
|
||||
// TypeScript/JavaScript
|
||||
'.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js',
|
||||
// Python
|
||||
'.py', '/__init__.py',
|
||||
// Java
|
||||
'.java',
|
||||
// C/C++
|
||||
'.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', '.hh',
|
||||
// C#
|
||||
'.cs',
|
||||
// Go
|
||||
'.go',
|
||||
// Rust
|
||||
'.rs', '/mod.rs',
|
||||
];
|
||||
|
||||
/**
|
||||
* Try to match a path (with extensions) against the known file set.
|
||||
* Returns the matched file path or null.
|
||||
*/
|
||||
function tryResolveWithExtensions(
|
||||
basePath: string,
|
||||
allFiles: Set<string>,
|
||||
): string | null {
|
||||
for (const ext of EXTENSIONS) {
|
||||
const candidate = basePath + ext;
|
||||
if (allFiles.has(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suffix-based resolution: try progressively shorter suffixes against all files.
|
||||
* Used for package-style imports (Java, Python, etc.).
|
||||
*/
|
||||
function suffixResolve(
|
||||
pathParts: string[],
|
||||
normalizedFileList: string[],
|
||||
allFileList: string[],
|
||||
): string | null {
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const suffix = pathParts.slice(i).join('/');
|
||||
for (const ext of EXTENSIONS) {
|
||||
const suffixWithExt = suffix + ext;
|
||||
const suffixPattern = '/' + suffixWithExt;
|
||||
const matchIdx = normalizedFileList.findIndex(filePath =>
|
||||
filePath.endsWith(suffixPattern) || filePath.toLowerCase().endsWith(suffixPattern.toLowerCase())
|
||||
);
|
||||
if (matchIdx !== -1) {
|
||||
return allFileList[matchIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an import path to a file path in the repository.
|
||||
*
|
||||
* Language-specific preprocessing is applied before the generic resolution:
|
||||
* - TypeScript/JavaScript: rewrites tsconfig path aliases
|
||||
* - Rust: converts crate::/super::/self:: to relative paths
|
||||
*
|
||||
* Java wildcards and Go package imports are handled separately in processImports
|
||||
* because they resolve to multiple files.
|
||||
*/
|
||||
const resolveImportPath = (
|
||||
currentFile: string,
|
||||
importPath: string,
|
||||
currentFile: string,
|
||||
importPath: string,
|
||||
allFiles: Set<string>,
|
||||
allFileList: string[],
|
||||
resolveCache: Map<string, string | null>
|
||||
normalizedFileList: string[],
|
||||
resolveCache: Map<string, string | null>,
|
||||
language: SupportedLanguages,
|
||||
tsconfigPaths: TsconfigPaths | null,
|
||||
): string | null => {
|
||||
const cacheKey = `${currentFile}::${importPath}`;
|
||||
if (resolveCache.has(cacheKey)) return resolveCache.get(cacheKey) ?? null;
|
||||
|
||||
// 1. Resolve '..' and '.' for relative imports
|
||||
const cache = (result: string | null): string | null => {
|
||||
resolveCache.set(cacheKey, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
// ---- TypeScript/JavaScript: rewrite path aliases ----
|
||||
if (
|
||||
(language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) &&
|
||||
tsconfigPaths &&
|
||||
!importPath.startsWith('.')
|
||||
) {
|
||||
for (const [aliasPrefix, targetPrefix] of tsconfigPaths.aliases) {
|
||||
if (importPath.startsWith(aliasPrefix)) {
|
||||
const remainder = importPath.slice(aliasPrefix.length);
|
||||
// Build the rewritten path relative to baseUrl
|
||||
const rewritten = tsconfigPaths.baseUrl === '.'
|
||||
? targetPrefix + remainder
|
||||
: tsconfigPaths.baseUrl + '/' + targetPrefix + remainder;
|
||||
|
||||
// Try direct resolution from repo root
|
||||
const resolved = tryResolveWithExtensions(rewritten, allFiles);
|
||||
if (resolved) return cache(resolved);
|
||||
|
||||
// Try suffix matching as fallback
|
||||
const parts = rewritten.split('/').filter(Boolean);
|
||||
const suffixResult = suffixResolve(parts, normalizedFileList, allFileList);
|
||||
if (suffixResult) return cache(suffixResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Rust: convert module path syntax to file paths ----
|
||||
if (language === SupportedLanguages.Rust) {
|
||||
const rustResult = resolveRustImport(currentFile, importPath, allFiles);
|
||||
if (rustResult) return cache(rustResult);
|
||||
// Fall through to generic resolution if Rust-specific didn't match
|
||||
}
|
||||
|
||||
// ---- Generic relative import resolution (./ and ../) ----
|
||||
const currentDir = currentFile.split('/').slice(0, -1);
|
||||
const parts = importPath.split('/');
|
||||
|
||||
|
||||
for (const part of parts) {
|
||||
if (part === '.') continue;
|
||||
if (part === '..') {
|
||||
|
|
@ -37,44 +236,18 @@ const resolveImportPath = (
|
|||
currentDir.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const basePath = currentDir.join('/');
|
||||
|
||||
// 2. Try extensions for all supported languages
|
||||
const extensions = [
|
||||
'',
|
||||
// TypeScript/JavaScript
|
||||
'.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js',
|
||||
// Python
|
||||
'.py', '/__init__.py',
|
||||
// Java
|
||||
'.java',
|
||||
// C/C++
|
||||
'.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', '.hh',
|
||||
// C#
|
||||
'.cs',
|
||||
// Go
|
||||
'.go',
|
||||
// Rust
|
||||
'.rs', '/mod.rs'
|
||||
];
|
||||
|
||||
if (importPath.startsWith('.')) {
|
||||
for (const ext of extensions) {
|
||||
const candidate = basePath + ext;
|
||||
if (allFiles.has(candidate)) {
|
||||
resolveCache.set(cacheKey, candidate);
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
resolveCache.set(cacheKey, null);
|
||||
return null;
|
||||
const resolved = tryResolveWithExtensions(basePath, allFiles);
|
||||
return cache(resolved);
|
||||
}
|
||||
|
||||
// 3. Handle absolute/package imports (Java, Go, Python, etc.)
|
||||
// ---- Generic package/absolute import resolution (suffix matching) ----
|
||||
// Java wildcards are handled in processImports, not here
|
||||
if (importPath.endsWith('.*')) {
|
||||
resolveCache.set(cacheKey, null);
|
||||
return null;
|
||||
return cache(null);
|
||||
}
|
||||
|
||||
const pathLike = importPath.includes('/')
|
||||
|
|
@ -82,48 +255,242 @@ const resolveImportPath = (
|
|||
: importPath.replace(/\./g, '/');
|
||||
const pathParts = pathLike.split('/').filter(Boolean);
|
||||
|
||||
// Normalize all file paths to forward slashes for matching
|
||||
const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
|
||||
const resolved = suffixResolve(pathParts, normalizedFileList, allFileList);
|
||||
return cache(resolved);
|
||||
};
|
||||
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const suffix = pathParts.slice(i).join('/');
|
||||
for (const ext of extensions) {
|
||||
const suffixWithExt = suffix + ext;
|
||||
// Require path separator before match to avoid false positives like "View.java" matching "RootView.java"
|
||||
const suffixPattern = '/' + suffixWithExt;
|
||||
const matchIdx = normalizedFileList.findIndex(filePath =>
|
||||
filePath.endsWith(suffixPattern) || filePath.toLowerCase().endsWith(suffixPattern.toLowerCase())
|
||||
);
|
||||
if (matchIdx !== -1) {
|
||||
const match = allFileList[matchIdx];
|
||||
resolveCache.set(cacheKey, match);
|
||||
return match;
|
||||
// ============================================================================
|
||||
// RUST MODULE RESOLUTION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Resolve Rust use-path to a file.
|
||||
* Handles crate::, super::, self:: prefixes and :: path separators.
|
||||
*/
|
||||
function resolveRustImport(
|
||||
currentFile: string,
|
||||
importPath: string,
|
||||
allFiles: Set<string>,
|
||||
): string | null {
|
||||
let rustPath: string;
|
||||
|
||||
if (importPath.startsWith('crate::')) {
|
||||
// crate:: resolves from src/ directory (standard Rust layout)
|
||||
rustPath = importPath.slice(7).replace(/::/g, '/');
|
||||
|
||||
// Try from src/ (standard layout)
|
||||
const fromSrc = tryRustModulePath('src/' + rustPath, allFiles);
|
||||
if (fromSrc) return fromSrc;
|
||||
|
||||
// Try from repo root (non-standard)
|
||||
const fromRoot = tryRustModulePath(rustPath, allFiles);
|
||||
if (fromRoot) return fromRoot;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (importPath.startsWith('super::')) {
|
||||
// super:: = parent directory of current file's module
|
||||
const currentDir = currentFile.split('/').slice(0, -1);
|
||||
currentDir.pop(); // Go up one level for super::
|
||||
rustPath = importPath.slice(7).replace(/::/g, '/');
|
||||
const fullPath = [...currentDir, rustPath].join('/');
|
||||
return tryRustModulePath(fullPath, allFiles);
|
||||
}
|
||||
|
||||
if (importPath.startsWith('self::')) {
|
||||
// self:: = current module's directory
|
||||
const currentDir = currentFile.split('/').slice(0, -1);
|
||||
rustPath = importPath.slice(6).replace(/::/g, '/');
|
||||
const fullPath = [...currentDir, rustPath].join('/');
|
||||
return tryRustModulePath(fullPath, allFiles);
|
||||
}
|
||||
|
||||
// Bare path without prefix (e.g., from a use in a nested module)
|
||||
// Convert :: to / and try suffix matching
|
||||
if (importPath.includes('::')) {
|
||||
rustPath = importPath.replace(/::/g, '/');
|
||||
return tryRustModulePath(rustPath, allFiles);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve a Rust module path to a file.
|
||||
* Tries: path.rs, path/mod.rs, and with the last segment stripped
|
||||
* (last segment might be a symbol name, not a module).
|
||||
*/
|
||||
function tryRustModulePath(modulePath: string, allFiles: Set<string>): string | null {
|
||||
// Try direct: path.rs
|
||||
if (allFiles.has(modulePath + '.rs')) return modulePath + '.rs';
|
||||
// Try directory: path/mod.rs
|
||||
if (allFiles.has(modulePath + '/mod.rs')) return modulePath + '/mod.rs';
|
||||
// Try path/lib.rs (for crate root)
|
||||
if (allFiles.has(modulePath + '/lib.rs')) return modulePath + '/lib.rs';
|
||||
|
||||
// The last segment might be a symbol (function, struct, etc.), not a module.
|
||||
// Strip it and try again.
|
||||
const lastSlash = modulePath.lastIndexOf('/');
|
||||
if (lastSlash > 0) {
|
||||
const parentPath = modulePath.substring(0, lastSlash);
|
||||
if (allFiles.has(parentPath + '.rs')) return parentPath + '.rs';
|
||||
if (allFiles.has(parentPath + '/mod.rs')) return parentPath + '/mod.rs';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JAVA MULTI-FILE RESOLUTION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Resolve a Java wildcard import (com.example.*) to all matching .java files.
|
||||
* Returns an array of file paths.
|
||||
*/
|
||||
function resolveJavaWildcard(
|
||||
importPath: string,
|
||||
normalizedFileList: string[],
|
||||
allFileList: string[],
|
||||
): string[] {
|
||||
// "com.example.util.*" -> "com/example/util"
|
||||
const packagePath = importPath.slice(0, -2).replace(/\./g, '/');
|
||||
const packageSuffix = '/' + packagePath + '/';
|
||||
|
||||
const matches: string[] = [];
|
||||
for (let i = 0; i < normalizedFileList.length; i++) {
|
||||
const normalized = normalizedFileList[i];
|
||||
if (normalized.includes(packageSuffix) && normalized.endsWith('.java')) {
|
||||
// Ensure the file is directly in the package (not a subdirectory)
|
||||
const afterPackage = normalized.substring(normalized.indexOf(packageSuffix) + packageSuffix.length);
|
||||
if (!afterPackage.includes('/')) {
|
||||
matches.push(allFileList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve a Java static import by stripping the member name.
|
||||
* "com.example.Constants.VALUE" -> resolve "com.example.Constants"
|
||||
*/
|
||||
function resolveJavaStaticImport(
|
||||
importPath: string,
|
||||
normalizedFileList: string[],
|
||||
allFileList: string[],
|
||||
): string | null {
|
||||
// Static imports look like: com.example.Constants.VALUE or com.example.Constants.*
|
||||
// The last segment is a member name (field/method) if it starts with lowercase or is ALL_CAPS
|
||||
const segments = importPath.split('.');
|
||||
if (segments.length < 3) return null;
|
||||
|
||||
const lastSeg = segments[segments.length - 1];
|
||||
// If last segment is a wildcard or ALL_CAPS constant or starts with lowercase, strip it
|
||||
if (lastSeg === '*' || /^[a-z]/.test(lastSeg) || /^[A-Z_]+$/.test(lastSeg)) {
|
||||
const classPath = segments.slice(0, -1).join('/');
|
||||
const classSuffix = '/' + classPath + '.java';
|
||||
for (let i = 0; i < normalizedFileList.length; i++) {
|
||||
if (normalizedFileList[i].endsWith(classSuffix) ||
|
||||
normalizedFileList[i].toLowerCase().endsWith(classSuffix.toLowerCase())) {
|
||||
return allFileList[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unresolved imports (external packages, SDK imports) are expected - don't log
|
||||
resolveCache.set(cacheKey, null);
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GO PACKAGE RESOLUTION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Resolve a Go internal package import to all .go files in the package directory.
|
||||
* Returns an array of file paths.
|
||||
*/
|
||||
function resolveGoPackage(
|
||||
importPath: string,
|
||||
goModule: GoModuleConfig,
|
||||
normalizedFileList: string[],
|
||||
allFileList: string[],
|
||||
): string[] {
|
||||
if (!importPath.startsWith(goModule.modulePath)) return [];
|
||||
|
||||
// Strip module path to get relative package path
|
||||
const relativePkg = importPath.slice(goModule.modulePath.length + 1); // e.g., "internal/auth"
|
||||
if (!relativePkg) return [];
|
||||
|
||||
const pkgSuffix = '/' + relativePkg + '/';
|
||||
const matches: string[] = [];
|
||||
|
||||
for (let i = 0; i < normalizedFileList.length; i++) {
|
||||
const normalized = normalizedFileList[i];
|
||||
// File must be directly in the package directory (not a subdirectory)
|
||||
if (normalized.includes(pkgSuffix) && normalized.endsWith('.go') && !normalized.endsWith('_test.go')) {
|
||||
const afterPkg = normalized.substring(normalized.indexOf(pkgSuffix) + pkgSuffix.length);
|
||||
if (!afterPkg.includes('/')) {
|
||||
matches.push(allFileList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN IMPORT PROCESSOR
|
||||
// ============================================================================
|
||||
|
||||
export const processImports = async (
|
||||
graph: KnowledgeGraph,
|
||||
files: { path: string; content: string }[],
|
||||
astCache: ASTCache,
|
||||
importMap: ImportMap,
|
||||
onProgress?: (current: number, total: number) => void
|
||||
onProgress?: (current: number, total: number) => void,
|
||||
repoRoot?: string,
|
||||
) => {
|
||||
// Create a Set of all file paths for fast lookup during resolution
|
||||
const allFilePaths = new Set(files.map(f => f.path));
|
||||
const parser = await loadParser();
|
||||
const resolveCache = new Map<string, string | null>();
|
||||
const allFileList = files.map(f => f.path);
|
||||
|
||||
// Pre-compute normalized file list once (forward slashes)
|
||||
const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
|
||||
|
||||
// Track import statistics
|
||||
let totalImportsFound = 0;
|
||||
let totalImportsResolved = 0;
|
||||
|
||||
// Load language-specific configs once before the file loop
|
||||
const effectiveRoot = repoRoot || '';
|
||||
const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
|
||||
const goModule = await loadGoModulePath(effectiveRoot);
|
||||
|
||||
// Helper: add an IMPORTS edge + update import map
|
||||
const addImportEdge = (filePath: string, resolvedPath: string) => {
|
||||
const sourceId = generateId('File', filePath);
|
||||
const targetId = generateId('File', resolvedPath);
|
||||
const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
|
||||
|
||||
totalImportsResolved++;
|
||||
|
||||
graph.addRelationship({
|
||||
id: relId,
|
||||
sourceId,
|
||||
targetId,
|
||||
type: 'IMPORTS',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
|
||||
if (!importMap.has(filePath)) {
|
||||
importMap.set(filePath, new Set());
|
||||
}
|
||||
importMap.get(filePath)!.add(resolvedPath);
|
||||
};
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
onProgress?.(i + 1, files.length);
|
||||
|
|
@ -132,7 +499,7 @@ export const processImports = async (
|
|||
// 1. Check language support first
|
||||
const language = getLanguageFromFilename(file.path);
|
||||
if (!language) continue;
|
||||
|
||||
|
||||
const queryStr = LANGUAGE_QUERIES[language];
|
||||
if (!queryStr) continue;
|
||||
|
||||
|
|
@ -142,14 +509,11 @@ export const processImports = async (
|
|||
// 3. Get AST (Try Cache First)
|
||||
let tree = astCache.get(file.path);
|
||||
let wasReparsed = false;
|
||||
|
||||
|
||||
if (!tree) {
|
||||
// Cache Miss: Re-parse (slower, but necessary if evicted)
|
||||
// Use larger bufferSize for files > 32KB
|
||||
try {
|
||||
tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
|
||||
} catch (parseError) {
|
||||
// Skip files that can't be parsed
|
||||
continue;
|
||||
}
|
||||
wasReparsed = true;
|
||||
|
|
@ -158,13 +522,10 @@ export const processImports = async (
|
|||
let query;
|
||||
let matches;
|
||||
try {
|
||||
const language = parser.getLanguage();
|
||||
query = new Parser.Query(language, queryStr);
|
||||
const lang = parser.getLanguage();
|
||||
query = new Parser.Query(lang, queryStr);
|
||||
matches = query.matches(tree.rootNode);
|
||||
|
||||
// Removed verbose Java import logging
|
||||
} catch (queryError: any) {
|
||||
// Only log query errors in development mode
|
||||
if (isDev) {
|
||||
console.group(`🔴 Query Error: ${file.path}`);
|
||||
console.log('Language:', language);
|
||||
|
|
@ -175,7 +536,7 @@ export const processImports = async (
|
|||
console.log('AST has errors:', tree.rootNode?.hasError);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
|
||||
if (wasReparsed) (tree as any).delete?.();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -193,44 +554,55 @@ export const processImports = async (
|
|||
return;
|
||||
}
|
||||
|
||||
// Clean path (remove quotes)
|
||||
const rawImportPath = sourceNode.text.replace(/['"]/g, '');
|
||||
// Clean path (remove quotes and angle brackets for C/C++ includes)
|
||||
const rawImportPath = sourceNode.text.replace(/['"<>]/g, '');
|
||||
totalImportsFound++;
|
||||
|
||||
// Removed verbose per-import logging
|
||||
|
||||
// Resolve to actual file in the system
|
||||
|
||||
// ---- Java: handle wildcards and static imports specially ----
|
||||
if (language === SupportedLanguages.Java) {
|
||||
if (rawImportPath.endsWith('.*')) {
|
||||
const matchedFiles = resolveJavaWildcard(rawImportPath, normalizedFileList, allFileList);
|
||||
for (const matchedFile of matchedFiles) {
|
||||
addImportEdge(file.path, matchedFile);
|
||||
}
|
||||
return; // skip single-file resolution
|
||||
}
|
||||
|
||||
// Try static import resolution (strip member name)
|
||||
const staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList);
|
||||
if (staticResolved) {
|
||||
addImportEdge(file.path, staticResolved);
|
||||
return;
|
||||
}
|
||||
// Fall through to normal resolution for regular Java imports
|
||||
}
|
||||
|
||||
// ---- Go: handle package-level imports ----
|
||||
if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
|
||||
const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
|
||||
if (pkgFiles.length > 0) {
|
||||
for (const pkgFile of pkgFiles) {
|
||||
addImportEdge(file.path, pkgFile);
|
||||
}
|
||||
return; // skip single-file resolution
|
||||
}
|
||||
// Fall through if no files found (package might be external)
|
||||
}
|
||||
|
||||
// ---- Standard single-file resolution ----
|
||||
const resolvedPath = resolveImportPath(
|
||||
file.path,
|
||||
rawImportPath,
|
||||
allFilePaths,
|
||||
allFileList,
|
||||
resolveCache
|
||||
normalizedFileList,
|
||||
resolveCache,
|
||||
language,
|
||||
tsconfigPaths,
|
||||
);
|
||||
|
||||
if (resolvedPath) {
|
||||
// A. Update Graph (File -> IMPORTS -> File)
|
||||
const sourceId = generateId('File', file.path);
|
||||
const targetId = generateId('File', resolvedPath);
|
||||
const relId = generateId('IMPORTS', `${file.path}->${resolvedPath}`);
|
||||
|
||||
totalImportsResolved++;
|
||||
|
||||
graph.addRelationship({
|
||||
id: relId,
|
||||
sourceId,
|
||||
targetId,
|
||||
type: 'IMPORTS',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
|
||||
// B. Update Import Map (For Pass 4)
|
||||
// Store all resolved import paths for this file
|
||||
if (!importMap.has(file.path)) {
|
||||
importMap.set(file.path, new Set());
|
||||
}
|
||||
importMap.get(file.path)!.add(resolvedPath);
|
||||
addImportEdge(file.path, resolvedPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -240,10 +612,8 @@ export const processImports = async (
|
|||
(tree as any).delete?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isDev) {
|
||||
console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export const runPipelineFromRepo = async (
|
|||
message: 'Resolving imports...',
|
||||
stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
});
|
||||
}, repoPath);
|
||||
|
||||
if (isDev) {
|
||||
const importsCount = graph.relationships.filter(r => r.type === 'IMPORTS').length;
|
||||
|
|
@ -199,6 +199,10 @@ export const runPipelineFromRepo = async (
|
|||
stats: { filesProcessed: files.length, totalFiles: files.length, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
|
||||
// Dynamic process cap based on codebase size
|
||||
const symbolCount = graph.nodes.filter(n => n.label !== 'File').length;
|
||||
const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
|
||||
|
||||
const processResult = await processProcesses(
|
||||
graph,
|
||||
communityResult.memberships,
|
||||
|
|
@ -210,7 +214,8 @@ export const runPipelineFromRepo = async (
|
|||
message,
|
||||
stats: { filesProcessed: files.length, totalFiles: files.length, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
}
|
||||
},
|
||||
{ maxProcesses: dynamicMaxProcesses, minSteps: 3 }
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const DEFAULT_CONFIG: ProcessDetectionConfig = {
|
|||
maxTraceDepth: 10,
|
||||
maxBranching: 4,
|
||||
maxProcesses: 75,
|
||||
minSteps: 2,
|
||||
minSteps: 3, // 3+ steps = genuine multi-hop flow (2-step is just "A calls B")
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -119,11 +119,16 @@ export const processProcesses = async (
|
|||
|
||||
onProgress?.(`Found ${allTraces.length} traces, deduplicating...`, 60);
|
||||
|
||||
// Step 3: Deduplicate similar traces
|
||||
// Step 3: Deduplicate similar traces (subset removal)
|
||||
const uniqueTraces = deduplicateTraces(allTraces);
|
||||
|
||||
// Step 3b: Deduplicate by entry+terminal pair (keep longest path per pair)
|
||||
const endpointDeduped = deduplicateByEndpoints(uniqueTraces);
|
||||
|
||||
onProgress?.(`Deduped ${uniqueTraces.length} → ${endpointDeduped.length} unique endpoint pairs`, 70);
|
||||
|
||||
// Step 4: Limit to max processes (prioritize longer traces)
|
||||
const limitedTraces = uniqueTraces
|
||||
const limitedTraces = endpointDeduped
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.slice(0, cfg.maxProcesses);
|
||||
|
||||
|
|
@ -206,11 +211,18 @@ export const processProcesses = async (
|
|||
|
||||
type AdjacencyList = Map<string, string[]>;
|
||||
|
||||
/**
|
||||
* Minimum edge confidence for process tracing.
|
||||
* Filters out ambiguous fuzzy-global matches (0.3) that cause
|
||||
* traces to jump across unrelated code areas.
|
||||
*/
|
||||
const MIN_TRACE_CONFIDENCE = 0.5;
|
||||
|
||||
const buildCallsGraph = (graph: KnowledgeGraph): AdjacencyList => {
|
||||
const adj = new Map<string, string[]>();
|
||||
|
||||
graph.relationships.forEach(rel => {
|
||||
if (rel.type === 'CALLS') {
|
||||
if (rel.type === 'CALLS' && rel.confidence >= MIN_TRACE_CONFIDENCE) {
|
||||
if (!adj.has(rel.sourceId)) {
|
||||
adj.set(rel.sourceId, []);
|
||||
}
|
||||
|
|
@ -225,7 +237,7 @@ const buildReverseCallsGraph = (graph: KnowledgeGraph): AdjacencyList => {
|
|||
const adj = new Map<string, string[]>();
|
||||
|
||||
graph.relationships.forEach(rel => {
|
||||
if (rel.type === 'CALLS') {
|
||||
if (rel.type === 'CALLS' && rel.confidence >= MIN_TRACE_CONFIDENCE) {
|
||||
if (!adj.has(rel.targetId)) {
|
||||
adj.set(rel.targetId, []);
|
||||
}
|
||||
|
|
@ -397,6 +409,31 @@ const deduplicateTraces = (traces: string[][]): string[][] => {
|
|||
return unique;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER: Deduplicate by entry+terminal endpoints
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Keep only the longest trace per unique entry→terminal pair.
|
||||
* Multiple paths between the same two endpoints are redundant for agents.
|
||||
*/
|
||||
const deduplicateByEndpoints = (traces: string[][]): string[][] => {
|
||||
if (traces.length === 0) return [];
|
||||
|
||||
const byEndpoints = new Map<string, string[]>();
|
||||
// Sort longest first so the first seen per key is the longest
|
||||
const sorted = [...traces].sort((a, b) => b.length - a.length);
|
||||
|
||||
for (const trace of sorted) {
|
||||
const key = `${trace[0]}::${trace[trace.length - 1]}`;
|
||||
if (!byEndpoints.has(key)) {
|
||||
byEndpoints.set(key, trace);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byEndpoints.values());
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER: String utilities
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -13,6 +13,39 @@ export interface BM25SearchResult {
|
|||
rank: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single FTS query via a custom executor (for MCP connection pool).
|
||||
* Returns the same shape as core queryFTS.
|
||||
*/
|
||||
async function queryFTSViaExecutor(
|
||||
executor: (cypher: string) => Promise<any[]>,
|
||||
tableName: string,
|
||||
indexName: string,
|
||||
query: string,
|
||||
limit: number,
|
||||
): Promise<Array<{ filePath: string; score: number }>> {
|
||||
const escapedQuery = query.replace(/'/g, "''");
|
||||
const cypher = `
|
||||
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', '${escapedQuery}', conjunctive := false)
|
||||
RETURN node, score
|
||||
ORDER BY score DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
try {
|
||||
const rows = await executor(cypher);
|
||||
return rows.map((row: any) => {
|
||||
const node = row.node || row[0] || {};
|
||||
const score = row.score ?? row[1] ?? 0;
|
||||
return {
|
||||
filePath: node.filePath || '',
|
||||
score: typeof score === 'number' ? score : parseFloat(score) || 0,
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search using KuzuDB's built-in FTS (always fresh, reads from disk)
|
||||
*
|
||||
|
|
@ -21,16 +54,33 @@ export interface BM25SearchResult {
|
|||
*
|
||||
* @param query - Search query string
|
||||
* @param limit - Maximum results
|
||||
* @param repoId - If provided, queries will be routed via the MCP connection pool
|
||||
* @returns Ranked search results from FTS indexes
|
||||
*/
|
||||
export const searchFTSFromKuzu = async (query: string, limit: number = 20): Promise<BM25SearchResult[]> => {
|
||||
// Search multiple tables with searchable content
|
||||
const [fileResults, functionResults, classResults, methodResults] = await Promise.all([
|
||||
queryFTS('File', 'file_fts', query, limit, false).catch(() => []),
|
||||
queryFTS('Function', 'function_fts', query, limit, false).catch(() => []),
|
||||
queryFTS('Class', 'class_fts', query, limit, false).catch(() => []),
|
||||
queryFTS('Method', 'method_fts', query, limit, false).catch(() => []),
|
||||
]);
|
||||
export const searchFTSFromKuzu = async (query: string, limit: number = 20, repoId?: string): Promise<BM25SearchResult[]> => {
|
||||
let fileResults: any[], functionResults: any[], classResults: any[], methodResults: any[], interfaceResults: any[];
|
||||
|
||||
if (repoId) {
|
||||
// Use MCP connection pool via dynamic import
|
||||
const { executeQuery } = await import('../../mcp/core/kuzu-adapter.js');
|
||||
const executor = (cypher: string) => executeQuery(repoId, cypher);
|
||||
[fileResults, functionResults, classResults, methodResults, interfaceResults] = await Promise.all([
|
||||
queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit),
|
||||
queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit),
|
||||
queryFTSViaExecutor(executor, 'Class', 'class_fts', query, limit),
|
||||
queryFTSViaExecutor(executor, 'Method', 'method_fts', query, limit),
|
||||
queryFTSViaExecutor(executor, 'Interface', 'interface_fts', query, limit),
|
||||
]);
|
||||
} else {
|
||||
// Use core kuzu adapter (CLI / pipeline context)
|
||||
[fileResults, functionResults, classResults, methodResults, interfaceResults] = await Promise.all([
|
||||
queryFTS('File', 'file_fts', query, limit, false).catch(() => []),
|
||||
queryFTS('Function', 'function_fts', query, limit, false).catch(() => []),
|
||||
queryFTS('Class', 'class_fts', query, limit, false).catch(() => []),
|
||||
queryFTS('Method', 'method_fts', query, limit, false).catch(() => []),
|
||||
queryFTS('Interface', 'interface_fts', query, limit, false).catch(() => []),
|
||||
]);
|
||||
}
|
||||
|
||||
// Merge results by filePath, summing scores for same file
|
||||
const merged = new Map<string, { filePath: string; score: number }>();
|
||||
|
|
@ -50,6 +100,7 @@ export const searchFTSFromKuzu = async (query: string, limit: number = 20): Prom
|
|||
addResults(functionResults);
|
||||
addResults(classResults);
|
||||
addResults(methodResults);
|
||||
addResults(interfaceResults);
|
||||
|
||||
// Sort by score descending and add rank
|
||||
const sorted = Array.from(merged.values())
|
||||
|
|
|
|||
|
|
@ -41,14 +41,22 @@ export const initEmbedder = async (): Promise<FeatureExtractionPipeline> => {
|
|||
|
||||
for (const device of devicesToTry) {
|
||||
try {
|
||||
embedderInstance = await (pipeline as any)(
|
||||
'feature-extraction',
|
||||
MODEL_ID,
|
||||
{
|
||||
device: device,
|
||||
dtype: 'fp32',
|
||||
}
|
||||
);
|
||||
// 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;
|
||||
process.stdout.write = (() => true) as any;
|
||||
try {
|
||||
embedderInstance = await (pipeline as any)(
|
||||
'feature-extraction',
|
||||
MODEL_ID,
|
||||
{
|
||||
device: device,
|
||||
dtype: 'fp32',
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
process.stdout.write = origWrite;
|
||||
}
|
||||
console.error(`GitNexus: Embedding model loaded (${device})`);
|
||||
return embedderInstance!;
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -1,63 +1,151 @@
|
|||
/**
|
||||
* KuzuDB Adapter (Persistent Connection)
|
||||
* KuzuDB Adapter (Connection Pool)
|
||||
*
|
||||
* Holds a single database connection for the lifetime of the MCP session.
|
||||
* This is safe since the watcher has been removed -- only one process
|
||||
* accesses the database at a time.
|
||||
* Manages a pool of KuzuDB connections keyed by repoId.
|
||||
* Connections are lazily opened on first query and evicted
|
||||
* after idle timeout or when pool exceeds max size (LRU).
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import kuzu from 'kuzu';
|
||||
|
||||
let db: kuzu.Database | null = null;
|
||||
let conn: kuzu.Connection | null = null;
|
||||
interface PoolEntry {
|
||||
db: kuzu.Database;
|
||||
conn: kuzu.Connection;
|
||||
lastUsed: number;
|
||||
dbPath: string;
|
||||
}
|
||||
|
||||
const pool = new Map<string, PoolEntry>();
|
||||
const MAX_POOL_SIZE = 5;
|
||||
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
let idleTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/**
|
||||
* Initialize with a persistent connection to the database
|
||||
* Start the idle cleanup timer (runs every 60s)
|
||||
*/
|
||||
export const initKuzu = async (path: string): Promise<void> => {
|
||||
if (conn) return; // Already initialized
|
||||
function ensureIdleTimer(): void {
|
||||
if (idleTimer) return;
|
||||
idleTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [repoId, entry] of pool) {
|
||||
if (now - entry.lastUsed > IDLE_TIMEOUT_MS) {
|
||||
closeOne(repoId);
|
||||
}
|
||||
}
|
||||
}, 60_000);
|
||||
// Don't keep the process alive just for this timer
|
||||
if (idleTimer && typeof idleTimer === 'object' && 'unref' in idleTimer) {
|
||||
(idleTimer as NodeJS.Timeout).unref();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict the least-recently-used connection if pool is at capacity
|
||||
*/
|
||||
function evictLRU(): void {
|
||||
if (pool.size < MAX_POOL_SIZE) return;
|
||||
|
||||
let oldestId: string | null = null;
|
||||
let oldestTime = Infinity;
|
||||
for (const [id, entry] of pool) {
|
||||
if (entry.lastUsed < oldestTime) {
|
||||
oldestTime = entry.lastUsed;
|
||||
oldestId = id;
|
||||
}
|
||||
}
|
||||
if (oldestId) {
|
||||
closeOne(oldestId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a single pool entry
|
||||
*/
|
||||
function closeOne(repoId: string): void {
|
||||
const entry = pool.get(repoId);
|
||||
if (!entry) return;
|
||||
try { entry.conn.close(); } catch {}
|
||||
try { entry.db.close(); } catch {}
|
||||
pool.delete(repoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize (or reuse) a connection for a specific repo
|
||||
*/
|
||||
export const initKuzu = async (repoId: string, dbPath: string): Promise<void> => {
|
||||
const existing = pool.get(repoId);
|
||||
if (existing) {
|
||||
existing.lastUsed = Date.now();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if database exists
|
||||
try {
|
||||
await fs.stat(path);
|
||||
await fs.stat(dbPath);
|
||||
} catch {
|
||||
throw new Error(`KuzuDB not found at ${path}. Run: gitnexus analyze`);
|
||||
throw new Error(`KuzuDB not found at ${dbPath}. Run: gitnexus analyze`);
|
||||
}
|
||||
|
||||
db = new kuzu.Database(path);
|
||||
conn = new kuzu.Connection(db);
|
||||
evictLRU();
|
||||
|
||||
// Silence stdout during KuzuDB init — native module may write to stdout
|
||||
// which corrupts the MCP stdio protocol.
|
||||
const origWrite = process.stdout.write;
|
||||
process.stdout.write = (() => true) as any;
|
||||
let db: kuzu.Database;
|
||||
let conn: kuzu.Connection;
|
||||
try {
|
||||
db = new kuzu.Database(dbPath);
|
||||
conn = new kuzu.Connection(db);
|
||||
} finally {
|
||||
process.stdout.write = origWrite;
|
||||
}
|
||||
pool.set(repoId, { db, conn, lastUsed: Date.now(), dbPath });
|
||||
|
||||
ensureIdleTimer();
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute a query using the persistent connection
|
||||
* Execute a query on a specific repo's connection
|
||||
*/
|
||||
export const executeQuery = async (cypher: string): Promise<any[]> => {
|
||||
if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
||||
export const executeQuery = async (repoId: string, cypher: string): Promise<any[]> => {
|
||||
const entry = pool.get(repoId);
|
||||
if (!entry) {
|
||||
throw new Error(`KuzuDB not initialized for repo "${repoId}". Call initKuzu first.`);
|
||||
}
|
||||
|
||||
const queryResult = await conn.query(cypher);
|
||||
entry.lastUsed = Date.now();
|
||||
const queryResult = await entry.conn.query(cypher);
|
||||
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
||||
const rows = await result.getAll();
|
||||
return rows;
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the persistent connection
|
||||
* Close one or all connections.
|
||||
* If repoId is provided, close only that connection.
|
||||
* If omitted, close all connections in the pool.
|
||||
*/
|
||||
export const closeKuzu = async (): Promise<void> => {
|
||||
if (conn) {
|
||||
try { await conn.close(); } catch {}
|
||||
conn = null;
|
||||
export const closeKuzu = async (repoId?: string): Promise<void> => {
|
||||
if (repoId) {
|
||||
closeOne(repoId);
|
||||
return;
|
||||
}
|
||||
if (db) {
|
||||
try { await db.close(); } catch {}
|
||||
db = null;
|
||||
|
||||
// Close all
|
||||
for (const id of [...pool.keys()]) {
|
||||
closeOne(id);
|
||||
}
|
||||
|
||||
if (idleTimer) {
|
||||
clearInterval(idleTimer);
|
||||
idleTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the database connection is active
|
||||
* Check if a specific repo's connection is active
|
||||
*/
|
||||
export const isKuzuReady = (): boolean => conn !== null && db !== null;
|
||||
export const isKuzuReady = (repoId: string): boolean => pool.has(repoId);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* MCP Resources
|
||||
* MCP Resources (Multi-Repo)
|
||||
*
|
||||
* Provides structured on-demand data to AI agents.
|
||||
* Resources complement tools by offering lightweight, cacheable data.
|
||||
* All resources use repo-scoped URIs: gitnexus://repo/{name}/context
|
||||
*/
|
||||
|
||||
import type { LocalBackend } from './local/local-backend.js';
|
||||
|
|
@ -23,35 +23,48 @@ export interface ResourceTemplate {
|
|||
}
|
||||
|
||||
/**
|
||||
* Static resources available when codebase is indexed
|
||||
* Static resources — includes per-repo resources and the global repos list
|
||||
*/
|
||||
export function getResourceDefinitions(projectName: string): ResourceDefinition[] {
|
||||
return [
|
||||
export function getResourceDefinitions(backend: LocalBackend): ResourceDefinition[] {
|
||||
const resources: ResourceDefinition[] = [
|
||||
{
|
||||
uri: 'gitnexus://context',
|
||||
name: `${projectName} Overview`,
|
||||
description: 'Codebase stats, hotspots, and available tools',
|
||||
mimeType: 'text/yaml',
|
||||
},
|
||||
{
|
||||
uri: 'gitnexus://clusters',
|
||||
name: 'All Clusters',
|
||||
description: 'List of all functional clusters with stats',
|
||||
mimeType: 'text/yaml',
|
||||
},
|
||||
{
|
||||
uri: 'gitnexus://processes',
|
||||
name: 'All Processes',
|
||||
description: 'List of all execution flows with types',
|
||||
mimeType: 'text/yaml',
|
||||
},
|
||||
{
|
||||
uri: 'gitnexus://schema',
|
||||
name: 'Graph Schema',
|
||||
description: 'Node types and relationships for Cypher queries',
|
||||
uri: 'gitnexus://repos',
|
||||
name: 'All Indexed Repositories',
|
||||
description: 'List of all indexed repos with stats. Read this first to discover available repos.',
|
||||
mimeType: 'text/yaml',
|
||||
},
|
||||
];
|
||||
|
||||
// Add per-repo context resources
|
||||
const repos = backend.listRepos();
|
||||
for (const repo of repos) {
|
||||
resources.push({
|
||||
uri: `gitnexus://repo/${repo.name}/context`,
|
||||
name: `${repo.name} Overview`,
|
||||
description: `Codebase stats and available tools for ${repo.name}`,
|
||||
mimeType: 'text/yaml',
|
||||
});
|
||||
resources.push({
|
||||
uri: `gitnexus://repo/${repo.name}/clusters`,
|
||||
name: `${repo.name} Clusters`,
|
||||
description: `All functional clusters for ${repo.name}`,
|
||||
mimeType: 'text/yaml',
|
||||
});
|
||||
resources.push({
|
||||
uri: `gitnexus://repo/${repo.name}/processes`,
|
||||
name: `${repo.name} Processes`,
|
||||
description: `All execution flows for ${repo.name}`,
|
||||
mimeType: 'text/yaml',
|
||||
});
|
||||
resources.push({
|
||||
uri: `gitnexus://repo/${repo.name}/schema`,
|
||||
name: `${repo.name} Schema`,
|
||||
description: `Graph schema for Cypher queries on ${repo.name}`,
|
||||
mimeType: 'text/yaml',
|
||||
});
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -60,13 +73,13 @@ export function getResourceDefinitions(projectName: string): ResourceDefinition[
|
|||
export function getResourceTemplates(): ResourceTemplate[] {
|
||||
return [
|
||||
{
|
||||
uriTemplate: 'gitnexus://cluster/{name}',
|
||||
uriTemplate: 'gitnexus://repo/{name}/cluster/{clusterName}',
|
||||
name: 'Cluster Detail',
|
||||
description: 'Deep dive into a specific cluster',
|
||||
mimeType: 'text/yaml',
|
||||
},
|
||||
{
|
||||
uriTemplate: 'gitnexus://process/{name}',
|
||||
uriTemplate: 'gitnexus://repo/{name}/process/{processName}',
|
||||
name: 'Process Trace',
|
||||
description: 'Step-by-step execution trace',
|
||||
mimeType: 'text/yaml',
|
||||
|
|
@ -75,55 +88,126 @@ export function getResourceTemplates(): ResourceTemplate[] {
|
|||
}
|
||||
|
||||
/**
|
||||
* Read a resource and return its content
|
||||
* Parse a resource URI to extract the repo name and resource type.
|
||||
*/
|
||||
export async function readResource(uri: string, backend: LocalBackend): Promise<string> {
|
||||
// Static resources
|
||||
if (uri === 'gitnexus://context') {
|
||||
return getContextResource(backend);
|
||||
function parseUri(uri: string): { repoName?: string; resourceType: string; param?: string } {
|
||||
if (uri === 'gitnexus://repos') return { resourceType: 'repos' };
|
||||
|
||||
// Repo-scoped: gitnexus://repo/{name}/context
|
||||
const repoMatch = uri.match(/^gitnexus:\/\/repo\/([^/]+)\/(.+)$/);
|
||||
if (repoMatch) {
|
||||
const repoName = decodeURIComponent(repoMatch[1]);
|
||||
const rest = repoMatch[2];
|
||||
|
||||
if (rest.startsWith('cluster/')) {
|
||||
return { repoName, resourceType: 'cluster', param: decodeURIComponent(rest.replace('cluster/', '')) };
|
||||
}
|
||||
if (rest.startsWith('process/')) {
|
||||
return { repoName, resourceType: 'process', param: decodeURIComponent(rest.replace('process/', '')) };
|
||||
}
|
||||
|
||||
return { repoName, resourceType: rest };
|
||||
}
|
||||
if (uri === 'gitnexus://clusters') {
|
||||
return getClustersResource(backend);
|
||||
}
|
||||
if (uri === 'gitnexus://processes') {
|
||||
return getProcessesResource(backend);
|
||||
}
|
||||
if (uri === 'gitnexus://schema') {
|
||||
return getSchemaResource();
|
||||
}
|
||||
|
||||
// Dynamic resources
|
||||
if (uri.startsWith('gitnexus://cluster/')) {
|
||||
const name = uri.replace('gitnexus://cluster/', '');
|
||||
return getClusterDetailResource(name, backend);
|
||||
}
|
||||
if (uri.startsWith('gitnexus://process/')) {
|
||||
const name = uri.replace('gitnexus://process/', '');
|
||||
return getProcessDetailResource(name, backend);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown resource: ${uri}`);
|
||||
|
||||
throw new Error(`Unknown resource URI: ${uri}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Context resource - codebase overview
|
||||
* Read a resource and return its content
|
||||
*/
|
||||
async function getContextResource(backend: LocalBackend): Promise<string> {
|
||||
const context = backend.context;
|
||||
export async function readResource(uri: string, backend: LocalBackend): Promise<string> {
|
||||
const parsed = parseUri(uri);
|
||||
|
||||
// Global repos list — no repo context needed
|
||||
if (parsed.resourceType === 'repos') {
|
||||
return getReposResource(backend);
|
||||
}
|
||||
|
||||
const repoName = parsed.repoName;
|
||||
|
||||
switch (parsed.resourceType) {
|
||||
case 'context':
|
||||
return getContextResource(backend, repoName);
|
||||
case 'clusters':
|
||||
return getClustersResource(backend, repoName);
|
||||
case 'processes':
|
||||
return getProcessesResource(backend, repoName);
|
||||
case 'schema':
|
||||
return getSchemaResource();
|
||||
case 'cluster':
|
||||
return getClusterDetailResource(parsed.param!, backend, repoName);
|
||||
case 'process':
|
||||
return getProcessDetailResource(parsed.param!, backend, repoName);
|
||||
default:
|
||||
throw new Error(`Unknown resource: ${uri}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Resource Implementations ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Repos resource — list all indexed repositories
|
||||
*/
|
||||
function getReposResource(backend: LocalBackend): string {
|
||||
const repos = backend.listRepos();
|
||||
|
||||
if (repos.length === 0) {
|
||||
return 'repos: []\n# No repositories indexed. Run: gitnexus analyze';
|
||||
}
|
||||
|
||||
const lines: string[] = ['repos:'];
|
||||
for (const repo of repos) {
|
||||
lines.push(` - name: "${repo.name}"`);
|
||||
lines.push(` path: "${repo.path}"`);
|
||||
lines.push(` indexed: "${repo.indexedAt}"`);
|
||||
lines.push(` commit: "${repo.lastCommit?.slice(0, 7) || 'unknown'}"`);
|
||||
if (repo.stats) {
|
||||
lines.push(` files: ${repo.stats.files || 0}`);
|
||||
lines.push(` symbols: ${repo.stats.nodes || 0}`);
|
||||
lines.push(` processes: ${repo.stats.processes || 0}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (repos.length > 1) {
|
||||
lines.push('');
|
||||
lines.push('# Multiple repos indexed. Use repo parameter in tool calls:');
|
||||
lines.push(`# gitnexus_search({query: "auth", repo: "${repos[0].name}"})`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Context resource — codebase overview for a specific repo
|
||||
*/
|
||||
async function getContextResource(backend: LocalBackend, repoName?: string): Promise<string> {
|
||||
// Resolve repo
|
||||
const repo = backend.resolveRepo(repoName);
|
||||
const repoId = repo.name.toLowerCase();
|
||||
const context = backend.getContext(repoId) || backend.getContext();
|
||||
|
||||
if (!context) {
|
||||
return 'error: No codebase loaded. Run: gitnexus analyze';
|
||||
}
|
||||
|
||||
// Check staleness
|
||||
const repoPath = backend.repoPath;
|
||||
const lastCommit = backend.meta?.lastCommit || 'HEAD';
|
||||
const repoPath = repo.repoPath;
|
||||
const lastCommit = repo.lastCommit || 'HEAD';
|
||||
const staleness = repoPath ? checkStaleness(repoPath, lastCommit) : { isStale: false, commitsBehind: 0 };
|
||||
|
||||
// Get aggregated cluster count (matches what overview/clusters resource shows)
|
||||
let clusterCount = context.stats.communityCount;
|
||||
try {
|
||||
const overview = await backend.callTool('overview', { showClusters: true, showProcesses: false, limit: 100, repo: repoName });
|
||||
if (overview.clusters) {
|
||||
clusterCount = overview.clusters.length;
|
||||
}
|
||||
} catch { /* fall back to raw count */ }
|
||||
|
||||
const lines: string[] = [
|
||||
`project: ${context.projectName}`,
|
||||
];
|
||||
|
||||
// Add staleness warning if index is behind
|
||||
if (staleness.isStale && staleness.hint) {
|
||||
lines.push('');
|
||||
lines.push(`staleness: "${staleness.hint}"`);
|
||||
|
|
@ -133,46 +217,59 @@ async function getContextResource(backend: LocalBackend): Promise<string> {
|
|||
lines.push('stats:');
|
||||
lines.push(` files: ${context.stats.fileCount}`);
|
||||
lines.push(` symbols: ${context.stats.functionCount}`);
|
||||
lines.push(` clusters: ${context.stats.communityCount}`);
|
||||
lines.push(` clusters: ${clusterCount}`);
|
||||
lines.push(` processes: ${context.stats.processCount}`);
|
||||
lines.push('');
|
||||
lines.push('tools_available:');
|
||||
lines.push(' - list_repos: Discover all indexed repositories');
|
||||
lines.push(' - search: Hybrid semantic + keyword search');
|
||||
lines.push(' - explore: Deep dive on symbol/cluster/process');
|
||||
lines.push(' - impact: Blast radius analysis');
|
||||
lines.push(' - overview: List all clusters and processes');
|
||||
lines.push(' - cypher: Raw graph queries');
|
||||
lines.push(' - analyze: Re-index to update stale data');
|
||||
lines.push('');
|
||||
lines.push('re_index: Run `npx gitnexus analyze` in terminal if data is stale');
|
||||
lines.push('');
|
||||
lines.push('resources_available:');
|
||||
lines.push(' - gitnexus://clusters: All clusters');
|
||||
lines.push(' - gitnexus://processes: All processes');
|
||||
lines.push(' - gitnexus://cluster/{name}: Cluster details');
|
||||
lines.push(' - gitnexus://process/{name}: Process trace');
|
||||
lines.push(' - gitnexus://repos: All indexed repositories');
|
||||
lines.push(` - gitnexus://repo/${context.projectName}/clusters: All clusters`);
|
||||
lines.push(` - gitnexus://repo/${context.projectName}/processes: All processes`);
|
||||
lines.push(` - gitnexus://repo/${context.projectName}/cluster/{name}: Cluster details`);
|
||||
lines.push(` - gitnexus://repo/${context.projectName}/process/{name}: Process trace`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clusters resource - list all clusters
|
||||
* Clusters resource
|
||||
*/
|
||||
async function getClustersResource(backend: LocalBackend): Promise<string> {
|
||||
async function getClustersResource(backend: LocalBackend, repoName?: string): Promise<string> {
|
||||
try {
|
||||
const result = await backend.callTool('overview', { showClusters: true, showProcesses: false, limit: 50 });
|
||||
// Request more than we display so aggregation has enough raw data
|
||||
const result = await backend.callTool('overview', { showClusters: true, showProcesses: false, limit: 100, repo: repoName });
|
||||
|
||||
if (!result.clusters || result.clusters.length === 0) {
|
||||
return 'clusters: []\n# No clusters detected. Run: gitnexus analyze';
|
||||
}
|
||||
|
||||
const displayLimit = 20;
|
||||
const lines: string[] = ['clusters:'];
|
||||
const toShow = result.clusters.slice(0, displayLimit);
|
||||
|
||||
for (const cluster of result.clusters) {
|
||||
for (const cluster of toShow) {
|
||||
const label = cluster.heuristicLabel || cluster.label || cluster.id;
|
||||
lines.push(` - name: "${label}"`);
|
||||
lines.push(` symbols: ${cluster.symbolCount || 0}`);
|
||||
if (cluster.cohesion) {
|
||||
lines.push(` cohesion: ${(cluster.cohesion * 100).toFixed(0)}%`);
|
||||
}
|
||||
if (cluster.subCommunities && cluster.subCommunities > 1) {
|
||||
lines.push(` sub_clusters: ${cluster.subCommunities}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.clusters.length > displayLimit) {
|
||||
lines.push(`\n# Showing top ${displayLimit} of ${result.clusters.length} clusters. Use gitnexus_search or gitnexus_explore for more.`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
|
|
@ -182,25 +279,31 @@ async function getClustersResource(backend: LocalBackend): Promise<string> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Processes resource - list all processes
|
||||
* Processes resource
|
||||
*/
|
||||
async function getProcessesResource(backend: LocalBackend): Promise<string> {
|
||||
async function getProcessesResource(backend: LocalBackend, repoName?: string): Promise<string> {
|
||||
try {
|
||||
const result = await backend.callTool('overview', { showClusters: false, showProcesses: true, limit: 50 });
|
||||
const result = await backend.callTool('overview', { showClusters: false, showProcesses: true, limit: 50, repo: repoName });
|
||||
|
||||
if (!result.processes || result.processes.length === 0) {
|
||||
return 'processes: []\n# No processes detected. Run: gitnexus analyze';
|
||||
}
|
||||
|
||||
const displayLimit = 20;
|
||||
const lines: string[] = ['processes:'];
|
||||
const toShow = result.processes.slice(0, displayLimit);
|
||||
|
||||
for (const proc of result.processes) {
|
||||
for (const proc of toShow) {
|
||||
const label = proc.heuristicLabel || proc.label || proc.id;
|
||||
lines.push(` - name: "${label}"`);
|
||||
lines.push(` type: ${proc.processType || 'unknown'}`);
|
||||
lines.push(` steps: ${proc.stepCount || 0}`);
|
||||
}
|
||||
|
||||
if (result.processes.length > displayLimit) {
|
||||
lines.push(`\n# Showing top ${displayLimit} of ${result.processes.length} processes. Use gitnexus_explore for more.`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
} catch (err: any) {
|
||||
return `error: ${err.message}`;
|
||||
|
|
@ -208,7 +311,7 @@ async function getProcessesResource(backend: LocalBackend): Promise<string> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Schema resource - graph structure for Cypher queries
|
||||
* Schema resource — graph structure for Cypher queries
|
||||
*/
|
||||
function getSchemaResource(): string {
|
||||
return `# GitNexus Graph Schema
|
||||
|
|
@ -252,9 +355,9 @@ example_queries:
|
|||
/**
|
||||
* Cluster detail resource
|
||||
*/
|
||||
async function getClusterDetailResource(name: string, backend: LocalBackend): Promise<string> {
|
||||
async function getClusterDetailResource(name: string, backend: LocalBackend, repoName?: string): Promise<string> {
|
||||
try {
|
||||
const result = await backend.callTool('explore', { name, type: 'cluster' });
|
||||
const result = await backend.callTool('explore', { name, type: 'cluster', repo: repoName });
|
||||
|
||||
if (result.error) {
|
||||
return `error: ${result.error}`;
|
||||
|
|
@ -271,6 +374,9 @@ async function getClusterDetailResource(name: string, backend: LocalBackend): Pr
|
|||
if (cluster.cohesion) {
|
||||
lines.push(`cohesion: ${(cluster.cohesion * 100).toFixed(0)}%`);
|
||||
}
|
||||
if (cluster.subCommunities && cluster.subCommunities > 1) {
|
||||
lines.push(`sub_clusters: ${cluster.subCommunities}`);
|
||||
}
|
||||
|
||||
if (members.length > 0) {
|
||||
lines.push('');
|
||||
|
|
@ -294,9 +400,9 @@ async function getClusterDetailResource(name: string, backend: LocalBackend): Pr
|
|||
/**
|
||||
* Process detail resource
|
||||
*/
|
||||
async function getProcessDetailResource(name: string, backend: LocalBackend): Promise<string> {
|
||||
async function getProcessDetailResource(name: string, backend: LocalBackend, repoName?: string): Promise<string> {
|
||||
try {
|
||||
const result = await backend.callTool('explore', { name, type: 'process' });
|
||||
const result = await backend.callTool('explore', { name, type: 'process', repo: repoName });
|
||||
|
||||
if (result.error) {
|
||||
return `error: ${result.error}`;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
/**
|
||||
* MCP Server
|
||||
* MCP Server (Multi-Repo)
|
||||
*
|
||||
* Model Context Protocol server that runs on stdio.
|
||||
* External AI tools (Cursor, Claude) spawn this process and
|
||||
* communicate via stdin/stdout using the MCP protocol.
|
||||
*
|
||||
* Tools: context, search, cypher, overview, explore, impact, analyze
|
||||
* Resources: context, clusters, processes, schema, cluster/{name}, process/{name}
|
||||
* Supports multiple indexed repositories via the global registry.
|
||||
*
|
||||
* Tools: list_repos, search, cypher, overview, explore, impact, analyze
|
||||
* Resources: repos, repo/{name}/context, repo/{name}/clusters, ...
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
|
|
@ -26,7 +28,7 @@ export async function startMCPServer(backend: LocalBackend): Promise<void> {
|
|||
const server = new Server(
|
||||
{
|
||||
name: 'gitnexus',
|
||||
version: '0.2.0',
|
||||
version: '1.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
|
|
@ -38,13 +40,7 @@ export async function startMCPServer(backend: LocalBackend): Promise<void> {
|
|||
|
||||
// Handle list resources request
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
const context = backend.context;
|
||||
|
||||
if (!context) {
|
||||
return { resources: [] };
|
||||
}
|
||||
|
||||
const resources = getResourceDefinitions(context.projectName);
|
||||
const resources = getResourceDefinitions(backend);
|
||||
return {
|
||||
resources: resources.map(r => ({
|
||||
uri: r.uri,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* MCP Tool Definitions
|
||||
*
|
||||
* Defines the tools that GitNexus exposes to external AI agents.
|
||||
* Only includes tools that provide unique value over native IDE capabilities.
|
||||
* All tools support an optional `repo` parameter for multi-repo setups.
|
||||
*/
|
||||
|
||||
export interface ToolDefinition {
|
||||
|
|
@ -23,51 +23,38 @@ export interface ToolDefinition {
|
|||
|
||||
export const GITNEXUS_TOOLS: ToolDefinition[] = [
|
||||
{
|
||||
name: 'analyze',
|
||||
description: `Index or re-index the current repository. Runs the full pipeline in-process.
|
||||
name: 'list_repos',
|
||||
description: `List all indexed repositories available to GitNexus.
|
||||
|
||||
Creates .gitnexus/ in repo root with:
|
||||
- Knowledge graph (functions, classes, calls, imports)
|
||||
- Full-text search indexes
|
||||
- Community detection (Leiden)
|
||||
- Process tracing
|
||||
- Embeddings for semantic search
|
||||
Returns each repo's name, path, indexed date, last commit, and stats.
|
||||
Use this to discover which repos are available before querying.
|
||||
|
||||
Run this when:
|
||||
- First time using GitNexus on a repo
|
||||
- After major code changes
|
||||
- When staleness warning appears in gitnexus://context
|
||||
- When 'not indexed' error appears
|
||||
|
||||
Note: This may take 30-120 seconds for large repos.`,
|
||||
When multiple repos are indexed, you MUST specify the "repo" parameter
|
||||
on other tools (search, explore, impact, etc.) to target the correct one.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Repo path (default: current directory)' },
|
||||
force: { type: 'boolean', description: 'Re-index even if exists', default: false },
|
||||
skipEmbeddings: { type: 'boolean', description: 'Skip embedding generation (faster)', default: false },
|
||||
},
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
description: `Hybrid search (keyword + semantic) across the codebase.
|
||||
Returns code nodes with their graph connections, grouped by process.
|
||||
Returns code nodes with cluster context and optional graph connections.
|
||||
|
||||
BETTER THAN IDE search because:
|
||||
- Process-aware grouping (shows execution flows)
|
||||
- Cluster context (which functional area)
|
||||
- Relationship data (callers/callees)
|
||||
- Cluster context (which functional area each result belongs to)
|
||||
- Relationship data (callers/callees with depth=full)
|
||||
- Hybrid ranking (BM25 + semantic via Reciprocal Rank Fusion)
|
||||
|
||||
RETURNS: Array of {name, type, filePath, code, connections[], cluster, processes[]}`,
|
||||
RETURNS: Array of {name, type, filePath, cluster?, connections[]?, fusedScore, searchSource}`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Natural language or keyword search query' },
|
||||
limit: { type: 'number', description: 'Max results to return', default: 10 },
|
||||
depth: { type: 'string', description: 'Result detail: "definitions" (symbols only) or "full" (with relationships)', enum: ['definitions', 'full'], default: 'definitions' },
|
||||
groupByProcess: { type: 'boolean', description: 'Group results by process', default: true },
|
||||
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
|
|
@ -98,6 +85,7 @@ TIPS:
|
|||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Cypher query to execute' },
|
||||
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
|
|
@ -118,6 +106,7 @@ Use after search to understand context of a specific node.`,
|
|||
properties: {
|
||||
name: { type: 'string', description: 'Name of symbol, cluster, or process to explore' },
|
||||
type: { type: 'string', description: 'Type: symbol, cluster, or process' },
|
||||
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
||||
},
|
||||
required: ['name', 'type'],
|
||||
},
|
||||
|
|
@ -138,6 +127,7 @@ Use to understand overall codebase structure before diving deep.`,
|
|||
showProcesses: { type: 'boolean', description: 'Include process list', default: true },
|
||||
showClusters: { type: 'boolean', description: 'Include cluster list', default: true },
|
||||
limit: { type: 'number', description: 'Max items per category', default: 20 },
|
||||
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
|
|
@ -171,6 +161,7 @@ Depth groups:
|
|||
relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS (default: usage-based)' },
|
||||
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
|
||||
minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
|
||||
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
||||
},
|
||||
required: ['target', 'direction'],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
* Repository Manager
|
||||
*
|
||||
* Manages GitNexus index storage in .gitnexus/ at repo root.
|
||||
* Also maintains a global registry at ~/.gitnexus/registry.json
|
||||
* so the MCP server can discover indexed repos from any cwd.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
export interface RepoMeta {
|
||||
repoPath: string;
|
||||
|
|
@ -29,8 +32,22 @@ export interface IndexedRepo {
|
|||
meta: RepoMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of an entry in the global registry (~/.gitnexus/registry.json)
|
||||
*/
|
||||
export interface RegistryEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
storagePath: string;
|
||||
indexedAt: string;
|
||||
lastCommit: string;
|
||||
stats?: RepoMeta['stats'];
|
||||
}
|
||||
|
||||
const GITNEXUS_DIR = '.gitnexus';
|
||||
|
||||
// ─── Local Storage Helpers ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the .gitnexus storage path for a repository
|
||||
*/
|
||||
|
|
@ -135,3 +152,113 @@ export const addToGitignore = async (repoPath: string): Promise<void> => {
|
|||
await fs.writeFile(gitignorePath, `${GITNEXUS_DIR}\n`, 'utf-8');
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Global Registry (~/.gitnexus/registry.json) ───────────────────────
|
||||
|
||||
/**
|
||||
* Get the path to the global GitNexus directory
|
||||
*/
|
||||
export const getGlobalDir = (): string => {
|
||||
return path.join(os.homedir(), '.gitnexus');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the path to the global registry file
|
||||
*/
|
||||
export const getGlobalRegistryPath = (): string => {
|
||||
return path.join(getGlobalDir(), 'registry.json');
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the global registry. Returns empty array if not found.
|
||||
*/
|
||||
export const readRegistry = async (): Promise<RegistryEntry[]> => {
|
||||
try {
|
||||
const raw = await fs.readFile(getGlobalRegistryPath(), 'utf-8');
|
||||
const data = JSON.parse(raw);
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Write the global registry to disk
|
||||
*/
|
||||
const writeRegistry = async (entries: RegistryEntry[]): Promise<void> => {
|
||||
const dir = getGlobalDir();
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(getGlobalRegistryPath(), JSON.stringify(entries, null, 2), 'utf-8');
|
||||
};
|
||||
|
||||
/**
|
||||
* Register (add or update) a repo in the global registry.
|
||||
* Called after `gitnexus analyze` completes.
|
||||
*/
|
||||
export const registerRepo = async (repoPath: string, meta: RepoMeta): Promise<void> => {
|
||||
const resolved = path.resolve(repoPath);
|
||||
const name = path.basename(resolved);
|
||||
const { storagePath } = getStoragePaths(resolved);
|
||||
|
||||
const entries = await readRegistry();
|
||||
const existing = entries.findIndex(
|
||||
(e) => path.resolve(e.path) === resolved
|
||||
);
|
||||
|
||||
const entry: RegistryEntry = {
|
||||
name,
|
||||
path: resolved,
|
||||
storagePath,
|
||||
indexedAt: meta.indexedAt,
|
||||
lastCommit: meta.lastCommit,
|
||||
stats: meta.stats,
|
||||
};
|
||||
|
||||
if (existing >= 0) {
|
||||
entries[existing] = entry;
|
||||
} else {
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
await writeRegistry(entries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a repo from the global registry.
|
||||
* Called after `gitnexus clean`.
|
||||
*/
|
||||
export const unregisterRepo = async (repoPath: string): Promise<void> => {
|
||||
const resolved = path.resolve(repoPath);
|
||||
const entries = await readRegistry();
|
||||
const filtered = entries.filter(
|
||||
(e) => path.resolve(e.path) !== resolved
|
||||
);
|
||||
await writeRegistry(filtered);
|
||||
};
|
||||
|
||||
/**
|
||||
* List all registered repos from the global registry.
|
||||
* Optionally validates that each entry's .gitnexus/ still exists.
|
||||
*/
|
||||
export const listRegisteredRepos = async (opts?: { validate?: boolean }): Promise<RegistryEntry[]> => {
|
||||
const entries = await readRegistry();
|
||||
if (!opts?.validate) return entries;
|
||||
|
||||
// Validate each entry still has a .gitnexus/ directory
|
||||
const valid: RegistryEntry[] = [];
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
await fs.access(path.join(entry.storagePath, 'meta.json'));
|
||||
valid.push(entry);
|
||||
} catch {
|
||||
// Index no longer exists — skip
|
||||
}
|
||||
}
|
||||
|
||||
// If we pruned any entries, save the cleaned registry
|
||||
if (valid.length !== entries.length) {
|
||||
await writeRegistry(valid);
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue