Skip to content

Commit 72535fb

Browse files
feat!: add granular node types, GitNexus comparison, PreToolUse hooks, multi-repo MCP roadmap
- Node types: Go struct→struct, Rust struct/enum/trait, Java enum, C# struct/record/enum, PHP trait/enum, Ruby module — in both WASM and native Rust extractors - Add SYMBOL_KINDS constant and update all kind IN filters across queries, builder, export, embedder, cycles, watcher, and MCP - Add GitNexus column to README comparison table - Add Phase 2.5 Multi-Repo MCP to ROADMAP - Add PreToolUse hooks for Read/Grep context enrichment via codegraph deps - Update CLAUDE.md and README with new node kind documentation BREAKING CHANGE: Node kinds changed for structs, enums, traits, records, and modules. Rebuild with `codegraph build --no-incremental`.
1 parent 51c829e commit 72535fb

File tree

22 files changed

+156
-58
lines changed

22 files changed

+156
-58
lines changed

.claude/hooks/enrich-context.sh

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env bash
2+
# enrich-context.sh — PreToolUse hook for Read and Grep tools
3+
# Provides dependency context from codegraph when reading/searching files.
4+
# Always exits 0 (informational only, never blocks).
5+
6+
set -euo pipefail
7+
8+
# Read the tool input from stdin
9+
INPUT=$(cat)
10+
11+
# Extract file path based on tool type
12+
# Read tool uses tool_input.file_path, Grep uses tool_input.path
13+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
14+
15+
# Guard: no file path found
16+
if [ -z "$FILE_PATH" ]; then
17+
exit 0
18+
fi
19+
20+
# Guard: codegraph DB must exist
21+
DB_PATH="${CLAUDE_PROJECT_DIR:-.}/.codegraph/graph.db"
22+
if [ ! -f "$DB_PATH" ]; then
23+
exit 0
24+
fi
25+
26+
# Guard: codegraph must be available
27+
if ! command -v codegraph &>/dev/null && ! command -v npx &>/dev/null; then
28+
exit 0
29+
fi
30+
31+
# Convert absolute path to relative (strip project dir prefix)
32+
REL_PATH="$FILE_PATH"
33+
if [[ "$FILE_PATH" == "${CLAUDE_PROJECT_DIR}"* ]]; then
34+
REL_PATH="${FILE_PATH#"${CLAUDE_PROJECT_DIR}"/}"
35+
fi
36+
# Normalize backslashes to forward slashes (Windows compatibility)
37+
REL_PATH="${REL_PATH//\\//}"
38+
39+
# Run codegraph deps and capture output
40+
DEPS=""
41+
if command -v codegraph &>/dev/null; then
42+
DEPS=$(codegraph deps "$REL_PATH" --json -d "$DB_PATH" 2>/dev/null) || true
43+
else
44+
DEPS=$(npx --yes @optave/codegraph deps "$REL_PATH" --json -d "$DB_PATH" 2>/dev/null) || true
45+
fi
46+
47+
# Guard: no output or error
48+
if [ -z "$DEPS" ] || [ "$DEPS" = "null" ]; then
49+
exit 0
50+
fi
51+
52+
# Output as informational context (never deny)
53+
echo "$DEPS" | jq -c '{
54+
hookSpecificOutput: (
55+
"Codegraph context for " + (.file // "unknown") + ":\n" +
56+
" Imports: " + ((.results[0].imports // []) | length | tostring) + " files\n" +
57+
" Imported by: " + ((.results[0].importedBy // []) | length | tostring) + " files\n" +
58+
" Definitions: " + ((.results[0].definitions // []) | length | tostring) + " symbols"
59+
)
60+
}' 2>/dev/null || true
61+
62+
exit 0

.claude/settings.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@
1010
"timeout": 10
1111
}
1212
]
13+
},
14+
{
15+
"matcher": "Read",
16+
"hooks": [
17+
{
18+
"type": "command",
19+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/enrich-context.sh\"",
20+
"timeout": 5
21+
}
22+
]
23+
},
24+
{
25+
"matcher": "Grep",
26+
"hooks": [
27+
{
28+
"type": "command",
29+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/enrich-context.sh\"",
30+
"timeout": 5
31+
}
32+
]
1333
}
1434
]
1535
}

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ JS source is plain JavaScript (ES modules) in `src/`. No transpilation step. The
4242
| `index.js` | Programmatic API exports |
4343
| `builder.js` | Graph building: file collection, parsing, import resolution, incremental hashing |
4444
| `parser.js` | tree-sitter WASM wrapper; `LANGUAGE_REGISTRY` + per-language extractors for functions, classes, methods, imports, exports, call sites |
45-
| `queries.js` | Query functions: symbol search, file deps, impact analysis, diff-impact |
45+
| `queries.js` | Query functions: symbol search, file deps, impact analysis, diff-impact; `SYMBOL_KINDS` constant defines all node kinds |
4646
| `embedder.js` | Semantic search with `@huggingface/transformers`; multi-query RRF ranking |
4747
| `db.js` | SQLite schema and operations (`better-sqlite3`) |
4848
| `mcp.js` | MCP server exposing graph queries to AI agents |
@@ -60,6 +60,7 @@ JS source is plain JavaScript (ES modules) in `src/`. No transpilation step. The
6060
- Platform-specific prebuilt binaries published as optional npm packages (`@optave/codegraph-{platform}-{arch}`)
6161
- WASM grammars are built from devDeps on `npm install` (via `prepare` script) and not committed to git — used as fallback when native addon is unavailable
6262
- **Language parser registry:** `LANGUAGE_REGISTRY` in `parser.js` is the single source of truth for all supported languages — maps each language to `{ id, extensions, grammarFile, extractor, required }`. `EXTENSIONS` in `constants.js` is derived from the registry. Adding a new language requires one registry entry + extractor function
63+
- **Node kinds:** `SYMBOL_KINDS` in `queries.js` lists all valid kinds: `function`, `method`, `class`, `interface`, `type`, `struct`, `enum`, `trait`, `record`, `module`. Language-specific types use their native kind (e.g. Go structs → `struct`, Rust traits → `trait`, Ruby modules → `module`) rather than mapping everything to `class`/`interface`
6364
- `@huggingface/transformers` and `@modelcontextprotocol/sdk` are optional dependencies, lazy-loaded
6465
- Non-required parsers (all except JS/TS/TSX) fail gracefully if their WASM grammar is unavailable
6566
- Import resolution uses a 6-level priority system with confidence scoring (import-aware → same-file → directory → parent → global → method hierarchy)

README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,27 +45,27 @@ Most dependency graph tools only tell you which **files** import which — codeg
4545

4646
### Feature comparison
4747

48-
| Capability | codegraph | Madge | dep-cruiser | Skott | Nx graph | Sourcetrail |
49-
|---|:---:|:---:|:---:|:---:|:---:|:---:|
50-
| Function-level analysis | **Yes** ||||| **Yes** |
51-
| Multi-language | **10** | 1 | 1 | 1 | Any (project) | 4 |
52-
| Semantic search | **Yes** ||||||
53-
| MCP / AI agent support | **Yes** ||||||
54-
| Git diff impact | **Yes** |||| Partial ||
55-
| Persistent database | **Yes** ||||| Yes |
56-
| Watch mode | **Yes** |||| Daemon ||
57-
| CI workflow included | **Yes** || Rules || Yes ||
58-
| Cycle detection | **Yes** | Yes | Yes | Yes |||
59-
| Zero config | **Yes** | Yes || Yes |||
60-
| Fully local / no telemetry | **Yes** | Yes | Yes | Yes | Partial | Yes |
61-
| Free & open source | **Yes** | Yes | Yes | Yes | Partial | Archived |
48+
| Capability | codegraph | Madge | dep-cruiser | Skott | Nx graph | Sourcetrail | GitNexus |
49+
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
50+
| Function-level analysis | **Yes** ||||| **Yes** | **Yes** |
51+
| Multi-language | **11** | 1 | 1 | 1 | Any (project) | 4 | 9 |
52+
| Semantic search | **Yes** |||||| **Yes** |
53+
| MCP / AI agent support | **Yes** |||||| **Yes** |
54+
| Git diff impact | **Yes** |||| Partial || **Yes** |
55+
| Persistent database | **Yes** ||||| Yes | **Yes** |
56+
| Watch mode | **Yes** |||| Daemon |||
57+
| CI workflow included | **Yes** || Rules || Yes |||
58+
| Cycle detection | **Yes** | Yes | Yes | Yes ||||
59+
| Zero config | **Yes** | Yes || Yes ||| **Yes** |
60+
| Fully local / no telemetry | **Yes** | Yes | Yes | Yes | Partial | Yes | **Yes** |
61+
| Free & open source | **Yes** | Yes | Yes | Yes | Partial | Archived | No |
6262

6363
### What makes codegraph different
6464

6565
| | Differentiator | In practice |
6666
|---|---|---|
6767
| **🔬** | **Function-level, not just files** | Traces `handleAuth()``validateToken()``decryptJWT()` and shows 14 callers across 9 files break if `decryptJWT` changes |
68-
| **🌐** | **Multi-language, one CLI** | JS/TS + Python + Go + Rust + Java + C# + PHP + Ruby + Terraform in a single graph — no juggling Madge, pyan, and cflow |
68+
| **🌐** | **Multi-language, one CLI** | JS/TS + Python + Go + Rust + Java + C# + PHP + Ruby + HCL in a single graph — no juggling Madge, pyan, and cflow |
6969
| **🤖** | **AI-agent ready** | Built-in [MCP server](https://modelcontextprotocol.io/) — AI assistants query your graph directly via `codegraph fn <name>` |
7070
| **💥** | **Git diff impact** | `codegraph diff-impact` shows changed functions, their callers, and full blast radius — ships with a GitHub Actions workflow |
7171
| **🔒** | **Fully local, zero telemetry** | No accounts, no API keys, no cloud, no data exfiltration — Apache-2.0, free forever |
@@ -88,6 +88,7 @@ Many tools in this space are cloud-based or SaaS — meaning your code leaves yo
8888
| [Understand](https://scitools.com/) | Deep multi-language static analysis | $100+/month per seat, proprietary, GUI-only, no CI or AI integration |
8989
| [Snyk Code](https://snyk.io/) | AI-powered security scanning | Cloud-based — code sent to Snyk servers for analysis, not a dependency graph tool |
9090
| [pyan](https://github.com/Technologicat/pyan) / [cflow](https://www.gnu.org/software/cflow/) | Function-level call graphs | Single-language each (Python / C only), no persistence, no queries |
91+
| [GitNexus](https://gitnexus.dev/) | Function-level graph with hybrid search and MCP | PolyForm Noncommercial license, no watch mode, no cycle detection, no CI workflow |
9192

9293
---
9394

@@ -228,7 +229,7 @@ codegraph mcp # Start MCP server for AI assistants
228229
| `-j, --json` | Output as JSON |
229230
| `-v, --verbose` | Enable debug output |
230231
| `--engine <engine>` | Parser engine: `native`, `wasm`, or `auto` (default: `auto`) |
231-
| `-k, --kind <kind>` | Filter by kind: `function`, `method`, `class` (search) |
232+
| `-k, --kind <kind>` | Filter by kind: `function`, `method`, `class`, `struct`, `enum`, `trait`, `record`, `module` (search) |
232233
| `--file <pattern>` | Filter by file path pattern (search) |
233234
| `--rrf-k <n>` | RRF smoothing constant for multi-query search (default 60) |
234235

ROADMAP.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Codegraph is a strong local-first code graph CLI. This roadmap describes planned
1313
| Phase | Theme | Key Deliverables | Status |
1414
|-------|-------|-----------------|--------|
1515
| [**1**](#phase-1--rust-core) | Rust Core | Rust parsing engine via napi-rs, parallel parsing, incremental tree-sitter, JS orchestration layer | **Complete** (v1.3.0) |
16-
| [**2**](#phase-2--foundation-hardening) | Foundation Hardening | Parser registry, complete MCP, test coverage, enhanced config | **Complete** (v1.4.0) |
16+
| [**2**](#phase-2--foundation-hardening) | Foundation Hardening | Parser registry, complete MCP, test coverage, enhanced config, multi-repo MCP | **Partial** — core complete (v1.4.0), 2.5 planned |
1717
| [**3**](#phase-3--intelligent-embeddings) | Intelligent Embeddings | LLM-generated descriptions, hybrid search | Planned |
1818
| [**4**](#phase-4--natural-language-queries) | Natural Language Queries | `ask` command, conversational sessions | Planned |
1919
| [**5**](#phase-5--expanded-language-support) | Expanded Language Support | 8 new languages (12 → 20), parser utilities | Planned |
@@ -171,6 +171,20 @@ New configuration options in `.codegraphrc.json`:
171171

172172
**Affected files:** `src/config.js`
173173

174+
### 2.5 — Multi-Repo MCP
175+
176+
Support querying multiple codebases from a single MCP server instance.
177+
178+
- Registry file at `~/.codegraph/registry.json` mapping repo names to their `.codegraph/graph.db` paths
179+
- Lazy DB connections — only opened when a repo is first queried
180+
- Add optional `repo` parameter to all MCP tools to target a specific repository
181+
- Auto-registration: `codegraph build` adds the current project to the registry
182+
- New CLI commands: `codegraph registry list|add|remove` for manual management
183+
- Default behavior: when `repo` is omitted, use the local `.codegraph/graph.db` (backwards compatible)
184+
185+
**New files:** `src/registry.js`
186+
**Affected files:** `src/mcp.js`, `src/cli.js`, `src/builder.js`
187+
174188
---
175189

176190
## Phase 3 — Intelligent Embeddings

crates/codegraph-core/src/extractors/csharp.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
5151
let name = node_text(&name_node, source).to_string();
5252
symbols.definitions.push(Definition {
5353
name: name.clone(),
54-
kind: "class".to_string(),
54+
kind: "struct".to_string(),
5555
line: start_line(node),
5656
end_line: Some(end_line(node)),
5757
decorators: None,
@@ -65,7 +65,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
6565
let name = node_text(&name_node, source).to_string();
6666
symbols.definitions.push(Definition {
6767
name: name.clone(),
68-
kind: "class".to_string(),
68+
kind: "record".to_string(),
6969
line: start_line(node),
7070
end_line: Some(end_line(node)),
7171
decorators: None,
@@ -112,7 +112,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
112112
if let Some(name_node) = node.child_by_field_name("name") {
113113
symbols.definitions.push(Definition {
114114
name: node_text(&name_node, source).to_string(),
115-
kind: "class".to_string(),
115+
kind: "enum".to_string(),
116116
line: start_line(node),
117117
end_line: Some(end_line(node)),
118118
decorators: None,

crates/codegraph-core/src/extractors/go.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
7676
"struct_type" => {
7777
symbols.definitions.push(Definition {
7878
name,
79-
kind: "class".to_string(),
79+
kind: "struct".to_string(),
8080
line: start_line(node),
8181
end_line: Some(end_line(node)),
8282
decorators: None,

crates/codegraph-core/src/extractors/java.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
120120
if let Some(name_node) = node.child_by_field_name("name") {
121121
symbols.definitions.push(Definition {
122122
name: node_text(&name_node, source).to_string(),
123-
kind: "class".to_string(),
123+
kind: "enum".to_string(),
124124
line: start_line(node),
125125
end_line: Some(end_line(node)),
126126
decorators: None,

crates/codegraph-core/src/extractors/php.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
131131
if let Some(name_node) = node.child_by_field_name("name") {
132132
symbols.definitions.push(Definition {
133133
name: node_text(&name_node, source).to_string(),
134-
kind: "interface".to_string(),
134+
kind: "trait".to_string(),
135135
line: start_line(node),
136136
end_line: Some(end_line(node)),
137137
decorators: None,
@@ -143,7 +143,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
143143
if let Some(name_node) = node.child_by_field_name("name") {
144144
symbols.definitions.push(Definition {
145145
name: node_text(&name_node, source).to_string(),
146-
kind: "class".to_string(),
146+
kind: "enum".to_string(),
147147
line: start_line(node),
148148
end_line: Some(end_line(node)),
149149
decorators: None,

crates/codegraph-core/src/extractors/ruby.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
5252
if let Some(name_node) = node.child_by_field_name("name") {
5353
symbols.definitions.push(Definition {
5454
name: node_text(&name_node, source).to_string(),
55-
kind: "class".to_string(),
55+
kind: "module".to_string(),
5656
line: start_line(node),
5757
end_line: Some(end_line(node)),
5858
decorators: None,

0 commit comments

Comments
 (0)