gitnexus 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/dist/cli/ai-context.d.ts +21 -0
- package/dist/cli/ai-context.js +219 -0
- package/dist/cli/analyze.d.ts +10 -0
- package/dist/cli/analyze.js +118 -0
- package/dist/cli/clean.d.ts +8 -0
- package/dist/cli/clean.js +29 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/list.d.ts +6 -0
- package/dist/cli/list.js +27 -0
- package/dist/cli/mcp.d.ts +7 -0
- package/dist/cli/mcp.js +85 -0
- package/dist/cli/serve.d.ts +3 -0
- package/dist/cli/serve.js +5 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.js +27 -0
- package/dist/config/ignore-service.d.ts +1 -0
- package/dist/config/ignore-service.js +208 -0
- package/dist/config/supported-languages.d.ts +11 -0
- package/dist/config/supported-languages.js +15 -0
- package/dist/core/embeddings/embedder.d.ts +60 -0
- package/dist/core/embeddings/embedder.js +205 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +50 -0
- package/dist/core/embeddings/embedding-pipeline.js +321 -0
- package/dist/core/embeddings/index.d.ts +9 -0
- package/dist/core/embeddings/index.js +9 -0
- package/dist/core/embeddings/text-generator.d.ts +24 -0
- package/dist/core/embeddings/text-generator.js +182 -0
- package/dist/core/embeddings/types.d.ts +87 -0
- package/dist/core/embeddings/types.js +32 -0
- package/dist/core/graph/graph.d.ts +2 -0
- package/dist/core/graph/graph.js +61 -0
- package/dist/core/graph/types.d.ts +50 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/ingestion/ast-cache.d.ts +11 -0
- package/dist/core/ingestion/ast-cache.js +34 -0
- package/dist/core/ingestion/call-processor.d.ts +8 -0
- package/dist/core/ingestion/call-processor.js +269 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
- package/dist/core/ingestion/cluster-enricher.js +170 -0
- package/dist/core/ingestion/community-processor.d.ts +39 -0
- package/dist/core/ingestion/community-processor.js +269 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +39 -0
- package/dist/core/ingestion/entry-point-scoring.js +235 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +5 -0
- package/dist/core/ingestion/filesystem-walker.js +26 -0
- package/dist/core/ingestion/framework-detection.d.ts +38 -0
- package/dist/core/ingestion/framework-detection.js +183 -0
- package/dist/core/ingestion/heritage-processor.d.ts +14 -0
- package/dist/core/ingestion/heritage-processor.js +134 -0
- package/dist/core/ingestion/import-processor.d.ts +8 -0
- package/dist/core/ingestion/import-processor.js +490 -0
- package/dist/core/ingestion/parsing-processor.d.ts +8 -0
- package/dist/core/ingestion/parsing-processor.js +249 -0
- package/dist/core/ingestion/pipeline.d.ts +2 -0
- package/dist/core/ingestion/pipeline.js +228 -0
- package/dist/core/ingestion/process-processor.d.ts +51 -0
- package/dist/core/ingestion/process-processor.js +278 -0
- package/dist/core/ingestion/structure-processor.d.ts +2 -0
- package/dist/core/ingestion/structure-processor.js +36 -0
- package/dist/core/ingestion/symbol-table.d.ts +33 -0
- package/dist/core/ingestion/symbol-table.js +38 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -0
- package/dist/core/ingestion/tree-sitter-queries.js +319 -0
- package/dist/core/ingestion/utils.d.ts +10 -0
- package/dist/core/ingestion/utils.js +44 -0
- package/dist/core/kuzu/csv-generator.d.ts +22 -0
- package/dist/core/kuzu/csv-generator.js +272 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +81 -0
- package/dist/core/kuzu/kuzu-adapter.js +568 -0
- package/dist/core/kuzu/schema.d.ts +53 -0
- package/dist/core/kuzu/schema.js +380 -0
- package/dist/core/search/bm25-index.d.ts +22 -0
- package/dist/core/search/bm25-index.js +52 -0
- package/dist/core/search/hybrid-search.d.ts +49 -0
- package/dist/core/search/hybrid-search.js +118 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +4 -0
- package/dist/core/tree-sitter/parser-loader.js +42 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +3 -0
- package/dist/mcp/core/embedder.d.ts +27 -0
- package/dist/mcp/core/embedder.js +93 -0
- package/dist/mcp/core/kuzu-adapter.d.ts +23 -0
- package/dist/mcp/core/kuzu-adapter.js +62 -0
- package/dist/mcp/local/local-backend.d.ts +73 -0
- package/dist/mcp/local/local-backend.js +752 -0
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +279 -0
- package/dist/mcp/server.d.ts +12 -0
- package/dist/mcp/server.js +130 -0
- package/dist/mcp/staleness.d.ts +15 -0
- package/dist/mcp/staleness.js +29 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +160 -0
- package/dist/server/api.d.ts +6 -0
- package/dist/server/api.js +156 -0
- package/dist/storage/git.d.ts +7 -0
- package/dist/storage/git.js +39 -0
- package/dist/storage/repo-manager.d.ts +61 -0
- package/dist/storage/repo-manager.js +106 -0
- package/dist/types/pipeline.d.ts +28 -0
- package/dist/types/pipeline.js +16 -0
- package/package.json +80 -0
- package/skills/debugging.md +104 -0
- package/skills/exploring.md +112 -0
- package/skills/impact-analysis.md +114 -0
- package/skills/refactoring.md +119 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# GitNexus
|
|
2
|
+
|
|
3
|
+
**Graph-powered code intelligence for AI agents.** Index any codebase into a knowledge graph, then query it via MCP or CLI.
|
|
4
|
+
|
|
5
|
+
Works with **Cursor**, **Claude Code**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/gitnexus)
|
|
8
|
+
[](https://polyformproject.org/licenses/noncommercial/1.0.0/)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why?
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
**One command to index. One MCP connection to give your AI agent full codebase awareness.**
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Index your repository
|
|
22
|
+
npx gitnexus analyze
|
|
23
|
+
|
|
24
|
+
# Start MCP server (for Cursor, Claude Code, etc.)
|
|
25
|
+
npx gitnexus mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install globally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g gitnexus
|
|
32
|
+
gitnexus analyze
|
|
33
|
+
gitnexus mcp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## MCP Setup
|
|
37
|
+
|
|
38
|
+
### Cursor / Windsurf
|
|
39
|
+
|
|
40
|
+
Add to your MCP config (`.cursor/mcp.json` or equivalent):
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"gitnexus": {
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "gitnexus", "mcp"],
|
|
48
|
+
"env": {
|
|
49
|
+
"GITNEXUS_CWD": "/path/to/your/repo"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Claude Code
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
claude mcp add gitnexus -- npx -y gitnexus mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### OpenCode
|
|
63
|
+
|
|
64
|
+
Add to your `opencode.json`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcp": {
|
|
69
|
+
"gitnexus": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "gitnexus", "mcp"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## What It Does
|
|
78
|
+
|
|
79
|
+
GitNexus indexes your codebase through 7 phases:
|
|
80
|
+
|
|
81
|
+
1. **Structure** — File/folder tree
|
|
82
|
+
2. **Parse** — AST extraction via Tree-sitter (9 languages)
|
|
83
|
+
3. **Imports** — Resolve import paths (including TS path aliases, Rust modules, Java wildcards, Go packages)
|
|
84
|
+
4. **Calls** — Function call resolution with confidence scoring (0.3-0.9)
|
|
85
|
+
5. **Heritage** — Class extends/implements chains
|
|
86
|
+
6. **Communities** — Leiden algorithm clusters related code into functional groups
|
|
87
|
+
7. **Processes** — Entry point detection and execution flow tracing
|
|
88
|
+
|
|
89
|
+
The result is a **KuzuDB graph database** stored locally in `.gitnexus/` with full-text search and semantic embeddings.
|
|
90
|
+
|
|
91
|
+
## MCP Tools
|
|
92
|
+
|
|
93
|
+
Your AI agent gets these tools automatically:
|
|
94
|
+
|
|
95
|
+
| Tool | What It Does | Example |
|
|
96
|
+
|------|-------------|---------|
|
|
97
|
+
| `search` | Hybrid search (BM25 + semantic) with cluster context | "Find authentication logic" |
|
|
98
|
+
| `overview` | List all clusters and processes | "Show me the codebase structure" |
|
|
99
|
+
| `explore` | Deep dive on a symbol, cluster, or process | "Tell me about UserService" |
|
|
100
|
+
| `impact` | Blast radius analysis | "What breaks if I change validateUser?" |
|
|
101
|
+
| `cypher` | Raw Cypher graph queries | Complex relationship queries |
|
|
102
|
+
| `read` | Smart file reader with fuzzy path matching | Read specific files |
|
|
103
|
+
| `grep` | Regex pattern search | Find TODOs, error codes |
|
|
104
|
+
|
|
105
|
+
## MCP Resources
|
|
106
|
+
|
|
107
|
+
| Resource | Purpose |
|
|
108
|
+
|----------|---------|
|
|
109
|
+
| `gitnexus://context` | Codebase stats and overview |
|
|
110
|
+
| `gitnexus://clusters` | All functional clusters |
|
|
111
|
+
| `gitnexus://cluster/{name}` | Cluster members and details |
|
|
112
|
+
| `gitnexus://processes` | All execution flows |
|
|
113
|
+
| `gitnexus://process/{name}` | Full process trace |
|
|
114
|
+
| `gitnexus://schema` | Graph schema for Cypher queries |
|
|
115
|
+
|
|
116
|
+
## CLI Commands
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
gitnexus analyze [path] # Index a repository (or update stale index)
|
|
120
|
+
gitnexus analyze --force # Force full re-index
|
|
121
|
+
gitnexus mcp # Start MCP server (stdio)
|
|
122
|
+
gitnexus serve # Start HTTP server for web UI
|
|
123
|
+
gitnexus list # List all indexed repositories
|
|
124
|
+
gitnexus status # Show index status for current repo
|
|
125
|
+
gitnexus clean # Delete index for current repo
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Supported Languages
|
|
129
|
+
|
|
130
|
+
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust
|
|
131
|
+
|
|
132
|
+
## How Impact Analysis Works
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
gitnexus_impact("UserService", "upstream")
|
|
136
|
+
|
|
137
|
+
TARGET: Class UserService (src/services/user.ts)
|
|
138
|
+
|
|
139
|
+
UPSTREAM (what depends on this):
|
|
140
|
+
Depth 1 (direct callers):
|
|
141
|
+
handleLogin [CALLS 90%] → src/api/auth.ts:45
|
|
142
|
+
handleRegister [CALLS 90%] → src/api/auth.ts:78
|
|
143
|
+
Depth 2:
|
|
144
|
+
authRouter [IMPORTS] → src/routes/auth.ts
|
|
145
|
+
|
|
146
|
+
8 files affected, 3 clusters touched
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Options: `maxDepth`, `minConfidence`, `relationTypes`, `includeTests`
|
|
150
|
+
|
|
151
|
+
## Agent Skills
|
|
152
|
+
|
|
153
|
+
GitNexus ships with skill files that teach AI agents how to use the tools effectively:
|
|
154
|
+
|
|
155
|
+
- **Exploring** — Navigate unfamiliar code using the knowledge graph
|
|
156
|
+
- **Debugging** — Trace bugs through call chains
|
|
157
|
+
- **Impact Analysis** — Analyze blast radius before changes
|
|
158
|
+
- **Refactoring** — Plan safe refactors using dependency mapping
|
|
159
|
+
|
|
160
|
+
These are installed automatically to `.claude/skills/` when you run `gitnexus analyze`.
|
|
161
|
+
|
|
162
|
+
## Requirements
|
|
163
|
+
|
|
164
|
+
- Node.js >= 18
|
|
165
|
+
- Git repository (uses git for commit tracking)
|
|
166
|
+
|
|
167
|
+
## Privacy
|
|
168
|
+
|
|
169
|
+
- All processing happens locally on your machine
|
|
170
|
+
- No code is sent to any server
|
|
171
|
+
- Index stored in `.gitnexus/` inside your repo (gitignored)
|
|
172
|
+
|
|
173
|
+
## Web UI
|
|
174
|
+
|
|
175
|
+
GitNexus also has a browser-based UI at [gitnexus.vercel.app](https://gitnexus.vercel.app) — 100% client-side, your code never leaves the browser.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
[PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/)
|
|
180
|
+
|
|
181
|
+
Free for non-commercial use. Contact for commercial licensing.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Context Generator
|
|
3
|
+
*
|
|
4
|
+
* Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
|
|
5
|
+
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Cline, etc.
|
|
6
|
+
* CLAUDE.md is for Claude Code which only reads that file.
|
|
7
|
+
*/
|
|
8
|
+
interface RepoStats {
|
|
9
|
+
files?: number;
|
|
10
|
+
nodes?: number;
|
|
11
|
+
edges?: number;
|
|
12
|
+
communities?: number;
|
|
13
|
+
processes?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generate AI context files after indexing
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats): Promise<{
|
|
19
|
+
files: string[];
|
|
20
|
+
}>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Context Generator
|
|
3
|
+
*
|
|
4
|
+
* Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
|
|
5
|
+
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Cline, etc.
|
|
6
|
+
* CLAUDE.md is for Claude Code which only reads that file.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
// ESM equivalent of __dirname
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const GITNEXUS_START_MARKER = '<!-- gitnexus:start -->';
|
|
15
|
+
const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
|
|
16
|
+
/**
|
|
17
|
+
* Generate the full GitNexus context content (resources-first approach)
|
|
18
|
+
*/
|
|
19
|
+
function generateGitNexusContent(projectName, stats) {
|
|
20
|
+
return `${GITNEXUS_START_MARKER}
|
|
21
|
+
# GitNexus MCP
|
|
22
|
+
|
|
23
|
+
This project is indexed by GitNexus, providing AI agents with deep code intelligence.
|
|
24
|
+
|
|
25
|
+
## Project: ${projectName}
|
|
26
|
+
|
|
27
|
+
| Metric | Count |
|
|
28
|
+
|--------|-------|
|
|
29
|
+
| Files | ${stats.files || 0} |
|
|
30
|
+
| Symbols | ${stats.nodes || 0} |
|
|
31
|
+
| Relationships | ${stats.edges || 0} |
|
|
32
|
+
| Communities | ${stats.communities || 0} |
|
|
33
|
+
| Processes | ${stats.processes || 0} |
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
\`\`\`
|
|
38
|
+
1. READ gitnexus://context → Get codebase overview (~150 tokens)
|
|
39
|
+
2. READ gitnexus://clusters → See all functional clusters
|
|
40
|
+
3. READ gitnexus://cluster/{name} → Deep dive on specific cluster
|
|
41
|
+
4. gitnexus_search(query) → Find code by query
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
## Available Resources
|
|
45
|
+
|
|
46
|
+
| Resource | Purpose |
|
|
47
|
+
|----------|---------|
|
|
48
|
+
| \`gitnexus://context\` | Codebase stats, tools, and resources overview |
|
|
49
|
+
| \`gitnexus://clusters\` | All clusters with symbol counts and cohesion |
|
|
50
|
+
| \`gitnexus://cluster/{name}\` | Cluster members and details |
|
|
51
|
+
| \`gitnexus://processes\` | All execution flows with types |
|
|
52
|
+
| \`gitnexus://process/{name}\` | Full process trace with steps |
|
|
53
|
+
| \`gitnexus://schema\` | Graph schema for Cypher queries |
|
|
54
|
+
|
|
55
|
+
## Available Tools
|
|
56
|
+
|
|
57
|
+
| Tool | Purpose | When to Use |
|
|
58
|
+
|------|---------|-------------|
|
|
59
|
+
| \`search\` | Semantic + keyword search | Finding code by query |
|
|
60
|
+
| \`overview\` | List clusters & processes | Understanding architecture |
|
|
61
|
+
| \`explore\` | Deep dive on symbol/cluster/process | Detailed investigation |
|
|
62
|
+
| \`impact\` | Blast radius analysis | Before making changes |
|
|
63
|
+
| \`cypher\` | Raw graph queries | Complex analysis |
|
|
64
|
+
|
|
65
|
+
## Workflow Examples
|
|
66
|
+
|
|
67
|
+
### Exploring the Codebase
|
|
68
|
+
\`\`\`
|
|
69
|
+
READ gitnexus://context → Stats and overview
|
|
70
|
+
READ gitnexus://clusters → Find relevant cluster
|
|
71
|
+
READ gitnexus://cluster/Auth → Explore Auth cluster
|
|
72
|
+
gitnexus_explore("validateUser", "symbol") → Detailed symbol info
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
### Planning a Change
|
|
76
|
+
\`\`\`
|
|
77
|
+
gitnexus_impact("UserService", "upstream") → See what breaks
|
|
78
|
+
READ gitnexus://processes → Check affected flows
|
|
79
|
+
gitnexus_explore("LoginFlow", "process") → Trace execution
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
## Graph Schema
|
|
83
|
+
|
|
84
|
+
**Nodes:** File, Function, Class, Interface, Method, Community, Process
|
|
85
|
+
|
|
86
|
+
**Relationships:** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS
|
|
87
|
+
|
|
88
|
+
\`\`\`cypher
|
|
89
|
+
// Example: Find callers of a function
|
|
90
|
+
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
|
|
91
|
+
RETURN caller.name, caller.filePath
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
${GITNEXUS_END_MARKER}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if a file exists
|
|
98
|
+
*/
|
|
99
|
+
async function fileExists(filePath) {
|
|
100
|
+
try {
|
|
101
|
+
await fs.access(filePath);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Create or update GitNexus section in a file
|
|
110
|
+
* - If file doesn't exist: create with GitNexus content
|
|
111
|
+
* - If file exists without GitNexus section: append
|
|
112
|
+
* - If file exists with GitNexus section: replace that section
|
|
113
|
+
*/
|
|
114
|
+
async function upsertGitNexusSection(filePath, content) {
|
|
115
|
+
const exists = await fileExists(filePath);
|
|
116
|
+
if (!exists) {
|
|
117
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
118
|
+
return 'created';
|
|
119
|
+
}
|
|
120
|
+
const existingContent = await fs.readFile(filePath, 'utf-8');
|
|
121
|
+
// Check if GitNexus section already exists
|
|
122
|
+
const startIdx = existingContent.indexOf(GITNEXUS_START_MARKER);
|
|
123
|
+
const endIdx = existingContent.indexOf(GITNEXUS_END_MARKER);
|
|
124
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
125
|
+
// Replace existing section
|
|
126
|
+
const before = existingContent.substring(0, startIdx);
|
|
127
|
+
const after = existingContent.substring(endIdx + GITNEXUS_END_MARKER.length);
|
|
128
|
+
const newContent = before + content + after;
|
|
129
|
+
await fs.writeFile(filePath, newContent.trim() + '\n', 'utf-8');
|
|
130
|
+
return 'updated';
|
|
131
|
+
}
|
|
132
|
+
// Append new section
|
|
133
|
+
const newContent = existingContent.trim() + '\n\n' + content + '\n';
|
|
134
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
135
|
+
return 'appended';
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Install GitNexus skills to .claude/skills/gitnexus/
|
|
139
|
+
* Works natively with Claude Code, Cursor, and GitHub Copilot
|
|
140
|
+
*/
|
|
141
|
+
async function installSkills(repoPath) {
|
|
142
|
+
const skillsDir = path.join(repoPath, '.claude', 'skills', 'gitnexus');
|
|
143
|
+
const installedSkills = [];
|
|
144
|
+
// Skill definitions bundled with the package
|
|
145
|
+
const skills = [
|
|
146
|
+
{
|
|
147
|
+
name: 'exploring',
|
|
148
|
+
description: 'Navigate unfamiliar code using GitNexus knowledge graph',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'debugging',
|
|
152
|
+
description: 'Trace bugs through call chains using knowledge graph',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'impact-analysis',
|
|
156
|
+
description: 'Analyze blast radius before making code changes',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'refactoring',
|
|
160
|
+
description: 'Plan safe refactors using blast radius and dependency mapping',
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
for (const skill of skills) {
|
|
164
|
+
const skillDir = path.join(skillsDir, skill.name);
|
|
165
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
166
|
+
try {
|
|
167
|
+
// Create skill directory
|
|
168
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
169
|
+
// Try to read from package skills directory
|
|
170
|
+
const packageSkillPath = path.join(__dirname, '..', '..', 'skills', `${skill.name}.md`);
|
|
171
|
+
let skillContent;
|
|
172
|
+
try {
|
|
173
|
+
skillContent = await fs.readFile(packageSkillPath, 'utf-8');
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Fallback: generate minimal skill content
|
|
177
|
+
skillContent = `---
|
|
178
|
+
name: gitnexus-${skill.name}
|
|
179
|
+
description: ${skill.description}
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
# ${skill.name.charAt(0).toUpperCase() + skill.name.slice(1)}
|
|
183
|
+
|
|
184
|
+
${skill.description}
|
|
185
|
+
|
|
186
|
+
Use GitNexus tools to accomplish this task.
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
await fs.writeFile(skillPath, skillContent, 'utf-8');
|
|
190
|
+
installedSkills.push(skill.name);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
// Skip on error, don't fail the whole process
|
|
194
|
+
console.warn(`Warning: Could not install skill ${skill.name}:`, err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return installedSkills;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Generate AI context files after indexing
|
|
201
|
+
*/
|
|
202
|
+
export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats) {
|
|
203
|
+
const content = generateGitNexusContent(projectName, stats);
|
|
204
|
+
const createdFiles = [];
|
|
205
|
+
// Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Cline, etc.)
|
|
206
|
+
const agentsPath = path.join(repoPath, 'AGENTS.md');
|
|
207
|
+
const agentsResult = await upsertGitNexusSection(agentsPath, content);
|
|
208
|
+
createdFiles.push(`AGENTS.md (${agentsResult})`);
|
|
209
|
+
// Create CLAUDE.md (for Claude Code)
|
|
210
|
+
const claudePath = path.join(repoPath, 'CLAUDE.md');
|
|
211
|
+
const claudeResult = await upsertGitNexusSection(claudePath, content);
|
|
212
|
+
createdFiles.push(`CLAUDE.md (${claudeResult})`);
|
|
213
|
+
// Install skills to .claude/skills/gitnexus/
|
|
214
|
+
const installedSkills = await installSkills(repoPath);
|
|
215
|
+
if (installedSkills.length > 0) {
|
|
216
|
+
createdFiles.push(`.claude/skills/gitnexus/ (${installedSkills.length} skills)`);
|
|
217
|
+
}
|
|
218
|
+
return { files: createdFiles };
|
|
219
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Command
|
|
3
|
+
*
|
|
4
|
+
* Indexes a repository and stores the knowledge graph in .gitnexus/
|
|
5
|
+
*/
|
|
6
|
+
export interface AnalyzeOptions {
|
|
7
|
+
force?: boolean;
|
|
8
|
+
skipEmbeddings?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const analyzeCommand: (inputPath?: string, options?: AnalyzeOptions) => Promise<void>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Command
|
|
3
|
+
*
|
|
4
|
+
* Indexes a repository and stores the knowledge graph in .gitnexus/
|
|
5
|
+
*/
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
|
9
|
+
import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex } from '../core/kuzu/kuzu-adapter.js';
|
|
10
|
+
import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js';
|
|
11
|
+
import { getStoragePaths, saveMeta, loadMeta, addToGitignore } from '../storage/repo-manager.js';
|
|
12
|
+
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
|
13
|
+
import { generateAIContextFiles } from './ai-context.js';
|
|
14
|
+
export const analyzeCommand = async (inputPath, options) => {
|
|
15
|
+
const spinner = ora('Checking repository...').start();
|
|
16
|
+
// If path provided, use it directly. Otherwise, find git root from cwd.
|
|
17
|
+
let repoPath;
|
|
18
|
+
if (inputPath) {
|
|
19
|
+
repoPath = path.resolve(inputPath);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const gitRoot = getGitRoot(process.cwd());
|
|
23
|
+
if (!gitRoot) {
|
|
24
|
+
spinner.fail('Not inside a git repository');
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
repoPath = gitRoot;
|
|
29
|
+
}
|
|
30
|
+
if (!isGitRepo(repoPath)) {
|
|
31
|
+
spinner.fail('Not a git repository');
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { storagePath, kuzuPath } = getStoragePaths(repoPath);
|
|
36
|
+
const currentCommit = getCurrentCommit(repoPath);
|
|
37
|
+
const existingMeta = await loadMeta(storagePath);
|
|
38
|
+
// Skip if already indexed at same commit
|
|
39
|
+
if (existingMeta && !options?.force && existingMeta.lastCommit === currentCommit) {
|
|
40
|
+
spinner.succeed('Repository already up to date');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Run ingestion pipeline
|
|
44
|
+
spinner.text = 'Running ingestion pipeline...';
|
|
45
|
+
const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
|
|
46
|
+
spinner.text = `${progress.phase}: ${progress.percent}%`;
|
|
47
|
+
});
|
|
48
|
+
// Load graph into KuzuDB
|
|
49
|
+
// Always start fresh - remove existing kuzu DB to avoid stale/corrupt data
|
|
50
|
+
spinner.text = 'Loading graph into KuzuDB...';
|
|
51
|
+
await closeKuzu();
|
|
52
|
+
// Kuzu 0.11 stores databases as: <name> (main file) + <name>.wal (WAL file)
|
|
53
|
+
// BOTH must be deleted or kuzu will find the orphaned WAL and corrupt the database
|
|
54
|
+
const fsClean = await import('fs/promises');
|
|
55
|
+
const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
|
|
56
|
+
for (const f of kuzuFiles) {
|
|
57
|
+
try {
|
|
58
|
+
await fsClean.rm(f, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
catch { /* may not exist */ }
|
|
61
|
+
}
|
|
62
|
+
await initKuzu(kuzuPath);
|
|
63
|
+
await loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath);
|
|
64
|
+
// Create FTS indexes for keyword search
|
|
65
|
+
// Indexes searchable content on: File, Function, Class, Method
|
|
66
|
+
spinner.text = 'Creating FTS indexes...';
|
|
67
|
+
try {
|
|
68
|
+
await createFTSIndex('File', 'file_fts', ['name', 'content']);
|
|
69
|
+
await createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
|
70
|
+
await createFTSIndex('Class', 'class_fts', ['name', 'content']);
|
|
71
|
+
await createFTSIndex('Method', 'method_fts', ['name', 'content']);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// FTS index creation may fail if tables are empty (no data for that type)
|
|
75
|
+
console.error('Note: Some FTS indexes may not have been created:', e.message);
|
|
76
|
+
}
|
|
77
|
+
// Generate embeddings
|
|
78
|
+
if (!options?.skipEmbeddings) {
|
|
79
|
+
spinner.text = 'Generating embeddings...';
|
|
80
|
+
await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
|
|
81
|
+
spinner.text = `Embeddings: ${progress.percent}%`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Save metadata
|
|
85
|
+
const stats = await getKuzuStats();
|
|
86
|
+
await saveMeta(storagePath, {
|
|
87
|
+
repoPath,
|
|
88
|
+
lastCommit: currentCommit,
|
|
89
|
+
indexedAt: new Date().toISOString(),
|
|
90
|
+
stats: {
|
|
91
|
+
files: pipelineResult.fileContents.size,
|
|
92
|
+
nodes: stats.nodes,
|
|
93
|
+
edges: stats.edges,
|
|
94
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
95
|
+
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
// Add .gitnexus to .gitignore
|
|
99
|
+
await addToGitignore(repoPath);
|
|
100
|
+
// Generate AI context files
|
|
101
|
+
const projectName = path.basename(repoPath);
|
|
102
|
+
const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
103
|
+
files: pipelineResult.fileContents.size,
|
|
104
|
+
nodes: stats.nodes,
|
|
105
|
+
edges: stats.edges,
|
|
106
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
107
|
+
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
108
|
+
});
|
|
109
|
+
// Close database
|
|
110
|
+
await closeKuzu();
|
|
111
|
+
spinner.succeed('Repository indexed successfully');
|
|
112
|
+
console.log(` Path: ${repoPath}`);
|
|
113
|
+
console.log(` Storage: ${storagePath}`);
|
|
114
|
+
console.log(` Stats: ${stats.nodes} nodes, ${stats.edges} edges`);
|
|
115
|
+
if (aiContext.files.length > 0) {
|
|
116
|
+
console.log(` AI Context: ${aiContext.files.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean Command
|
|
3
|
+
*
|
|
4
|
+
* Removes the .gitnexus index from the current repository.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import { findRepo } from '../storage/repo-manager.js';
|
|
8
|
+
export const cleanCommand = async (options) => {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const repo = await findRepo(cwd);
|
|
11
|
+
if (!repo) {
|
|
12
|
+
console.log('No indexed repository found in this directory.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const repoName = repo.repoPath.split(/[/\\]/).pop() || repo.repoPath;
|
|
16
|
+
if (!options?.force) {
|
|
17
|
+
console.log(`⚠️ This will delete the GitNexus index for: ${repoName}`);
|
|
18
|
+
console.log(` Path: ${repo.storagePath}`);
|
|
19
|
+
console.log('\nRun with --force to confirm deletion.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await fs.rm(repo.storagePath, { recursive: true, force: true });
|
|
24
|
+
console.log(`🗑️ Deleted: ${repo.storagePath}`);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error('Failed to delete:', err);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { analyzeCommand } from './analyze.js';
|
|
4
|
+
import { serveCommand } from './serve.js';
|
|
5
|
+
import { listCommand } from './list.js';
|
|
6
|
+
import { statusCommand } from './status.js';
|
|
7
|
+
import { mcpCommand } from './mcp.js';
|
|
8
|
+
import { cleanCommand } from './clean.js';
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('gitnexus')
|
|
12
|
+
.description('GitNexus local CLI and MCP server')
|
|
13
|
+
.version('1.0.0');
|
|
14
|
+
program
|
|
15
|
+
.command('analyze [path]')
|
|
16
|
+
.description('Index a repository (full analysis)')
|
|
17
|
+
.option('-f, --force', 'Force full re-index even if up to date')
|
|
18
|
+
.option('--skip-embeddings', 'Skip embedding generation (faster)')
|
|
19
|
+
.action(analyzeCommand);
|
|
20
|
+
program
|
|
21
|
+
.command('serve')
|
|
22
|
+
.description('Start local HTTP server for web UI connection')
|
|
23
|
+
.option('-p, --port <port>', 'Port number', '4747')
|
|
24
|
+
.action(serveCommand);
|
|
25
|
+
program
|
|
26
|
+
.command('mcp')
|
|
27
|
+
.description('Start MCP server (stdio)')
|
|
28
|
+
.action(mcpCommand);
|
|
29
|
+
program
|
|
30
|
+
.command('list')
|
|
31
|
+
.description('List indexed repositories')
|
|
32
|
+
.action(listCommand);
|
|
33
|
+
program
|
|
34
|
+
.command('status')
|
|
35
|
+
.description('Show index status for current repo')
|
|
36
|
+
.action(statusCommand);
|
|
37
|
+
program
|
|
38
|
+
.command('clean')
|
|
39
|
+
.description('Delete GitNexus index for current repo')
|
|
40
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
41
|
+
.action(cleanCommand);
|
|
42
|
+
program.parse(process.argv);
|
package/dist/cli/list.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Command
|
|
3
|
+
*
|
|
4
|
+
* Shows info about the indexed repo in the current directory.
|
|
5
|
+
*/
|
|
6
|
+
import { findRepo } from '../storage/repo-manager.js';
|
|
7
|
+
export const listCommand = async () => {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const repo = await findRepo(cwd);
|
|
10
|
+
if (!repo) {
|
|
11
|
+
console.log('No indexed repository found in this directory.');
|
|
12
|
+
console.log('Run `gitnexus analyze` to index your codebase.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const stats = repo.meta.stats || {};
|
|
16
|
+
const repoName = repo.repoPath.split(/[/\\]/).pop() || repo.repoPath;
|
|
17
|
+
const indexedDate = new Date(repo.meta.indexedAt).toLocaleString();
|
|
18
|
+
console.log(`\n📁 ${repoName}`);
|
|
19
|
+
console.log(` Path: ${repo.repoPath}`);
|
|
20
|
+
console.log(` Indexed: ${indexedDate}`);
|
|
21
|
+
console.log(` Stats: ${stats.files ?? 0} files, ${stats.nodes ?? 0} nodes, ${stats.edges ?? 0} edges`);
|
|
22
|
+
console.log(` Commit: ${repo.meta.lastCommit?.slice(0, 7) || 'unknown'}`);
|
|
23
|
+
if (stats.communities)
|
|
24
|
+
console.log(` Communities: ${stats.communities}`);
|
|
25
|
+
if (stats.processes)
|
|
26
|
+
console.log(` Processes: ${stats.processes}`);
|
|
27
|
+
};
|