Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .claude/hooks/enrich-context.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# enrich-context.sh — PreToolUse hook for Read and Grep tools
# Provides dependency context from codegraph when reading/searching files.
# Always exits 0 (informational only, never blocks).

set -euo pipefail

# Read the tool input from stdin
INPUT=$(cat)

# Extract file path based on tool type
# Read tool uses tool_input.file_path, Grep uses tool_input.path
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)

# Guard: no file path found
if [ -z "$FILE_PATH" ]; then
exit 0
fi

# Guard: codegraph DB must exist
DB_PATH="${CLAUDE_PROJECT_DIR:-.}/.codegraph/graph.db"
if [ ! -f "$DB_PATH" ]; then
exit 0
fi

# Guard: codegraph must be available
if ! command -v codegraph &>/dev/null && ! command -v npx &>/dev/null; then
exit 0
fi

# Convert absolute path to relative (strip project dir prefix)
REL_PATH="$FILE_PATH"
if [[ "$FILE_PATH" == "${CLAUDE_PROJECT_DIR}"* ]]; then
REL_PATH="${FILE_PATH#"${CLAUDE_PROJECT_DIR}"/}"
fi
# Normalize backslashes to forward slashes (Windows compatibility)
REL_PATH="${REL_PATH//\\//}"

# Run codegraph deps and capture output
DEPS=""
if command -v codegraph &>/dev/null; then
DEPS=$(codegraph deps "$REL_PATH" --json -d "$DB_PATH" 2>/dev/null) || true
else
DEPS=$(npx --yes @optave/codegraph deps "$REL_PATH" --json -d "$DB_PATH" 2>/dev/null) || true
fi

# Guard: no output or error
if [ -z "$DEPS" ] || [ "$DEPS" = "null" ]; then
exit 0
fi

# Output as informational context (never deny)
echo "$DEPS" | jq -c '{
hookSpecificOutput: (
"Codegraph context for " + (.file // "unknown") + ":\n" +
" Imports: " + ((.results[0].imports // []) | length | tostring) + " files\n" +
" Imported by: " + ((.results[0].importedBy // []) | length | tostring) + " files\n" +
" Definitions: " + ((.results[0].definitions // []) | length | tostring) + " symbols"
)
}' 2>/dev/null || true

exit 0
20 changes: 20 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@
"timeout": 10
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/enrich-context.sh\"",
"timeout": 5
}
]
},
{
"matcher": "Grep",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/enrich-context.sh\"",
"timeout": 5
}
]
}
]
}
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ JS source is plain JavaScript (ES modules) in `src/`. No transpilation step. The
| `index.js` | Programmatic API exports |
| `builder.js` | Graph building: file collection, parsing, import resolution, incremental hashing |
| `parser.js` | tree-sitter WASM wrapper; `LANGUAGE_REGISTRY` + per-language extractors for functions, classes, methods, imports, exports, call sites |
| `queries.js` | Query functions: symbol search, file deps, impact analysis, diff-impact |
| `queries.js` | Query functions: symbol search, file deps, impact analysis, diff-impact; `SYMBOL_KINDS` constant defines all node kinds |
| `embedder.js` | Semantic search with `@huggingface/transformers`; multi-query RRF ranking |
| `db.js` | SQLite schema and operations (`better-sqlite3`) |
| `mcp.js` | MCP server exposing graph queries to AI agents |
Expand All @@ -60,6 +60,7 @@ JS source is plain JavaScript (ES modules) in `src/`. No transpilation step. The
- Platform-specific prebuilt binaries published as optional npm packages (`@optave/codegraph-{platform}-{arch}`)
- 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
- **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
- **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`
- `@huggingface/transformers` and `@modelcontextprotocol/sdk` are optional dependencies, lazy-loaded
- Non-required parsers (all except JS/TS/TSX) fail gracefully if their WASM grammar is unavailable
- Import resolution uses a 6-level priority system with confidence scoring (import-aware → same-file → directory → parent → global → method hierarchy)
Expand Down
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,27 @@ Most dependency graph tools only tell you which **files** import which — codeg

### Feature comparison

| Capability | codegraph | Madge | dep-cruiser | Skott | Nx graph | Sourcetrail |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| Function-level analysis | **Yes** | — | — | — | — | **Yes** |
| Multi-language | **10** | 1 | 1 | 1 | Any (project) | 4 |
| Semantic search | **Yes** | — | — | — | — | — |
| MCP / AI agent support | **Yes** | — | — | — | — | — |
| Git diff impact | **Yes** | — | — | — | Partial | — |
| Persistent database | **Yes** | — | — | — | — | Yes |
| Watch mode | **Yes** | — | — | — | Daemon | — |
| CI workflow included | **Yes** | — | Rules | — | Yes | — |
| Cycle detection | **Yes** | Yes | Yes | Yes | — | — |
| Zero config | **Yes** | Yes | — | Yes | — | — |
| Fully local / no telemetry | **Yes** | Yes | Yes | Yes | Partial | Yes |
| Free & open source | **Yes** | Yes | Yes | Yes | Partial | Archived |
| Capability | codegraph | Madge | dep-cruiser | Skott | Nx graph | Sourcetrail | GitNexus |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Function-level analysis | **Yes** | — | — | — | — | **Yes** | **Yes** |
| Multi-language | **11** | 1 | 1 | 1 | Any (project) | 4 | 9 |
| Semantic search | **Yes** | — | — | — | — | — | **Yes** |
| MCP / AI agent support | **Yes** | — | — | — | — | — | **Yes** |
| Git diff impact | **Yes** | — | — | — | Partial | — | **Yes** |
| Persistent database | **Yes** | — | — | — | — | Yes | **Yes** |
| Watch mode | **Yes** | — | — | — | Daemon | — | — |
| CI workflow included | **Yes** | — | Rules | — | Yes | — | — |
| Cycle detection | **Yes** | Yes | Yes | Yes | — | — | — |
| Zero config | **Yes** | Yes | — | Yes | — | — | **Yes** |
| Fully local / no telemetry | **Yes** | Yes | Yes | Yes | Partial | Yes | **Yes** |
| Free & open source | **Yes** | Yes | Yes | Yes | Partial | Archived | No |

### What makes codegraph different

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

---

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

Expand Down
16 changes: 15 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Codegraph is a strong local-first code graph CLI. This roadmap describes planned
| Phase | Theme | Key Deliverables | Status |
|-------|-------|-----------------|--------|
| [**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) |
| [**2**](#phase-2--foundation-hardening) | Foundation Hardening | Parser registry, complete MCP, test coverage, enhanced config | **Complete** (v1.4.0) |
| [**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 |
| [**3**](#phase-3--intelligent-embeddings) | Intelligent Embeddings | LLM-generated descriptions, hybrid search | Planned |
| [**4**](#phase-4--natural-language-queries) | Natural Language Queries | `ask` command, conversational sessions | Planned |
| [**5**](#phase-5--expanded-language-support) | Expanded Language Support | 8 new languages (12 → 20), parser utilities | Planned |
Expand Down Expand Up @@ -171,6 +171,20 @@ New configuration options in `.codegraphrc.json`:

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

### 2.5 — Multi-Repo MCP

Support querying multiple codebases from a single MCP server instance.

- Registry file at `~/.codegraph/registry.json` mapping repo names to their `.codegraph/graph.db` paths
- Lazy DB connections — only opened when a repo is first queried
- Add optional `repo` parameter to all MCP tools to target a specific repository
- Auto-registration: `codegraph build` adds the current project to the registry
- New CLI commands: `codegraph registry list|add|remove` for manual management
- Default behavior: when `repo` is omitted, use the local `.codegraph/graph.db` (backwards compatible)

**New files:** `src/registry.js`
**Affected files:** `src/mcp.js`, `src/cli.js`, `src/builder.js`

---

## Phase 3 — Intelligent Embeddings
Expand Down
6 changes: 3 additions & 3 deletions crates/codegraph-core/src/extractors/csharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
let name = node_text(&name_node, source).to_string();
symbols.definitions.push(Definition {
name: name.clone(),
kind: "class".to_string(),
kind: "struct".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand All @@ -65,7 +65,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
let name = node_text(&name_node, source).to_string();
symbols.definitions.push(Definition {
name: name.clone(),
kind: "class".to_string(),
kind: "record".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand Down Expand Up @@ -112,7 +112,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.definitions.push(Definition {
name: node_text(&name_node, source).to_string(),
kind: "class".to_string(),
kind: "enum".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand Down
2 changes: 1 addition & 1 deletion crates/codegraph-core/src/extractors/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
"struct_type" => {
symbols.definitions.push(Definition {
name,
kind: "class".to_string(),
kind: "struct".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand Down
2 changes: 1 addition & 1 deletion crates/codegraph-core/src/extractors/java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.definitions.push(Definition {
name: node_text(&name_node, source).to_string(),
kind: "class".to_string(),
kind: "enum".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand Down
4 changes: 2 additions & 2 deletions crates/codegraph-core/src/extractors/php.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.definitions.push(Definition {
name: node_text(&name_node, source).to_string(),
kind: "interface".to_string(),
kind: "trait".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand All @@ -143,7 +143,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.definitions.push(Definition {
name: node_text(&name_node, source).to_string(),
kind: "class".to_string(),
kind: "enum".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand Down
2 changes: 1 addition & 1 deletion crates/codegraph-core/src/extractors/ruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.definitions.push(Definition {
name: node_text(&name_node, source).to_string(),
kind: "class".to_string(),
kind: "module".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand Down
6 changes: 3 additions & 3 deletions crates/codegraph-core/src/extractors/rust_lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.definitions.push(Definition {
name: node_text(&name_node, source).to_string(),
kind: "class".to_string(),
kind: "struct".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand All @@ -62,7 +62,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(name_node) = node.child_by_field_name("name") {
symbols.definitions.push(Definition {
name: node_text(&name_node, source).to_string(),
kind: "class".to_string(),
kind: "enum".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand All @@ -75,7 +75,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
let trait_name = node_text(&name_node, source).to_string();
symbols.definitions.push(Definition {
name: trait_name.clone(),
kind: "interface".to_string(),
kind: "trait".to_string(),
line: start_line(node),
end_line: Some(end_line(node)),
decorators: None,
Expand Down
4 changes: 2 additions & 2 deletions src/cycles.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export function findCycles(db, opts = {}) {
FROM edges e
JOIN nodes n1 ON e.source_id = n1.id
JOIN nodes n2 ON e.target_id = n2.id
WHERE n1.kind IN ('function', 'method', 'class')
AND n2.kind IN ('function', 'method', 'class')
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
AND e.kind = 'calls'
AND n1.id != n2.id
`)
Expand Down
4 changes: 2 additions & 2 deletions src/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function exportDOT(db, opts = {}) {
FROM edges e
JOIN nodes n1 ON e.source_id = n1.id
JOIN nodes n2 ON e.target_id = n2.id
WHERE n1.kind IN ('function', 'method', 'class') AND n2.kind IN ('function', 'method', 'class')
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
AND e.kind = 'calls'
`)
.all();
Expand Down Expand Up @@ -111,7 +111,7 @@ export function exportMermaid(db, opts = {}) {
FROM edges e
JOIN nodes n1 ON e.source_id = n1.id
JOIN nodes n2 ON e.target_id = n2.id
WHERE n1.kind IN ('function', 'method', 'class') AND n2.kind IN ('function', 'method', 'class')
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
AND e.kind = 'calls'
`)
.all();
Expand Down
2 changes: 1 addition & 1 deletion src/mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const TOOLS = [
{
name: 'list_functions',
description:
'List functions, methods, and classes in the codebase, optionally filtered by file or name pattern',
'List functions, methods, classes, structs, enums, traits, records, and modules in the codebase, optionally filtered by file or name pattern',
inputSchema: {
type: 'object',
properties: {
Expand Down
Loading
Loading