Merge origin/main into delphi-support

Integrate main branch changes (WASM grammar architecture, centralized
resolution caches, SQLite adapter) with delphi-support branch. Pascal
grammar is now built as WASM and shipped in src/extraction/wasm/ for
consistency with the WASM-based grammar loading approach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Olaf Monien
2026-02-17 20:23:54 +01:00
co-authored by Claude Opus 4.6
33 changed files with 1266 additions and 958 deletions
+58 -24
View File
@@ -132,8 +132,8 @@ Know exactly what breaks before you change it. Trace callers, callees, and the f
<tr>
<td width="33%" valign="top">
### 🌍 18+ Languages
TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte, Pascal/Delphi—all with the same API.
### 🌍 19+ Languages
TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte, Liquid, Pascal/Delphi—all with the same API.
</td>
<td width="33%" valign="top">
@@ -145,7 +145,7 @@ No data leaves your machine. No API keys. No external services. Everything runs
<td width="33%" valign="top">
### ⚡ Always Fresh
Git hooks automatically sync the index on every commit. Your code intelligence is always up to date.
Claude Code hooks automatically sync the index as you work. Your code intelligence is always up to date.
</td>
</tr>
@@ -165,6 +165,7 @@ The interactive installer will:
- Configure the MCP server in `~/.claude.json`
- Set up auto-allow permissions for CodeGraph tools
- Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph)
- Install Claude Code hooks for automatic index syncing
- Optionally initialize your current project
### 2. Restart Claude Code
@@ -216,7 +217,8 @@ npm install -g @colbymchenry/codegraph
"mcp__codegraph__codegraph_callees",
"mcp__codegraph__codegraph_impact",
"mcp__codegraph__codegraph_node",
"mcp__codegraph__codegraph_status"
"mcp__codegraph__codegraph_status",
"mcp__codegraph__codegraph_files"
]
}
}
@@ -246,6 +248,7 @@ CodeGraph builds a semantic knowledge graph of codebases for faster, smarter cod
| `codegraph_callees` | Find what a function calls |
| `codegraph_impact` | See what's affected by changing a symbol |
| `codegraph_node` | Get details + source code for a symbol |
| `codegraph_files` | Get project file structure from the index |
**When spawning Explore agents in a codegraph-enabled project:**
@@ -279,12 +282,13 @@ At the start of a session, ask the user if they'd like to initialize CodeGraph:
codegraph # Run interactive installer
codegraph install # Run interactive installer (explicit)
codegraph init [path] # Initialize in a project
codegraph uninit [path] # Remove CodeGraph from a project
codegraph index [path] # Full index
codegraph sync [path] # Incremental update
codegraph status [path] # Show statistics
codegraph query <search> # Search symbols
codegraph files [path] # Show project file structure
codegraph context <task> # Build context for AI
codegraph hooks install # Install git auto-sync hook
codegraph serve --mcp # Start MCP server
```
@@ -305,7 +309,8 @@ The installer will:
2. Configure the MCP server in `claude.json`
3. Optionally set up auto-allow permissions
4. Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph)
5. For local installs: initialize and index the current project
5. Install Claude Code hooks for automatic index syncing
6. For local installs: initialize and index the current project
### `codegraph init [path]`
@@ -315,7 +320,15 @@ Initialize CodeGraph in a project directory. Creates a `.codegraph/` directory w
codegraph init # Initialize in current directory
codegraph init /path/to/project # Initialize in specific directory
codegraph init --index # Initialize and immediately index
codegraph init --no-hooks # Skip git hook installation
```
### `codegraph uninit [path]`
Remove CodeGraph from a project. Deletes the `.codegraph/` directory and all indexed data.
```bash
codegraph uninit # Remove from current directory
codegraph uninit --force # Skip confirmation prompt
```
### `codegraph index [path]`
@@ -350,7 +363,6 @@ Output includes:
- Nodes by kind (functions, classes, methods, etc.)
- Files by language
- Pending changes (if any)
- Git hook status
### `codegraph query <search>`
@@ -363,6 +375,21 @@ codegraph query "process" --limit 20 # Limit results
codegraph query "validate" --json # Output as JSON
```
### `codegraph files [path]`
Show the project file structure from the index. Faster than filesystem scanning since it reads from the indexed data.
```bash
codegraph files # Show file tree
codegraph files --format flat # Simple list
codegraph files --format grouped # Group by language
codegraph files --filter src/components # Filter by directory
codegraph files --pattern "*.test.ts" # Filter by glob pattern
codegraph files --max-depth 2 # Limit tree depth
codegraph files --no-metadata # Hide language/symbol counts
codegraph files --json # Output as JSON
```
### `codegraph context <task>`
Build relevant code context for a task. Uses semantic search to find entry points, then expands through the graph to find related code.
@@ -373,16 +400,6 @@ codegraph context "add user authentication" --format json
codegraph context "refactor payment service" --max-nodes 30
```
### `codegraph hooks`
Manage git hooks for automatic syncing.
```bash
codegraph hooks install # Install post-commit hook
codegraph hooks remove # Remove hook
codegraph hooks status # Check if hook is installed
```
### `codegraph serve`
Start CodeGraph as an MCP server for AI assistants.
@@ -438,6 +455,14 @@ Get details about a specific symbol. Use `includeCode: true` only when needed.
codegraph_node(symbol: "authenticate", includeCode: true)
```
### `codegraph_files`
Get the project file structure from the index. Faster than filesystem scanning.
```
codegraph_files(path: "src/components", format: "tree", includeMetadata: true)
```
### `codegraph_status`
Check index health and statistics.
@@ -452,6 +477,7 @@ Claude's **Explore agents** use these tools instead of grep/glob/Read for faster
| Multiple `Read` calls | `codegraph_context(task)` | Related code in one call |
| Manual file tracing | `codegraph_callers/callees` | Call graph traversal |
| Guessing impact | `codegraph_impact(symbol)` | Know what breaks |
| `Glob`/`find` scanning | `codegraph_files(path)` | Indexed file structure |
This hybrid approach gives you **~30% fewer tokens** and **~25% fewer tool calls** while letting Claude's native agents handle synthesis.
@@ -517,7 +543,11 @@ All data is stored in a local SQLite database (`.codegraph/codegraph.db`):
- **nodes** table: All code entities with metadata
- **edges** table: Relationships between nodes
- **files** table: File tracking for incremental updates
- **node_vectors** / **vector_map**: Embeddings for semantic search (using sqlite-vss)
- **unresolved_refs** table: References pending resolution
- **vectors** table: Embeddings stored as BLOBs for semantic search
- **nodes_fts**: FTS5 virtual table for full-text search
- **schema_versions** table: Schema version tracking
- **project_metadata** table: Project-level key-value metadata
### 3. Reference Resolution
@@ -561,7 +591,6 @@ The `.codegraph/config.json` file controls indexing behavior:
```json
{
"version": 1,
"projectName": "my-project",
"languages": ["typescript", "javascript"],
"exclude": [
"node_modules/**",
@@ -569,9 +598,11 @@ The `.codegraph/config.json` file controls indexing behavior:
"build/**",
"*.min.js"
],
"frameworks": ["express", "react"],
"frameworks": [],
"maxFileSize": 1048576,
"gitHooksEnabled": true
"extractDocstrings": true,
"trackCallSites": true,
"enableEmbeddings": false
}
```
@@ -583,7 +614,9 @@ The `.codegraph/config.json` file controls indexing behavior:
| `exclude` | Glob patterns to ignore | `["node_modules/**", ...]` |
| `frameworks` | Framework hints for better resolution | `[]` |
| `maxFileSize` | Skip files larger than this (bytes) | `1048576` (1MB) |
| `gitHooksEnabled` | Enable git hook installation | `true` |
| `extractDocstrings` | Whether to extract docstrings from code | `true` |
| `trackCallSites` | Whether to track call site locations | `true` |
| `enableEmbeddings` | Enable semantic search embeddings | `false` |
## 🌐 Supported Languages
@@ -601,9 +634,10 @@ The `.codegraph/config.json` file controls indexing behavior:
| C | `.c`, `.h` | Full support |
| C++ | `.cpp`, `.hpp`, `.cc` | Full support |
| Swift | `.swift` | Basic support |
| Kotlin | `.kt` | Basic support |
| Kotlin | `.kt`, `.kts` | Basic support |
| Dart | `.dart` | Full support |
| Svelte | `.svelte` | Full support (script extraction, Svelte 5 runes, SvelteKit routes) |
| Liquid | `.liquid` | Full support |
| Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) |
## 🔧 Troubleshooting
+6 -2
View File
@@ -4,16 +4,20 @@
* Tests for the tree-sitter extraction system.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { extractFromSource, scanDirectory, shouldIncludeFile } from '../src/extraction';
import { detectLanguage, isLanguageSupported, getSupportedLanguages } from '../src/extraction/grammars';
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars } from '../src/extraction/grammars';
import { normalizePath } from '../src/utils';
import { DEFAULT_CONFIG } from '../src/types';
beforeAll(async () => {
await initGrammars();
});
// Create a temporary directory for each test
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
+1 -1
View File
@@ -317,7 +317,7 @@ describe('Database Connection', () => {
const version = db.getSchemaVersion();
expect(version).not.toBeNull();
expect(version?.version).toBe(1);
expect(version?.version).toBe(2);
db.close();
});
+25 -23
View File
@@ -13,7 +13,7 @@
* - CLI uninit command
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -24,8 +24,13 @@ import {
getSupportedLanguages,
clearParserCache,
getUnavailableGrammarErrors,
initGrammars,
} from '../src/extraction/grammars';
beforeAll(async () => {
await initGrammars();
});
// Create a temporary directory for each test
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-pr19-test-'));
@@ -320,8 +325,9 @@ describe('Database Layer Improvements', () => {
const { DatabaseConnection } = await import('../src/db');
const { QueryBuilder } = await import('../src/db/queries');
const db = DatabaseConnection.initialize(testDir);
const queries = new QueryBuilder(db.getDatabase());
const dbPath = path.join(testDir, 'codegraph.db');
const db = DatabaseConnection.initialize(dbPath);
const queries = new QueryBuilder(db.getDb());
// Insert a node first (needed as foreign key)
queries.insertNode({
@@ -375,8 +381,9 @@ describe('Database Layer Improvements', () => {
const { DatabaseConnection } = await import('../src/db');
const { QueryBuilder } = await import('../src/db/queries');
const db = DatabaseConnection.initialize(testDir);
const queries = new QueryBuilder(db.getDatabase());
const dbPath = path.join(testDir, 'codegraph.db');
const db = DatabaseConnection.initialize(dbPath);
const queries = new QueryBuilder(db.getDb());
// Insert some nodes
for (let i = 0; i < 3; i++) {
@@ -405,8 +412,9 @@ describe('Database Layer Improvements', () => {
it.skipIf(!HAS_SQLITE)('should set performance pragmas on initialization', async () => {
const { DatabaseConnection } = await import('../src/db');
const db = DatabaseConnection.initialize(testDir);
const rawDb = db.getDatabase();
const dbPath = path.join(testDir, 'codegraph.db');
const db = DatabaseConnection.initialize(dbPath);
const rawDb = db.getDb();
// Check pragmas were set
const synchronous = rawDb.pragma('synchronous', { simple: true });
@@ -428,8 +436,9 @@ describe('Database Layer Improvements', () => {
const { DatabaseConnection } = await import('../src/db');
const { QueryBuilder } = await import('../src/db/queries');
const db = DatabaseConnection.initialize(testDir);
const queries = new QueryBuilder(db.getDatabase());
const dbPath = path.join(testDir, 'codegraph.db');
const db = DatabaseConnection.initialize(dbPath);
const queries = new QueryBuilder(db.getDb());
// Should not throw on empty array
expect(() => queries.insertUnresolvedRefsBatch([])).not.toThrow();
@@ -665,28 +674,21 @@ describe('CLI uninit', () => {
// Tree-sitter Version Pinning
// =============================================================================
describe('Tree-sitter Version Pinning', () => {
it('should have exact versions (no caret) in package.json', () => {
describe('Tree-sitter WASM Setup', () => {
it('should use web-tree-sitter and tree-sitter-wasms in dependencies', () => {
const pkgPath = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
const treeSitterDeps = Object.entries(pkg.dependencies as Record<string, string>)
.filter(([name]) => name.startsWith('tree-sitter') || name.includes('tree-sitter'));
for (const [name, version] of treeSitterDeps) {
// Skip github: references
if (version.startsWith('github:')) continue;
expect(version, `${name} should not use caret range`).not.toMatch(/^\^/);
}
expect(pkg.dependencies['web-tree-sitter']).toBeDefined();
expect(pkg.dependencies['tree-sitter-wasms']).toBeDefined();
});
it('should have tree-sitter override pinned', () => {
it('should not have native tree-sitter in dependencies', () => {
const pkgPath = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
expect(pkg.overrides).toBeDefined();
expect(pkg.overrides['tree-sitter']).toBeDefined();
expect(pkg.overrides['tree-sitter']).not.toMatch(/^\^/);
expect(pkg.dependencies['tree-sitter']).toBeUndefined();
expect(pkg.overrides).toBeUndefined();
});
});
+109
View File
@@ -10,6 +10,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { execFileSync } from 'child_process';
import CodeGraph from '../src/index';
describe('Sync Module', () => {
@@ -150,4 +151,112 @@ describe('Sync Module', () => {
});
});
});
describe('Git-based sync', () => {
let testDir: string;
let cg: CodeGraph;
function git(...args: string[]) {
execFileSync('git', args, { cwd: testDir, stdio: 'pipe' });
}
beforeEach(async () => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-git-sync-'));
// Initialize a git repo with an initial commit
git('init');
git('config', 'user.email', 'test@test.com');
git('config', 'user.name', 'Test');
const srcDir = path.join(testDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(
path.join(srcDir, 'index.ts'),
`export function hello() { return 'world'; }`
);
git('add', '-A');
git('commit', '-m', 'initial');
// Initialize CodeGraph and index
cg = CodeGraph.initSync(testDir, {
config: {
include: ['**/*.ts'],
exclude: [],
},
});
await cg.indexAll();
});
afterEach(() => {
if (cg) {
cg.destroy();
}
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
it('should detect modified files via git', async () => {
fs.writeFileSync(
path.join(testDir, 'src', 'index.ts'),
`export function hello() { return 'modified'; }`
);
const result = await cg.sync();
expect(result.filesModified).toBe(1);
expect(result.changedFilePaths).toContain('src/index.ts');
});
it('should detect new untracked files via git', async () => {
fs.writeFileSync(
path.join(testDir, 'src', 'new.ts'),
`export function newFunc() { return 42; }`
);
const result = await cg.sync();
expect(result.filesAdded).toBe(1);
expect(result.changedFilePaths).toContain('src/new.ts');
// Verify the function was indexed
const nodes = cg.searchNodes('newFunc');
expect(nodes.length).toBeGreaterThan(0);
});
it('should detect deleted files via git', async () => {
fs.unlinkSync(path.join(testDir, 'src', 'index.ts'));
const result = await cg.sync();
expect(result.filesRemoved).toBe(1);
// Verify function is gone
const nodes = cg.searchNodes('hello');
expect(nodes.length).toBe(0);
});
it('should skip files not matching config', async () => {
// Create a .js file which doesn't match **/*.ts
fs.writeFileSync(
path.join(testDir, 'src', 'ignored.js'),
`function ignored() {}`
);
const result = await cg.sync();
expect(result.filesAdded).toBe(0);
expect(result.filesModified).toBe(0);
});
it('should report no changes on clean working tree', async () => {
const result = await cg.sync();
expect(result.filesAdded).toBe(0);
expect(result.filesModified).toBe(0);
expect(result.filesRemoved).toBe(0);
expect(result.changedFilePaths).toBeUndefined();
});
});
});
+33 -554
View File
@@ -1,21 +1,22 @@
{
"name": "@colbymchenry/codegraph",
"version": "0.4.7",
"version": "0.5.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@colbymchenry/codegraph",
"version": "0.4.7",
"version": "0.5.5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@xenova/transformers": "^2.17.0",
"better-sqlite3": "^11.0.0",
"commander": "^14.0.2",
"figlet": "^1.8.0",
"node-sqlite3-wasm": "^0.8.30",
"picomatch": "^4.0.3",
"tree-sitter": "0.22.4"
"tree-sitter-wasms": "^0.1.11",
"web-tree-sitter": "^0.25.3"
},
"bin": {
"codegraph": "dist/bin/codegraph.js"
@@ -32,22 +33,8 @@
"node": ">=18.0.0"
},
"optionalDependencies": {
"@sengac/tree-sitter-dart": "1.1.6",
"sqlite-vss": "^0.1.2",
"tree-sitter-c": "0.23.2",
"tree-sitter-c-sharp": "0.23.1",
"tree-sitter-cpp": "0.23.4",
"tree-sitter-go": "0.23.4",
"tree-sitter-java": "0.23.5",
"tree-sitter-javascript": "0.23.1",
"tree-sitter-kotlin": "0.3.8",
"tree-sitter-pascal": "github:Isopod/tree-sitter-pascal#042119eca2e18a60e56317fb06ee3ba5c32cb447",
"tree-sitter-php": "0.23.11",
"tree-sitter-python": "0.23.4",
"tree-sitter-ruby": "0.23.1",
"tree-sitter-rust": "0.23.1",
"tree-sitter-swift": "0.6.0",
"tree-sitter-typescript": "0.23.2"
"better-sqlite3": "^11.0.0",
"sqlite-vss": "^0.1.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
@@ -871,48 +858,6 @@
"win32"
]
},
"node_modules/@sengac/tree-sitter": {
"version": "0.25.15",
"resolved": "https://registry.npmjs.org/@sengac/tree-sitter/-/tree-sitter-0.25.15.tgz",
"integrity": "sha512-FQlxMNWYYp/tw03qoN9gpUZ3Lrhp1ti/MoG5Gcc4h98PFa6tbvN3qMkPRt4mWhmyKrL3QrOiLxEab8Gj6ZTHbw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
}
},
"node_modules/@sengac/tree-sitter-dart": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@sengac/tree-sitter-dart/-/tree-sitter-dart-1.1.6.tgz",
"integrity": "sha512-lLsF6pVmsC8+JkCnSvRzqa1jJYs+129EOn93MZCsvNnmDrZ2gcEaiqhTj69ttsjQZ2sR+LNxumdphHsw/Ln0Ew==",
"hasInstallScript": true,
"license": "ISC",
"optional": true,
"dependencies": {
"node-addon-api": "^7.1.0",
"node-gyp-build": "^4.8.0"
},
"peerDependencies": {
"@sengac/tree-sitter": "^0.25.10"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
},
"node_modules/@sengac/tree-sitter/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/@types/better-sqlite3": {
"version": "7.6.13",
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
@@ -1227,6 +1172,7 @@
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
@@ -1237,6 +1183,7 @@
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
@@ -1548,7 +1495,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/flatbuffers": {
"version": "1.12.0",
@@ -1627,13 +1575,6 @@
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC",
"optional": true
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@@ -1728,23 +1669,11 @@
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT",
"optional": true
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
"node_modules/node-sqlite3-wasm": {
"version": "0.8.53",
"resolved": "https://registry.npmjs.org/node-sqlite3-wasm/-/node-sqlite3-wasm-0.8.53.tgz",
"integrity": "sha512-HPuGOPj3L+h3WSf0XikIXTDpsRxlVmzBC3RMgqi3yDg9CEbm/4Hw3rrDodeITqITjm07X4atWLlDMMI8KERMiQ==",
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
@@ -2335,461 +2264,13 @@
"node": ">=14.0.0"
}
},
"node_modules/tree-sitter": {
"version": "0.22.4",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz",
"integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"node_modules/tree-sitter-wasms": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.13.tgz",
"integrity": "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ==",
"license": "Unlicense",
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
}
},
"node_modules/tree-sitter-c": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.23.2.tgz",
"integrity": "sha512-9kADOx31AF94DHcrsMGW0zM/2LS6v7wFkPHPVm7RQU+vYVVZMKZ2FJ9e99pm5feqsAcjUzB9CarqDLgRT1Fe/w==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-c-sharp": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/tree-sitter-c-sharp/-/tree-sitter-c-sharp-0.23.1.tgz",
"integrity": "sha512-9zZ4FlcTRWWfRf6f4PgGhG8saPls6qOOt75tDfX7un9vQZJmARjPrAC6yBNCX2T/VKcCjIDbgq0evFaB3iGhQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-c-sharp/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-c/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-cli": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.23.2.tgz",
"integrity": "sha512-kPPXprOqREX+C/FgUp2Qpt9jd0vSwn+hOgjzVv/7hapdoWpa+VeWId53rf4oNNd29ikheF12BYtGD/W90feMbA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"bin": {
"tree-sitter": "cli.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tree-sitter-cpp": {
"version": "0.23.4",
"resolved": "https://registry.npmjs.org/tree-sitter-cpp/-/tree-sitter-cpp-0.23.4.tgz",
"integrity": "sha512-qR5qUDyhZ5jJ6V8/umiBxokRbe89bCGmcq/dk94wI4kN86qfdV8k0GHIUEKaqWgcu42wKal5E97LKpLeVW8sKw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.1",
"node-gyp-build": "^4.8.2",
"tree-sitter-c": "^0.23.1"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-cpp/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-cpp/node_modules/tree-sitter-c": {
"version": "0.23.6",
"resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.23.6.tgz",
"integrity": "sha512-0dxXKznVyUA0s6PjNolJNs2yF87O5aL538A/eR6njA5oqX3C3vH4vnx3QdOKwuUdpKEcFdHuiDpRKLLCA/tjvQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"peerDependencies": {
"tree-sitter": "^0.22.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-go": {
"version": "0.23.4",
"resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.23.4.tgz",
"integrity": "sha512-iQaHEs4yMa/hMo/ZCGqLfG61F0miinULU1fFh+GZreCRtKylFLtvn798ocCZjO2r/ungNZgAY1s1hPFyAwkc7w==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.1",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-go/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-java": {
"version": "0.23.5",
"resolved": "https://registry.npmjs.org/tree-sitter-java/-/tree-sitter-java-0.23.5.tgz",
"integrity": "sha512-Yju7oQ0Xx7GcUT01mUglPP+bYfvqjNCGdxqigTnew9nLGoII42PNVP3bHrYeMxswiCRM0yubWmN5qk+zsg0zMA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-java/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-javascript": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz",
"integrity": "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-javascript/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-kotlin": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz",
"integrity": "sha512-A4obq6bjzmYrA+F0JLLoheFPcofFkctNaZSpnDd+GPn1SfVZLY4/GG4C0cYVBTOShuPBGGAOPLM1JWLZQV4m1g==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^7.1.0",
"node-gyp-build": "^4.8.0"
},
"peerDependencies": {
"tree-sitter": "^0.21.0"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-pascal": {
"version": "0.10.2",
"resolved": "git+ssh://git@github.com/Isopod/tree-sitter-pascal.git#042119eca2e18a60e56317fb06ee3ba5c32cb447",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.8.4"
},
"peerDependencies": {
"tree-sitter": "^0.22.0"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-php": {
"version": "0.23.11",
"resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.11.tgz",
"integrity": "sha512-n+YHSKmYKCyPXsg72rqoUtXyCmNRsG/xe7ExrF2g6bXDERcQ/NPOKIzNfRIcI3f3TtbD6PooA0gMW0EpuuUjVA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-php/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-python": {
"version": "0.23.4",
"resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.23.4.tgz",
"integrity": "sha512-MbmUAl7y5UCUWqHscHke7DdRDwQnVNMNKQYQc4Gq2p09j+fgPxaU8JVsuOI/0HD3BSEEe5k9j3xmdtIWbDtDgw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.1",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-python/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-ruby": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/tree-sitter-ruby/-/tree-sitter-ruby-0.23.1.tgz",
"integrity": "sha512-d9/RXgWjR6HanN7wTYhS5bpBQLz1VkH048Vm3CodPGyJVnamXMGb8oEhDypVCBq4QnHui9sTXuJBBP3WtCw5RA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-ruby/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-rust": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/tree-sitter-rust/-/tree-sitter-rust-0.23.1.tgz",
"integrity": "sha512-wrMptzUAfbl3DbNrldZveyNM2CWmRw2VvEo2j/855qQbMMz4dlCF+TBwRN/1FL1S6cYvAEAJaCMesGqhocFJhQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-rust/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-swift": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.6.0.tgz",
"integrity": "sha512-9vOJZes4/UFjBr4COHtp6ZHVuZYwfChSQbpneXQog04dAstfx5px3ybVX2cN+ylvLqsvVpmXLpidxxgF2rDQ7A==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.0.0",
"node-gyp-build": "^4.8.0",
"tree-sitter-cli": "^0.23",
"which": "2.0.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-swift/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-typescript": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.23.2.tgz",
"integrity": "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2",
"tree-sitter-javascript": "^0.23.1"
},
"peerDependencies": {
"tree-sitter": "^0.21.0"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-typescript/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
"tree-sitter-wasms": "^0.1.11"
}
},
"node_modules/tunnel-agent": {
@@ -2980,20 +2461,18 @@
}
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"optional": true,
"dependencies": {
"isexe": "^2.0.0"
"node_modules/web-tree-sitter": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz",
"integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==",
"license": "MIT",
"peerDependencies": {
"@types/emscripten": "^1.40.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
"peerDependenciesMeta": {
"@types/emscripten": {
"optional": true
}
}
},
"node_modules/why-is-node-running": {
+7 -23
View File
@@ -1,6 +1,6 @@
{
"name": "@colbymchenry/codegraph",
"version": "0.4.7",
"version": "0.5.5",
"description": "Supercharge Claude Code with semantic code intelligence. 30% fewer tokens, 25% fewer tool calls, 100% local.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -15,7 +15,7 @@
"scripts": {
"build": "tsc && npm run copy-assets",
"postinstall": "node scripts/postinstall.js",
"copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql')\"",
"copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql');fs.mkdirSync('dist/extraction/wasm',{recursive:true});fs.readdirSync('src/extraction/wasm').filter(f=>f.endsWith('.wasm')).forEach(f=>fs.copyFileSync('src/extraction/wasm/'+f,'dist/extraction/wasm/'+f))\"",
"dev": "tsc --watch",
"cli": "npm run build && node dist/bin/codegraph.js",
"test": "vitest run",
@@ -34,11 +34,12 @@
"license": "MIT",
"dependencies": {
"@xenova/transformers": "^2.17.0",
"better-sqlite3": "^11.0.0",
"commander": "^14.0.2",
"figlet": "^1.8.0",
"node-sqlite3-wasm": "^0.8.30",
"picomatch": "^4.0.3",
"tree-sitter": "0.22.4"
"tree-sitter-wasms": "^0.1.11",
"web-tree-sitter": "^0.25.3"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.0",
@@ -49,27 +50,10 @@
"vitest": "^2.1.9"
},
"optionalDependencies": {
"@sengac/tree-sitter-dart": "1.1.6",
"tree-sitter-pascal": "github:Isopod/tree-sitter-pascal#042119eca2e18a60e56317fb06ee3ba5c32cb447",
"sqlite-vss": "^0.1.2",
"tree-sitter-c": "0.23.2",
"tree-sitter-c-sharp": "0.23.1",
"tree-sitter-cpp": "0.23.4",
"tree-sitter-go": "0.23.4",
"tree-sitter-java": "0.23.5",
"tree-sitter-javascript": "0.23.1",
"tree-sitter-kotlin": "0.3.8",
"tree-sitter-php": "0.23.11",
"tree-sitter-python": "0.23.4",
"tree-sitter-ruby": "0.23.1",
"tree-sitter-rust": "0.23.1",
"tree-sitter-swift": "0.6.0",
"tree-sitter-typescript": "0.23.2"
"better-sqlite3": "^11.0.0",
"sqlite-vss": "^0.1.2"
},
"engines": {
"node": ">=18.0.0"
},
"overrides": {
"tree-sitter": "0.22.4"
}
}
-3
View File
@@ -62,9 +62,6 @@ async function downloadModel() {
}
}
// @sengac/tree-sitter-dart ships with NAPI prebuilds for all platforms
// No patching needed (replaced old tree-sitter-dart v1.0.0 which used NAN bindings)
downloadModel().catch(() => {
// Silent exit - don't break npm install
process.exit(0);
+62 -23
View File
@@ -25,11 +25,26 @@
import { Command } from 'commander';
import * as path from 'path';
import * as fs from 'fs';
import CodeGraph, { getCodeGraphDir, findNearestCodeGraphRoot } from '../index';
import type { IndexProgress } from '../index';
import { runInstaller } from '../installer';
import { spawn } from 'child_process';
import { getCodeGraphDir, findNearestCodeGraphRoot, isInitialized } from '../directory';
import { initSentry, captureException } from '../sentry';
// Lazy-load heavy modules (CodeGraph, runInstaller) to keep CLI startup fast.
async function loadCodeGraph(): Promise<typeof import('../index')> {
try {
return await import('../index');
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error('\x1b[31m✗\x1b[0m Failed to load CodeGraph modules.');
console.error(`\n Node: ${process.version} Platform: ${process.platform} ${process.arch}`);
console.error(`\n Error: ${msg}`);
console.error('\n Try reinstalling with: npm install -g @colbymchenry/codegraph\n');
process.exit(1);
}
}
type IndexProgress = import('../index').IndexProgress;
// Check if running with no arguments - run installer
// Read version for Sentry release tag
const pkgVersion = (() => {
@@ -40,9 +55,11 @@ const pkgVersion = (() => {
initSentry({ processName: 'codegraph-cli', version: pkgVersion });
if (process.argv.length === 2) {
runInstaller().catch((err) => {
import('../installer').then(({ runInstaller }) =>
runInstaller()
).catch((err) => {
captureException(err);
console.error('Installation failed:', err.message);
console.error('Installation failed:', err instanceof Error ? err.message : String(err));
process.exit(1);
});
} else {
@@ -116,7 +133,7 @@ function resolveProjectPath(pathArg?: string): string {
const absolutePath = path.resolve(pathArg || process.cwd());
// If exact path is initialized (has codegraph.db), use it
if (CodeGraph.isInitialized(absolutePath)) {
if (isInitialized(absolutePath)) {
return absolutePath;
}
@@ -130,7 +147,7 @@ function resolveProjectPath(pathArg?: string): string {
if (parent === current) break;
current = parent;
if (CodeGraph.isInitialized(current)) {
if (isInitialized(current)) {
return current;
}
}
@@ -239,13 +256,14 @@ program
try {
// Check if already initialized
if (CodeGraph.isInitialized(projectPath)) {
if (isInitialized(projectPath)) {
warn(`CodeGraph already initialized in ${projectPath}`);
info('Use "codegraph index" to re-index or "codegraph sync" to update');
return;
}
// Initialize
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.init(projectPath, {
index: false, // We'll handle indexing ourselves for progress
});
@@ -294,7 +312,7 @@ program
const projectPath = resolveProjectPath(pathArg);
try {
if (!CodeGraph.isInitialized(projectPath)) {
if (!isInitialized(projectPath)) {
warn(`CodeGraph is not initialized in ${projectPath}`);
return;
}
@@ -317,6 +335,7 @@ program
}
}
const { default: CodeGraph } = await loadCodeGraph();
const cg = CodeGraph.openSync(projectPath);
cg.uninitialize();
@@ -340,12 +359,13 @@ program
const projectPath = resolveProjectPath(pathArg);
try {
if (!CodeGraph.isInitialized(projectPath)) {
if (!isInitialized(projectPath)) {
error(`CodeGraph not initialized in ${projectPath}`);
info('Run "codegraph init" first');
process.exit(1);
}
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
if (!options.quiet) {
@@ -407,13 +427,14 @@ program
const projectPath = resolveProjectPath(pathArg);
try {
if (!CodeGraph.isInitialized(projectPath)) {
if (!isInitialized(projectPath)) {
if (!options.quiet) {
error(`CodeGraph not initialized in ${projectPath}`);
}
process.exit(1);
}
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
const result = await cg.sync({
@@ -466,7 +487,7 @@ program
const projectPath = resolveProjectPath(pathArg);
try {
if (!CodeGraph.isInitialized(projectPath)) {
if (!isInitialized(projectPath)) {
if (options.json) {
console.log(JSON.stringify({ initialized: false, projectPath }));
return;
@@ -478,6 +499,7 @@ program
return;
}
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
const stats = cg.getStats();
const changes = cg.getChangedFiles();
@@ -578,11 +600,12 @@ program
const projectPath = resolveProjectPath(options.path);
try {
if (!CodeGraph.isInitialized(projectPath)) {
if (!isInitialized(projectPath)) {
error(`CodeGraph not initialized in ${projectPath}`);
process.exit(1);
}
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
const limit = parseInt(options.limit || '10', 10);
@@ -651,11 +674,12 @@ program
const projectPath = resolveProjectPath(options.path);
try {
if (!CodeGraph.isInitialized(projectPath)) {
if (!isInitialized(projectPath)) {
error(`CodeGraph not initialized in ${projectPath}`);
process.exit(1);
}
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
let files = cg.getFiles();
@@ -853,11 +877,12 @@ program
const projectPath = resolveProjectPath(options.path);
try {
if (!CodeGraph.isInitialized(projectPath)) {
if (!isInitialized(projectPath)) {
error(`CodeGraph not initialized in ${projectPath}`);
process.exit(1);
}
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
const context = await cg.buildContext(task, {
@@ -957,8 +982,11 @@ program
/**
* codegraph sync-if-dirty [path]
*
* Syncs the index only if .codegraph/.dirty exists.
* Removes the marker BEFORE syncing so edits during sync
* Checks if .codegraph/.dirty exists and, if so, spawns a detached
* background process to run `codegraph sync`. The hook process exits
* immediately so Claude Code's Stop hook never blocks.
*
* Removes the marker BEFORE spawning so edits during sync
* create a new marker for the next Stop event.
* Runs silently and always exits 0.
*/
@@ -972,7 +1000,7 @@ program
if (!projectRoot) {
process.exit(0);
}
const dirtyPath = path.join(getCodeGraphDir(projectRoot), '.dirty');
const dirtyPath = path.join(getCodeGraphDir(projectRoot!), '.dirty');
// No marker → nothing to do (sub-ms exit)
if (!fs.existsSync(dirtyPath)) {
@@ -983,14 +1011,24 @@ program
try { fs.unlinkSync(dirtyPath); } catch { /* ignore */ }
// If not fully initialized (no DB), exit
if (!CodeGraph.isInitialized(projectRoot)) {
if (!isInitialized(projectRoot!)) {
process.exit(0);
}
// Run sync
const cg = await CodeGraph.open(projectRoot);
await cg.sync();
cg.destroy();
// Spawn `codegraph sync` as a detached background process
// so this hook exits immediately and doesn't block Claude Code
const isWindows = process.platform === 'win32';
const child = spawn(
isWindows ? 'codegraph' : process.argv[0]!,
isWindows ? ['sync', '--quiet', projectRoot!] : [process.argv[1]!, 'sync', '--quiet', projectRoot!],
{
detached: true,
stdio: 'ignore',
windowsHide: true,
shell: isWindows,
}
);
child.unref();
} catch {
// Never fail — this runs at the end of Claude responses
}
@@ -1004,6 +1042,7 @@ program
.command('install')
.description('Run interactive installer for Claude Code integration')
.action(async () => {
const { runInstaller } = await import('../installer');
await runInstaller();
});
+8 -6
View File
@@ -4,20 +4,22 @@
* Handles SQLite database initialization and connection management.
*/
import Database from 'better-sqlite3';
import { SqliteDatabase, createDatabase } from './sqlite-adapter';
import * as fs from 'fs';
import * as path from 'path';
import { SchemaVersion } from '../types';
import { runMigrations, getCurrentVersion, CURRENT_SCHEMA_VERSION } from './migrations';
export { SqliteDatabase, getActiveBackend } from './sqlite-adapter';
/**
* Database connection wrapper with lifecycle management
*/
export class DatabaseConnection {
private db: Database.Database;
private db: SqliteDatabase;
private dbPath: string;
private constructor(db: Database.Database, dbPath: string) {
private constructor(db: SqliteDatabase, dbPath: string) {
this.db = db;
this.dbPath = dbPath;
}
@@ -33,7 +35,7 @@ export class DatabaseConnection {
}
// Create and configure database
const db = new Database(dbPath);
const db = createDatabase(dbPath);
// Enable foreign keys and WAL mode for better performance
db.pragma('foreign_keys = ON');
@@ -71,7 +73,7 @@ export class DatabaseConnection {
throw new Error(`Database not found: ${dbPath}`);
}
const db = new Database(dbPath);
const db = createDatabase(dbPath);
// Enable foreign keys and WAL mode
db.pragma('foreign_keys = ON');
@@ -99,7 +101,7 @@ export class DatabaseConnection {
/**
* Get the underlying database instance
*/
getDb(): Database.Database {
getDb(): SqliteDatabase {
return this.db;
}
+8 -8
View File
@@ -4,7 +4,7 @@
* Schema versioning and migration support.
*/
import Database from 'better-sqlite3';
import { SqliteDatabase } from './sqlite-adapter';
/**
* Current schema version
@@ -17,7 +17,7 @@ export const CURRENT_SCHEMA_VERSION = 2;
interface Migration {
version: number;
description: string;
up: (db: Database.Database) => void;
up: (db: SqliteDatabase) => void;
}
/**
@@ -50,7 +50,7 @@ const migrations: Migration[] = [
/**
* Get the current schema version from the database
*/
export function getCurrentVersion(db: Database.Database): number {
export function getCurrentVersion(db: SqliteDatabase): number {
try {
const row = db
.prepare('SELECT MAX(version) as version FROM schema_versions')
@@ -65,7 +65,7 @@ export function getCurrentVersion(db: Database.Database): number {
/**
* Record a migration as applied
*/
function recordMigration(db: Database.Database, version: number, description: string): void {
function recordMigration(db: SqliteDatabase, version: number, description: string): void {
db.prepare(
'INSERT INTO schema_versions (version, applied_at, description) VALUES (?, ?, ?)'
).run(version, Date.now(), description);
@@ -74,7 +74,7 @@ function recordMigration(db: Database.Database, version: number, description: st
/**
* Run all pending migrations
*/
export function runMigrations(db: Database.Database, fromVersion: number): void {
export function runMigrations(db: SqliteDatabase, fromVersion: number): void {
const pending = migrations.filter((m) => m.version > fromVersion);
if (pending.length === 0) {
@@ -96,7 +96,7 @@ export function runMigrations(db: Database.Database, fromVersion: number): void
/**
* Check if the database needs migration
*/
export function needsMigration(db: Database.Database): boolean {
export function needsMigration(db: SqliteDatabase): boolean {
const current = getCurrentVersion(db);
return current < CURRENT_SCHEMA_VERSION;
}
@@ -104,7 +104,7 @@ export function needsMigration(db: Database.Database): boolean {
/**
* Get list of pending migrations
*/
export function getPendingMigrations(db: Database.Database): Migration[] {
export function getPendingMigrations(db: SqliteDatabase): Migration[] {
const current = getCurrentVersion(db);
return migrations
.filter((m) => m.version > current)
@@ -115,7 +115,7 @@ export function getPendingMigrations(db: Database.Database): Migration[] {
* Get migration history from database
*/
export function getMigrationHistory(
db: Database.Database
db: SqliteDatabase
): Array<{ version: number; appliedAt: number; description: string | null }> {
const rows = db
.prepare('SELECT version, applied_at, description FROM schema_versions ORDER BY version')
+48 -24
View File
@@ -4,7 +4,7 @@
* Prepared statements for CRUD operations on the knowledge graph.
*/
import Database from 'better-sqlite3';
import { SqliteDatabase, SqliteStatement } from './sqlite-adapter';
import {
Node,
Edge,
@@ -143,7 +143,7 @@ function rowToFileRecord(row: FileRow): FileRecord {
* Query builder for the knowledge graph database
*/
export class QueryBuilder {
private db: Database.Database;
private db: SqliteDatabase;
// Node cache for frequently accessed nodes (LRU-style, max 1000 entries)
private nodeCache: Map<string, Node> = new Map();
@@ -151,30 +151,30 @@ export class QueryBuilder {
// Prepared statements (lazily initialized)
private stmts: {
insertNode?: Database.Statement;
updateNode?: Database.Statement;
deleteNode?: Database.Statement;
deleteNodesByFile?: Database.Statement;
getNodeById?: Database.Statement;
getNodesByFile?: Database.Statement;
getNodesByKind?: Database.Statement;
insertEdge?: Database.Statement;
upsertFile?: Database.Statement;
deleteEdgesBySource?: Database.Statement;
deleteEdgesByTarget?: Database.Statement;
getEdgesBySource?: Database.Statement;
getEdgesByTarget?: Database.Statement;
insertFile?: Database.Statement;
updateFile?: Database.Statement;
deleteFile?: Database.Statement;
getFileByPath?: Database.Statement;
getAllFiles?: Database.Statement;
insertUnresolved?: Database.Statement;
deleteUnresolvedByNode?: Database.Statement;
getUnresolvedByName?: Database.Statement;
insertNode?: SqliteStatement;
updateNode?: SqliteStatement;
deleteNode?: SqliteStatement;
deleteNodesByFile?: SqliteStatement;
getNodeById?: SqliteStatement;
getNodesByFile?: SqliteStatement;
getNodesByKind?: SqliteStatement;
insertEdge?: SqliteStatement;
upsertFile?: SqliteStatement;
deleteEdgesBySource?: SqliteStatement;
deleteEdgesByTarget?: SqliteStatement;
getEdgesBySource?: SqliteStatement;
getEdgesByTarget?: SqliteStatement;
insertFile?: SqliteStatement;
updateFile?: SqliteStatement;
deleteFile?: SqliteStatement;
getFileByPath?: SqliteStatement;
getAllFiles?: SqliteStatement;
insertUnresolved?: SqliteStatement;
deleteUnresolvedByNode?: SqliteStatement;
getUnresolvedByName?: SqliteStatement;
} = {};
constructor(db: Database.Database) {
constructor(db: SqliteDatabase) {
this.db = db;
}
@@ -896,6 +896,30 @@ export class QueryBuilder {
}));
}
/**
* Get unresolved references scoped to specific file paths.
* Uses the idx_unresolved_file_path index for efficient lookup.
*/
getUnresolvedReferencesByFiles(filePaths: string[]): UnresolvedReference[] {
if (filePaths.length === 0) return [];
const placeholders = filePaths.map(() => '?').join(',');
const rows = this.db
.prepare(`SELECT * FROM unresolved_refs WHERE file_path IN (${placeholders})`)
.all(...filePaths) as UnresolvedRefRow[];
return rows.map((row) => ({
fromNodeId: row.from_node_id,
referenceName: row.reference_name,
referenceKind: row.reference_kind as EdgeKind,
line: row.line,
column: row.col,
candidates: row.candidates ? safeJsonParse(row.candidates, undefined) : undefined,
filePath: row.file_path,
language: row.language as Language,
}));
}
/**
* Delete all unresolved references (after resolution)
*/
+227
View File
@@ -0,0 +1,227 @@
/**
* SQLite Adapter
*
* Provides a unified interface over better-sqlite3 (native) and
* node-sqlite3-wasm (WASM fallback) for universal cross-platform support.
*/
export interface SqliteStatement {
run(...params: any[]): { changes: number; lastInsertRowid: number | bigint };
get(...params: any[]): any;
all(...params: any[]): any[];
}
export interface SqliteDatabase {
prepare(sql: string): SqliteStatement;
exec(sql: string): void;
pragma(str: string): any;
transaction<T>(fn: (...args: any[]) => T): (...args: any[]) => T;
close(): void;
readonly open: boolean;
}
export type SqliteBackend = 'native' | 'wasm';
let activeBackend: SqliteBackend | null = null;
/**
* Get the currently active SQLite backend.
*/
export function getActiveBackend(): SqliteBackend | null {
return activeBackend;
}
/**
* Translate @named parameters (better-sqlite3 style) to positional ? params
* for node-sqlite3-wasm, which only supports positional binding.
*
* Returns the rewritten SQL and an ordered list of parameter names.
* If no named params are found, returns null for paramOrder (positional mode).
*/
function translateNamedParams(sql: string): { sql: string; paramOrder: string[] | null } {
const paramOrder: string[] = [];
const rewritten = sql.replace(/@(\w+)/g, (_match, name: string) => {
paramOrder.push(name);
return '?';
});
if (paramOrder.length === 0) {
return { sql, paramOrder: null };
}
return { sql: rewritten, paramOrder };
}
/**
* Convert better-sqlite3-style params to a positional array for node-sqlite3-wasm.
*
* Handles three calling conventions:
* - Named object: run({ id: '1', name: 'a' }) → positional array via paramOrder
* - Positional args: run('a', 'b') → ['a', 'b']
* - No args: run() → undefined
*/
function resolveParams(params: any[], paramOrder: string[] | null): any {
if (params.length === 0) return undefined;
// If paramOrder exists and first arg is a plain object, do named→positional translation
if (paramOrder && params.length === 1 && params[0] !== null && typeof params[0] === 'object' && !Array.isArray(params[0]) && !(params[0] instanceof Buffer) && !(params[0] instanceof Uint8Array)) {
const obj = params[0];
return paramOrder.map(name => obj[name]);
}
// Positional: single value or already an array
if (params.length === 1) return params[0];
return params;
}
/**
* Wraps node-sqlite3-wasm to match the better-sqlite3 interface.
*
* Key differences handled:
* - better-sqlite3 uses @named params; node-sqlite3-wasm uses positional ? only
* - better-sqlite3 uses variadic args: stmt.run(a, b, c)
* - node-sqlite3-wasm uses a single array/object: stmt.run([a, b, c])
* - node-sqlite3-wasm has `isOpen` instead of `open`
* - node-sqlite3-wasm doesn't have a `pragma()` method
* - node-sqlite3-wasm doesn't have a `transaction()` method
*/
class WasmDatabaseAdapter implements SqliteDatabase {
private _db: any;
// Track raw WASM statements so we can finalize them on close.
// node-sqlite3-wasm won't release its file lock if statements are left open.
private _openStmts = new Set<any>();
constructor(dbPath: string) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Database } = require('node-sqlite3-wasm');
this._db = new Database(dbPath);
}
get open(): boolean {
return this._db.isOpen;
}
prepare(sql: string): SqliteStatement {
const { sql: rewrittenSql, paramOrder } = translateNamedParams(sql);
const stmt = this._db.prepare(rewrittenSql);
this._openStmts.add(stmt);
return {
run(...params: any[]) {
const resolved = resolveParams(params, paramOrder);
const result = resolved !== undefined ? stmt.run(resolved) : stmt.run();
return {
changes: result?.changes ?? 0,
lastInsertRowid: result?.lastInsertRowid ?? 0,
};
},
get(...params: any[]) {
const resolved = resolveParams(params, paramOrder);
return resolved !== undefined ? stmt.get(resolved) : stmt.get();
},
all(...params: any[]) {
const resolved = resolveParams(params, paramOrder);
return resolved !== undefined ? stmt.all(resolved) : stmt.all();
},
};
}
exec(sql: string): void {
this._db.exec(sql);
}
pragma(str: string): any {
const trimmed = str.trim();
// Write pragma: "key = value"
if (trimmed.includes('=')) {
const eqIdx = trimmed.indexOf('=');
const key = trimmed.substring(0, eqIdx).trim();
const value = trimmed.substring(eqIdx + 1).trim();
// WAL is not supported in WASM SQLite — use DELETE journal mode
if (key === 'journal_mode' && value.toUpperCase() === 'WAL') {
this._db.exec('PRAGMA journal_mode = DELETE');
return;
}
// mmap is not available in WASM — silently skip
if (key === 'mmap_size') {
return;
}
// synchronous = NORMAL is unsafe without WAL — use FULL
if (key === 'synchronous' && value.toUpperCase() === 'NORMAL') {
this._db.exec('PRAGMA synchronous = FULL');
return;
}
this._db.exec(`PRAGMA ${key} = ${value}`);
return;
}
// Read pragma: "key" — return the value
const stmt = this._db.prepare(`PRAGMA ${trimmed}`);
const result = stmt.get();
stmt.finalize();
return result;
}
transaction<T>(fn: (...args: any[]) => T): (...args: any[]) => T {
return (...args: any[]) => {
this._db.exec('BEGIN');
try {
const result = fn(...args);
this._db.exec('COMMIT');
return result;
} catch (error) {
this._db.exec('ROLLBACK');
throw error;
}
};
}
close(): void {
// Finalize all tracked statements before closing.
// node-sqlite3-wasm won't release its directory-based file lock
// if any prepared statements remain open.
for (const stmt of this._openStmts) {
try { stmt.finalize(); } catch { /* already finalized */ }
}
this._openStmts.clear();
this._db.close();
}
}
/**
* Create a database connection. Tries native better-sqlite3 first,
* falls back to node-sqlite3-wasm.
*/
export function createDatabase(dbPath: string): SqliteDatabase {
let nativeError: string | undefined;
let wasmError: string | undefined;
// Try native better-sqlite3 first
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Database = require('better-sqlite3');
const db = new Database(dbPath);
activeBackend = 'native';
return db as SqliteDatabase;
} catch (error) {
nativeError = error instanceof Error ? error.message : String(error);
}
// Fall back to WASM
try {
const db = new WasmDatabaseAdapter(dbPath);
activeBackend = 'wasm';
console.warn('[CodeGraph] Using WASM SQLite backend (native better-sqlite3 unavailable)');
return db;
} catch (error) {
wasmError = error instanceof Error ? error.message : String(error);
}
throw new Error(
`Failed to load any SQLite backend.\n` +
` Native (better-sqlite3): ${nativeError}\n` +
` WASM (node-sqlite3-wasm): ${wasmError}`
);
}
+8 -2
View File
@@ -241,10 +241,16 @@ export function validateDirectory(projectRoot: string): {
return { valid: false, errors };
}
// Check for required files
// Auto-repair missing .gitignore (non-critical file)
const gitignorePath = path.join(codegraphDir, '.gitignore');
if (!fs.existsSync(gitignorePath)) {
errors.push('.gitignore missing in .codegraph directory');
try {
const gitignoreContent = `# CodeGraph data files\n# These are local to each machine and should not be committed\n\n# Database\n*.db\n*.db-wal\n*.db-shm\n\n# Cache\ncache/\n\n# Logs\n*.log\n\n# Hook markers\n.dirty\n`;
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
} catch {
// Non-fatal: warn but don't block
errors.push('.gitignore missing in .codegraph directory and could not be created');
}
}
return {
+66 -107
View File
@@ -1,91 +1,39 @@
/**
* Grammar Loading and Caching
*
* Uses lazy per-language loading so one missing native grammar does not
* break extraction for all other languages.
* Uses web-tree-sitter (WASM) for universal cross-platform support.
* All grammars are pre-loaded asynchronously via initGrammars(), then
* getParser() returns synchronously from cache.
*/
import Parser from 'tree-sitter';
import * as path from 'path';
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
import { Language } from '../types';
type GrammarLoader = () => unknown;
type GrammarLanguage = Exclude<Language, 'svelte' | 'liquid' | 'unknown'>;
/**
* Lazy grammar loaders — each language's native binding is only loaded
* on first use, so a failure in one grammar doesn't affect others.
* WASM filename map — maps each language to its .wasm grammar file
* in the tree-sitter-wasms package.
*/
const grammarLoaders: Record<GrammarLanguage, GrammarLoader> = {
typescript: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-typescript').typescript;
},
tsx: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-typescript').tsx;
},
javascript: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-javascript');
},
jsx: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-javascript');
},
python: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-python');
},
go: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-go');
},
rust: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-rust');
},
java: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-java');
},
c: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-c');
},
cpp: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-cpp');
},
csharp: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-c-sharp');
},
php: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-php').php;
},
ruby: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-ruby');
},
swift: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-swift');
},
kotlin: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-kotlin');
},
dart: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('@sengac/tree-sitter-dart');
},
pascal: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-pascal');
},
// Note: tree-sitter-liquid has ABI compatibility issues with tree-sitter 0.22+
// Liquid extraction is handled separately via regex in tree-sitter.ts
const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
typescript: 'tree-sitter-typescript.wasm',
tsx: 'tree-sitter-tsx.wasm',
javascript: 'tree-sitter-javascript.wasm',
jsx: 'tree-sitter-javascript.wasm',
python: 'tree-sitter-python.wasm',
go: 'tree-sitter-go.wasm',
rust: 'tree-sitter-rust.wasm',
java: 'tree-sitter-java.wasm',
c: 'tree-sitter-c.wasm',
cpp: 'tree-sitter-cpp.wasm',
csharp: 'tree-sitter-c_sharp.wasm',
php: 'tree-sitter-php.wasm',
ruby: 'tree-sitter-ruby.wasm',
swift: 'tree-sitter-swift.wasm',
kotlin: 'tree-sitter-kotlin.wasm',
dart: 'tree-sitter-dart.wasm',
pascal: 'tree-sitter-pascal.wasm',
};
/**
@@ -132,55 +80,65 @@ export const EXTENSION_MAP: Record<string, Language> = {
* Caches for loaded grammars and parsers
*/
const parserCache = new Map<Language, Parser>();
const grammarCache = new Map<Language, unknown | null>();
const languageCache = new Map<Language, WasmLanguage>();
const unavailableGrammarErrors = new Map<Language, string>();
let grammarsInitialized = false;
/**
* Load a grammar on demand, caching the result.
* Returns null if the grammar is not available on this platform.
* Initialize all WASM grammars. Must be called before any parsing.
* Idempotent — safe to call multiple times.
*/
function loadGrammar(language: Language): unknown | null {
if (grammarCache.has(language)) {
return grammarCache.get(language) ?? null;
}
export async function initGrammars(): Promise<void> {
if (grammarsInitialized) return;
const loader = grammarLoaders[language as GrammarLanguage];
if (!loader) {
grammarCache.set(language, null);
return null;
}
await Parser.init();
// Load all grammars in parallel
const entries = Object.entries(WASM_GRAMMAR_FILES) as [GrammarLanguage, string][];
await Promise.allSettled(
entries.map(async ([lang, wasmFile]) => {
try {
const grammar = loader();
if (!grammar) {
throw new Error(`Grammar loader returned empty value for ${language}`);
}
grammarCache.set(language, grammar);
return grammar;
// Pascal ships its own WASM (not in tree-sitter-wasms)
const wasmPath = lang === 'pascal'
? path.join(__dirname, 'wasm', wasmFile)
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
const language = await WasmLanguage.load(wasmPath);
languageCache.set(lang, language);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(`[CodeGraph] Failed to load ${language} grammar — parsing will be unavailable: ${message}`);
unavailableGrammarErrors.set(language, message);
grammarCache.set(language, null);
return null;
console.warn(`[CodeGraph] Failed to load ${lang} grammar — parsing will be unavailable: ${message}`);
unavailableGrammarErrors.set(lang, message);
}
})
);
grammarsInitialized = true;
}
/**
* Get a parser for the specified language
* Check if grammars have been initialized
*/
export function isGrammarsInitialized(): boolean {
return grammarsInitialized;
}
/**
* Get a parser for the specified language.
* Returns synchronously from pre-loaded cache.
*/
export function getParser(language: Language): Parser | null {
if (parserCache.has(language)) {
return parserCache.get(language)!;
}
const grammar = loadGrammar(language);
if (!grammar) {
const lang = languageCache.get(language);
if (!lang) {
return null;
}
const parser = new Parser();
parser.setLanguage(grammar as Parameters<typeof parser.setLanguage>[0]);
parser.setLanguage(lang);
parserCache.set(language, parser);
return parser;
}
@@ -200,15 +158,15 @@ export function isLanguageSupported(language: Language): boolean {
if (language === 'svelte') return true; // custom extractor (script block delegation)
if (language === 'liquid') return true; // custom regex extractor
if (language === 'unknown') return false;
return loadGrammar(language) !== null;
return languageCache.has(language);
}
/**
* Get all currently supported languages.
*/
export function getSupportedLanguages(): Language[] {
const available = (Object.keys(grammarLoaders) as GrammarLanguage[])
.filter((language) => loadGrammar(language) !== null);
const available = (Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[])
.filter((language) => languageCache.has(language));
return [...available, 'svelte', 'liquid'];
}
@@ -217,7 +175,8 @@ export function getSupportedLanguages(): Language[] {
*/
export function clearParserCache(): void {
parserCache.clear();
grammarCache.clear();
// Note: languageCache is NOT cleared — WASM languages persist.
// To fully re-init, set grammarsInitialized = false and call initGrammars() again.
unavailableGrammarErrors.clear();
}
+224 -45
View File
@@ -18,7 +18,7 @@ import {
} from '../types';
import { QueryBuilder } from '../db/queries';
import { extractFromSource } from './tree-sitter';
import { detectLanguage, isLanguageSupported } from './grammars';
import { detectLanguage, isLanguageSupported, initGrammars } from './grammars';
import { logDebug, logWarn } from '../errors';
import { captureException } from '../sentry';
import { validatePathWithinRoot, normalizePath } from '../utils';
@@ -63,6 +63,7 @@ export interface SyncResult {
filesRemoved: number;
nodesUpdated: number;
durationMs: number;
changedFilePaths?: string[];
}
/**
@@ -105,27 +106,80 @@ export function shouldIncludeFile(
}
/**
* Get directories ignored by .gitignore using git ls-files.
* Returns a Set of normalized relative directory paths (forward slashes, no trailing slash).
* Gracefully returns empty Set on any failure.
* Get all files visible to git (tracked + untracked but not ignored).
* Respects .gitignore at all levels (root, subdirectories).
* Returns null on failure (non-git project) so callers can fall back.
*/
function getGitIgnoredDirectories(rootDir: string): Set<string> {
function getGitVisibleFiles(rootDir: string): Set<string> | null {
try {
// -c = cached (tracked), -o = others (untracked), --exclude-standard = respect .gitignore
const output = execFileSync(
'git',
['ls-files', '-co', '--exclude-standard'],
{ cwd: rootDir, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }
);
const files = new Set<string>();
for (const line of output.split('\n')) {
const trimmed = line.trim();
if (trimmed) {
files.add(normalizePath(trimmed));
}
}
return files;
} catch {
return null;
}
}
/**
* Result of git-based change detection.
* Returns null when git is unavailable (non-git project or command failure),
* signaling the caller to fall back to full filesystem scan.
*/
interface GitChanges {
modified: string[]; // M, MM, AM — files to re-hash + re-index
added: string[]; // ?? — new untracked files to index
deleted: string[]; // D — files to remove from DB
}
/**
* Use `git status` to detect changed files instead of scanning every file.
* Returns null on failure so callers fall back to full scan.
*/
function getGitChangedFiles(rootDir: string, config: CodeGraphConfig): GitChanges | null {
try {
const output = execFileSync(
'git',
['ls-files', '-oi', '--exclude-standard', '--directory'],
['status', '--porcelain', '--no-renames'],
{ cwd: rootDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }
);
const dirs = new Set<string>();
const modified: string[] = [];
const added: string[] = [];
const deleted: string[] = [];
for (const line of output.split('\n')) {
const trimmed = line.trim();
if (trimmed.endsWith('/')) {
dirs.add(normalizePath(trimmed.slice(0, -1)));
if (line.length < 4) continue; // Minimum: "XY file"
const statusCode = line.substring(0, 2);
const filePath = normalizePath(line.substring(3));
// Skip files that don't match include/exclude config
if (!shouldIncludeFile(filePath, config)) continue;
if (statusCode === '??') {
added.push(filePath);
} else if (statusCode.includes('D')) {
deleted.push(filePath);
} else {
// M, MM, AM, A (staged), etc. — treat as modified
modified.push(filePath);
}
}
return dirs;
return { modified, added, deleted };
} catch {
return new Set<string>();
return null;
}
}
@@ -135,21 +189,49 @@ function getGitIgnoredDirectories(rootDir: string): Set<string> {
const CODEGRAPH_IGNORE_MARKER = '.codegraphignore';
/**
* Recursively scan directory for source files
* Recursively scan directory for source files.
*
* In git repos, uses `git ls-files` to get the file list (inherently
* respects .gitignore at all levels), then filters by config include patterns.
* Falls back to filesystem walk for non-git projects.
*/
export function scanDirectory(
rootDir: string,
config: CodeGraphConfig,
onProgress?: (current: number, file: string) => void
): string[] {
// Fast path: use git to get all visible files (respects .gitignore everywhere)
const gitFiles = getGitVisibleFiles(rootDir);
if (gitFiles) {
const files: string[] = [];
let count = 0;
for (const filePath of gitFiles) {
if (shouldIncludeFile(filePath, config)) {
files.push(filePath);
count++;
onProgress?.(count, filePath);
}
}
return files;
}
// Fallback: walk filesystem for non-git projects
return scanDirectoryWalk(rootDir, config, onProgress);
}
/**
* Filesystem walk fallback for non-git projects.
*/
function scanDirectoryWalk(
rootDir: string,
config: CodeGraphConfig,
onProgress?: (current: number, file: string) => void
): string[] {
const files: string[] = [];
let count = 0;
// Track visited real paths to detect symlink cycles
const visitedDirs = new Set<string>();
const gitIgnoredDirs = getGitIgnoredDirectories(rootDir);
function walk(dir: string): void {
// Resolve real path to detect symlink cycles
let realDir: string;
try {
realDir = fs.realpathSync(dir);
@@ -164,7 +246,7 @@ export function scanDirectory(
}
visitedDirs.add(realDir);
// Check for .codegraphignore marker file - skip entire directory tree if present
// Check for .codegraphignore marker file
const ignoreMarker = path.join(dir, CODEGRAPH_IGNORE_MARKER);
if (fs.existsSync(ignoreMarker)) {
logDebug('Skipping directory due to .codegraphignore marker', { dir });
@@ -184,17 +266,11 @@ export function scanDirectory(
const fullPath = path.join(dir, entry.name);
const relativePath = normalizePath(path.relative(rootDir, fullPath));
// Follow symlinked directories, but skip symlinked files to non-project targets
if (entry.isSymbolicLink()) {
try {
const realTarget = fs.realpathSync(fullPath);
const stat = fs.statSync(realTarget);
if (stat.isDirectory()) {
// Check gitignore first (fast O(1) lookup)
if (gitIgnoredDirs.has(relativePath)) {
continue;
}
// Check exclusion, then recurse (cycle detection handles the rest)
const dirPattern = relativePath + '/';
let excluded = false;
for (const pattern of config.exclude) {
@@ -210,9 +286,7 @@ export function scanDirectory(
if (shouldIncludeFile(relativePath, config)) {
files.push(relativePath);
count++;
if (onProgress) {
onProgress(count, relativePath);
}
onProgress?.(count, relativePath);
}
}
} catch {
@@ -222,11 +296,6 @@ export function scanDirectory(
}
if (entry.isDirectory()) {
// Check gitignore first (fast O(1) lookup)
if (gitIgnoredDirs.has(relativePath)) {
continue;
}
// Check if directory should be excluded
const dirPattern = relativePath + '/';
let excluded = false;
for (const pattern of config.exclude) {
@@ -242,9 +311,7 @@ export function scanDirectory(
if (shouldIncludeFile(relativePath, config)) {
files.push(relativePath);
count++;
if (onProgress) {
onProgress(count, relativePath);
}
onProgress?.(count, relativePath);
}
}
}
@@ -275,6 +342,7 @@ export class ExtractionOrchestrator {
onProgress?: (progress: IndexProgress) => void,
signal?: AbortSignal
): Promise<IndexResult> {
await initGrammars();
const startTime = Date.now();
const errors: ExtractionError[] = [];
let filesIndexed = 0;
@@ -611,28 +679,85 @@ export class ExtractionOrchestrator {
}
/**
* Sync with current file state
* Sync with current file state.
* Uses git status as a fast path when available, falling back to full scan.
*/
async sync(onProgress?: (progress: IndexProgress) => void): Promise<SyncResult> {
await initGrammars();
const startTime = Date.now();
let filesChecked = 0;
let filesAdded = 0;
let filesModified = 0;
let filesRemoved = 0;
let nodesUpdated = 0;
const changedFilePaths: string[] = [];
// Get current files on disk
onProgress?.({
phase: 'scanning',
current: 0,
total: 0,
});
const filesToIndex: string[] = [];
const gitChanges = getGitChangedFiles(this.rootDir, this.config);
if (gitChanges) {
// === Git fast path ===
// Only inspect the files git reports as changed instead of scanning everything.
filesChecked = gitChanges.modified.length + gitChanges.added.length + gitChanges.deleted.length;
// Handle deleted files
for (const filePath of gitChanges.deleted) {
const tracked = this.queries.getFileByPath(filePath);
if (tracked) {
this.queries.deleteFile(filePath);
filesRemoved++;
}
}
// Handle modified files — read + hash only these files
for (const filePath of gitChanges.modified) {
const fullPath = path.join(this.rootDir, filePath);
let content: string;
try {
content = fs.readFileSync(fullPath, 'utf-8');
} catch (error) {
captureException(error, { operation: 'sync-read-file', filePath });
logDebug('Skipping unreadable file during sync', { filePath, error: String(error) });
continue;
}
const contentHash = hashContent(content);
const tracked = this.queries.getFileByPath(filePath);
if (!tracked) {
filesToIndex.push(filePath);
changedFilePaths.push(filePath);
filesAdded++;
} else if (tracked.contentHash !== contentHash) {
filesToIndex.push(filePath);
changedFilePaths.push(filePath);
filesModified++;
}
}
// Handle added (untracked) files
for (const filePath of gitChanges.added) {
filesToIndex.push(filePath);
changedFilePaths.push(filePath);
filesAdded++;
}
} else {
// === Fallback: full scan (non-git project or git failure) ===
const currentFiles = new Set(scanDirectory(this.rootDir, this.config));
filesChecked = currentFiles.size;
// Get tracked files from database
// Build Map for O(1) lookups instead of .find() per file
const trackedFiles = this.queries.getAllFiles();
const trackedMap = new Map<string, FileRecord>();
for (const f of trackedFiles) {
trackedMap.set(f.path, f);
}
// Find files to remove (in DB but not on disk)
for (const tracked of trackedFiles) {
@@ -643,8 +768,6 @@ export class ExtractionOrchestrator {
}
// Find files to add or update
const filesToIndex: string[] = [];
for (const filePath of currentFiles) {
const fullPath = path.join(this.rootDir, filePath);
let content: string;
@@ -657,18 +780,19 @@ export class ExtractionOrchestrator {
}
const contentHash = hashContent(content);
const tracked = trackedFiles.find((f) => f.path === filePath);
const tracked = trackedMap.get(filePath);
if (!tracked) {
// New file
filesToIndex.push(filePath);
changedFilePaths.push(filePath);
filesAdded++;
} else if (tracked.contentHash !== contentHash) {
// Modified file
filesToIndex.push(filePath);
changedFilePaths.push(filePath);
filesModified++;
}
}
}
// Index changed files
const total = filesToIndex.length;
@@ -692,16 +816,71 @@ export class ExtractionOrchestrator {
filesRemoved,
nodesUpdated,
durationMs: Date.now() - startTime,
changedFilePaths: changedFilePaths.length > 0 ? changedFilePaths : undefined,
};
}
/**
* Get files that have changed since last index
* Get files that have changed since last index.
* Uses git status as a fast path when available, falling back to full scan.
*/
getChangedFiles(): { added: string[]; modified: string[]; removed: string[] } {
const gitChanges = getGitChangedFiles(this.rootDir, this.config);
if (gitChanges) {
// === Git fast path ===
const added: string[] = [];
const modified: string[] = [];
const removed: string[] = [];
// Deleted files — only report if tracked in DB
for (const filePath of gitChanges.deleted) {
const tracked = this.queries.getFileByPath(filePath);
if (tracked) {
removed.push(filePath);
}
}
// Modified files — read + hash only these, compare with DB
for (const filePath of gitChanges.modified) {
const fullPath = path.join(this.rootDir, filePath);
let content: string;
try {
content = fs.readFileSync(fullPath, 'utf-8');
} catch (error) {
captureException(error, { operation: 'detect-changes-read-file', filePath });
logDebug('Skipping unreadable file while detecting changes', { filePath, error: String(error) });
continue;
}
const contentHash = hashContent(content);
const tracked = this.queries.getFileByPath(filePath);
if (!tracked) {
added.push(filePath);
} else if (tracked.contentHash !== contentHash) {
modified.push(filePath);
}
}
// Added (untracked) files
for (const filePath of gitChanges.added) {
added.push(filePath);
}
return { added, modified, removed };
}
// === Fallback: full scan (non-git project or git failure) ===
const currentFiles = new Set(scanDirectory(this.rootDir, this.config));
const trackedFiles = this.queries.getAllFiles();
// Build Map for O(1) lookups
const trackedMap = new Map<string, FileRecord>();
for (const f of trackedFiles) {
trackedMap.set(f.path, f);
}
const added: string[] = [];
const modified: string[] = [];
const removed: string[] = [];
@@ -726,7 +905,7 @@ export class ExtractionOrchestrator {
}
const contentHash = hashContent(content);
const tracked = trackedFiles.find((f) => f.path === filePath);
const tracked = trackedMap.get(filePath);
if (!tracked) {
added.push(filePath);
@@ -741,4 +920,4 @@ export class ExtractionOrchestrator {
// Re-export useful types and functions
export { extractFromSource } from './tree-sitter';
export { detectLanguage, isLanguageSupported, getSupportedLanguages } from './grammars';
export { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars } from './grammars';
+13 -4
View File
@@ -4,7 +4,7 @@
* Handles parsing source code and extracting structural information.
*/
import { SyntaxNode, Tree } from 'tree-sitter';
import { Node as SyntaxNode, Tree } from 'web-tree-sitter';
import * as crypto from 'crypto';
import * as path from 'path';
import {
@@ -934,7 +934,10 @@ export class TreeSitterExtractor {
}
try {
this.tree = parser.parse(this.source);
this.tree = parser.parse(this.source) ?? null;
if (!this.tree) {
throw new Error('Parser returned null tree');
}
// Create file node representing the source file
const fileNode: Node = {
@@ -1775,9 +1778,15 @@ export class TreeSitterExtractor {
if (namespacePrefix && useGroup) {
// Grouped import - create one import per item
const prefix = getNodeText(namespacePrefix, this.source);
const useClauses = useGroup.namedChildren.filter((c: SyntaxNode) => c.type === 'namespace_use_clause');
const useClauses = useGroup.namedChildren.filter((c: SyntaxNode) =>
c.type === 'namespace_use_group_clause' || c.type === 'namespace_use_clause'
);
for (const clause of useClauses) {
const name = clause.namedChildren.find((c: SyntaxNode) => c.type === 'name');
// WASM grammar wraps names in namespace_name; native uses name directly
const nsName = clause.namedChildren.find((c: SyntaxNode) => c.type === 'namespace_name');
const name = nsName
? nsName.namedChildren.find((c: SyntaxNode) => c.type === 'name')
: clause.namedChildren.find((c: SyntaxNode) => c.type === 'name');
if (name) {
const fullPath = `${prefix}\\${getNodeText(name, this.source)}`;
this.createNode('import', fullPath, node, {
Binary file not shown.
+10 -4
View File
@@ -38,6 +38,7 @@ import {
IndexResult,
SyncResult,
extractFromSource,
initGrammars,
} from './extraction';
import {
ReferenceResolver,
@@ -60,7 +61,7 @@ export {
CODEGRAPH_DIR,
} from './directory';
export { IndexProgress, IndexResult, SyncResult } from './extraction';
export { detectLanguage, isLanguageSupported, getSupportedLanguages } from './extraction';
export { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars } from './extraction';
export { ResolutionResult } from './resolution';
export { EmbeddingProgress } from './vectors';
export {
@@ -184,6 +185,7 @@ export class CodeGraph {
* @returns A new CodeGraph instance
*/
static async init(projectRoot: string, options: InitOptions = {}): Promise<CodeGraph> {
await initGrammars();
const resolvedRoot = path.resolve(projectRoot);
// Check if already initialized
@@ -253,6 +255,7 @@ export class CodeGraph {
* @returns A CodeGraph instance
*/
static async open(projectRoot: string, options: OpenOptions = {}): Promise<CodeGraph> {
await initGrammars();
const resolvedRoot = path.resolve(projectRoot);
// Check if initialized
@@ -449,15 +452,18 @@ export class CodeGraph {
// Resolve references if files were updated
if (result.filesAdded > 0 || result.filesModified > 0) {
const unresolvedCount = this.queries.getUnresolvedReferences().length;
// Scope resolution to changed files when available (git fast path)
const unresolvedRefs = result.changedFilePaths
? this.queries.getUnresolvedReferencesByFiles(result.changedFilePaths)
: this.queries.getUnresolvedReferences();
options.onProgress?.({
phase: 'resolving',
current: 0,
total: unresolvedCount,
total: unresolvedRefs.length,
});
this.resolveReferences((current, total) => {
this.resolver.resolveAndPersist(unresolvedRefs, (current, total) => {
options.onProgress?.({
phase: 'resolving',
current,
+3 -2
View File
@@ -113,15 +113,16 @@ export function warn(message: string): void {
/**
* Show the "next steps" section after installation
*/
export function showNextSteps(location: 'global' | 'local'): void {
export function showNextSteps(location: 'global' | 'local', useNpx?: boolean): void {
console.log();
console.log(chalk.bold(' Done!') + ' Restart Claude Code to use CodeGraph.');
console.log();
if (location === 'global') {
const cmd = useNpx ? 'npx @colbymchenry/codegraph' : 'codegraph';
console.log(chalk.dim(' Quick start:'));
console.log(chalk.dim(' cd your-project'));
console.log(chalk.cyan(' codegraph init -i'));
console.log(chalk.cyan(` ${cmd} init -i`));
} else {
console.log(chalk.dim(' CodeGraph is ready to use in this project!'));
}
+15 -4
View File
@@ -97,19 +97,29 @@ function writeJsonFile(filePath: string, data: Record<string, any>): void {
atomicWriteFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
}
/**
* When true, all configs use `npx @colbymchenry/codegraph` instead of the
* bare `codegraph` command. Set by the installer when global install fails.
*/
let useNpxFallback = false;
export function setUseNpxFallback(value: boolean): void {
useNpxFallback = value;
}
/**
* Get the MCP server configuration for the given location
*/
function getMcpServerConfig(location: InstallLocation): Record<string, any> {
if (location === 'global') {
// Global: use 'codegraph' command directly (assumes globally installed)
if (location === 'global' && !useNpxFallback) {
// Global: use 'codegraph' command directly (globally installed and in PATH)
return {
type: 'stdio',
command: 'codegraph',
args: ['serve', '--mcp'],
};
}
// Local: use npx to run the package
// Local or npx fallback: use npx to run the package
return {
type: 'stdio',
command: 'npx',
@@ -212,7 +222,7 @@ export function hasPermissions(location: InstallLocation): boolean {
* Stop sync-if-dirty (sync, ensures fresh index before next user turn)
*/
function getHooksConfig(location: InstallLocation): Record<string, any> {
const command = location === 'global' ? 'codegraph' : 'npx @colbymchenry/codegraph';
const command = (location === 'global' && !useNpxFallback) ? 'codegraph' : 'npx @colbymchenry/codegraph';
return {
PostToolUse: [
@@ -229,6 +239,7 @@ function getHooksConfig(location: InstallLocation): Record<string, any> {
],
Stop: [
{
matcher: '.*',
hooks: [
{
type: 'command',
+24 -5
View File
@@ -8,8 +8,7 @@
import { execSync } from 'child_process';
import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts';
import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
import CodeGraph from '../index';
import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks, setUseNpxFallback } from './config-writer';
/**
* Format a number with commas
@@ -29,7 +28,8 @@ export async function runInstaller(): Promise<void> {
// Step 1: Check if codegraph is available (skip install if already there)
let codegraphAvailable = false;
try {
execSync('which codegraph', { stdio: 'pipe' });
const checkCmd = process.platform === 'win32' ? 'where codegraph' : 'command -v codegraph';
execSync(checkCmd, { stdio: 'pipe' });
codegraphAvailable = true;
} catch {
// Not installed globally yet
@@ -40,13 +40,20 @@ export async function runInstaller(): Promise<void> {
try {
execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
success('Installed codegraph command globally');
codegraphAvailable = true;
} catch {
// May fail if no permissions, but that's ok - npx still works
info('Could not install globally (try with sudo if needed)');
info('Could not install globally — will use npx instead');
info('(MCP server and hooks will use npx @colbymchenry/codegraph)');
}
console.log();
}
// If codegraph binary isn't in PATH, tell config-writer to use npx for everything
if (!codegraphAvailable) {
setUseNpxFallback(true);
}
// Step 2: Ask for installation location
const location = await promptInstallLocation();
console.log();
@@ -104,7 +111,7 @@ export async function runInstaller(): Promise<void> {
}
// Show next steps
showNextSteps(location);
showNextSteps(location, !codegraphAvailable);
} catch (err) {
console.log();
if (err instanceof Error && err.message.includes('readline was closed')) {
@@ -123,6 +130,18 @@ export async function runInstaller(): Promise<void> {
async function initializeLocalProject(): Promise<void> {
const projectPath = process.cwd();
// Lazy-load CodeGraph (requires native modules)
let CodeGraph: typeof import('../index').default;
try {
CodeGraph = (await import('../index')).default;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
error(`Could not load native modules: ${msg}`);
info('Skipping project initialization. You can run "codegraph init -i" later.');
info('If this persists, try a Node.js LTS version (20 or 22).');
return;
}
// Check if already initialized
if (CodeGraph.isInitialized(projectPath)) {
info('CodeGraph already initialized in this project');
+5
View File
@@ -88,6 +88,11 @@ export class MCPServer {
// Keep the process running
process.on('SIGINT', () => this.stop());
process.on('SIGTERM', () => this.stop());
// When the parent process (Claude Code) exits, stdin closes.
// Detect this and shut down gracefully to prevent orphaned processes.
process.stdin.on('end', () => this.stop());
process.stdin.on('close', () => this.stop());
}
/**
+6 -8
View File
@@ -12,6 +12,9 @@ import { clamp } from '../utils';
import { tmpdir } from 'os';
import { join } from 'path';
/** Maximum output length to prevent context bloat (characters) */
const MAX_OUTPUT_LENGTH = 15000;
/**
* Mark a Claude session as having consulted MCP tools.
* This enables Grep/Glob/Bash commands that would otherwise be blocked.
@@ -820,19 +823,14 @@ export class ToolHandler {
return { node: results[0]!.node, note: '' };
}
/**
* Maximum output length to prevent context bloat (characters)
*/
private readonly MAX_OUTPUT_LENGTH = 15000;
/**
* Truncate output if it exceeds the maximum length
*/
private truncateOutput(text: string): string {
if (text.length <= this.MAX_OUTPUT_LENGTH) return text;
const truncated = text.slice(0, this.MAX_OUTPUT_LENGTH);
if (text.length <= MAX_OUTPUT_LENGTH) return text;
const truncated = text.slice(0, MAX_OUTPUT_LENGTH);
const lastNewline = truncated.lastIndexOf('\n');
const cutPoint = lastNewline > this.MAX_OUTPUT_LENGTH * 0.8 ? lastNewline : this.MAX_OUTPUT_LENGTH;
const cutPoint = lastNewline > MAX_OUTPUT_LENGTH * 0.8 ? lastNewline : MAX_OUTPUT_LENGTH;
return truncated.slice(0, cutPoint) + '\n\n... (output truncated)';
}
+4 -9
View File
@@ -105,7 +105,7 @@ function resolveRelativeImport(
// Try the path as-is first
const basePath = path.resolve(fromDir, importPath);
const relativePath = path.relative(projectRoot, basePath);
const relativePath = path.relative(projectRoot, basePath).replace(/\\/g, '/');
// Try each extension
for (const ext of extensions) {
@@ -442,16 +442,11 @@ export function resolveViaImport(
ref: UnresolvedRef,
context: ResolutionContext
): ResolvedRef | null {
// Use cached import mappings or extract and cache them
let imports = importMappingCache.get(ref.filePath);
if (!imports) {
const content = context.readFile(ref.filePath);
if (!content) {
// Use cached import mappings (avoids re-reading and re-parsing per ref)
const imports = context.getImportMappings(ref.filePath, ref.language);
if (imports.length === 0 && !context.readFile(ref.filePath)) {
return null;
}
imports = extractImportMappings(ref.filePath, content, ref.language);
importMappingCache.set(ref.filePath, imports);
}
// Check if the reference name matches any import
for (const imp of imports) {
+55 -4
View File
@@ -15,9 +15,10 @@ import {
ResolutionResult,
ResolutionContext,
FrameworkResolver,
ImportMapping,
} from './types';
import { matchReference, clearFuzzyIndex } from './name-matcher';
import { resolveViaImport, clearImportMappingCache } from './import-resolver';
import { matchReference } from './name-matcher';
import { resolveViaImport, extractImportMappings } from './import-resolver';
import { detectFrameworks } from './frameworks';
import { logDebug } from '../errors';
@@ -40,6 +41,9 @@ export class ReferenceResolver {
private qualifiedNameCache: Map<string, Node[]> = new Map();
private kindCache: Map<string, Node[]> = new Map();
private nodeByIdCache: Map<string, Node> = new Map();
private lowerNameCache: Map<string, Node[]> = new Map();
private importMappingCache: Map<string, ImportMapping[]> = new Map();
private knownFiles: Set<string> | null = null;
private cachesWarmed = false;
constructor(projectRoot: string, queries: QueryBuilder) {
@@ -91,7 +95,19 @@ export class ReferenceResolver {
// Index by ID
this.nodeByIdCache.set(node.id, node);
// Index by lowercase name (for fuzzy matching)
const lowerName = node.name.toLowerCase();
const byLower = this.lowerNameCache.get(lowerName);
if (byLower) {
byLower.push(node);
} else {
this.lowerNameCache.set(lowerName, [node]);
}
}
// Pre-build known files set from index
this.knownFiles = new Set(this.queries.getAllFiles().map((f) => f.path));
this.cachesWarmed = true;
}
@@ -106,8 +122,9 @@ export class ReferenceResolver {
this.qualifiedNameCache.clear();
this.kindCache.clear();
this.nodeByIdCache.clear();
clearImportMappingCache();
clearFuzzyIndex();
this.lowerNameCache.clear();
this.importMappingCache.clear();
this.knownFiles = null;
this.cachesWarmed = false;
}
@@ -150,6 +167,14 @@ export class ReferenceResolver {
},
fileExists: (filePath: string) => {
// Check pre-built known files set first (O(1))
if (this.knownFiles) {
const normalized = filePath.replace(/\\/g, '/');
if (this.knownFiles.has(filePath) || this.knownFiles.has(normalized)) {
return true;
}
}
// Fall back to filesystem for files not yet indexed
const fullPath = path.join(this.projectRoot, filePath);
try {
return fs.existsSync(fullPath);
@@ -183,6 +208,32 @@ export class ReferenceResolver {
getAllFiles: () => {
return this.queries.getAllFiles().map((f) => f.path);
},
getNodesByLowerName: (lowerName: string) => {
if (this.cachesWarmed) {
return this.lowerNameCache.get(lowerName) ?? [];
}
// Fallback: scan all nodes (expensive, but only used if cache not warm)
return this.queries.getAllNodes().filter(
(n) => n.name.toLowerCase() === lowerName
);
},
getImportMappings: (filePath: string, language) => {
const cacheKey = filePath;
const cached = this.importMappingCache.get(cacheKey);
if (cached) return cached;
const content = this.context.readFile(filePath);
if (!content) {
this.importMappingCache.set(cacheKey, []);
return [];
}
const mappings = extractImportMappings(filePath, content, language);
this.importMappingCache.set(cacheKey, mappings);
return mappings;
},
};
}
+8 -32
View File
@@ -190,16 +190,6 @@ function findBestMatch(
return bestNode;
}
// Lazily-built case-insensitive index for fuzzy matching
let fuzzyIndex: Map<string, Node[]> | null = null;
/**
* Clear the fuzzy match index (call between indexing runs)
*/
export function clearFuzzyIndex(): void {
fuzzyIndex = null;
}
/**
* Fuzzy match - last resort with lower confidence
*/
@@ -207,38 +197,24 @@ export function matchFuzzy(
ref: UnresolvedRef,
context: ResolutionContext
): ResolvedRef | null {
// Build case-insensitive index on first use
if (!fuzzyIndex) {
fuzzyIndex = new Map();
const kinds: Array<Node['kind']> = ['function', 'method', 'class'];
for (const kind of kinds) {
for (const node of context.getNodesByKind(kind)) {
const lower = node.name.toLowerCase();
const existing = fuzzyIndex.get(lower);
if (existing) {
existing.push(node);
} else {
fuzzyIndex.set(lower, [node]);
}
}
}
}
const lowerName = ref.referenceName.toLowerCase();
// Exact case-insensitive match via index (O(1) lookup)
const caseInsensitive = fuzzyIndex.get(lowerName);
// Use pre-built lowercase index for O(1) lookup instead of scanning all nodes
const candidates = context.getNodesByLowerName(lowerName);
if (caseInsensitive && caseInsensitive.length === 1) {
// Filter to callable kinds only (function, method, class)
const callableKinds = new Set(['function', 'method', 'class']);
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind));
if (callableCandidates.length === 1) {
return {
original: ref,
targetNodeId: caseInsensitive[0]!.id,
targetNodeId: callableCandidates[0]!.id,
confidence: 0.5,
resolvedBy: 'fuzzy',
};
}
// Skip prefix matching — too expensive and low value (confidence 0.3)
return null;
}
+4
View File
@@ -79,6 +79,10 @@ export interface ResolutionContext {
getProjectRoot(): string;
/** Get all files */
getAllFiles(): string[];
/** Get nodes by lowercase name (O(1) lookup for fuzzy matching) */
getNodesByLowerName(lowerName: string): Node[];
/** Get cached import mappings for a file */
getImportMappings(filePath: string, language: Language): ImportMapping[];
}
/**
+2
View File
@@ -577,6 +577,8 @@ export const DEFAULT_CONFIG: CodeGraphConfig = {
'**/__pycache__/**',
'**/.venv/**',
'**/venv/**',
'**/site-packages/**',
'**/dist-packages/**',
'**/.pytest_cache/**',
'**/.mypy_cache/**',
'**/.ruff_cache/**',
+3 -3
View File
@@ -4,7 +4,7 @@
* High-level manager that coordinates embedding generation and vector search.
*/
import Database from 'better-sqlite3';
import { SqliteDatabase } from '../db/sqlite-adapter';
import { Node, SearchResult, SearchOptions } from '../types';
import { TextEmbedder, createEmbedder, EmbedderOptions, EMBEDDING_DIMENSION } from './embedder';
import { VectorSearchManager, createVectorSearch } from './search';
@@ -68,7 +68,7 @@ export class VectorManager {
private initialized = false;
constructor(
db: Database.Database,
db: SqliteDatabase,
queries: QueryBuilder,
options: VectorManagerOptions = {}
) {
@@ -355,7 +355,7 @@ export class VectorManager {
* Create a vector manager
*/
export function createVectorManager(
db: Database.Database,
db: SqliteDatabase,
queries: QueryBuilder,
options?: VectorManagerOptions
): VectorManager {
+7 -6
View File
@@ -5,7 +5,7 @@
* Falls back to brute-force cosine similarity if sqlite-vss is not available.
*/
import Database from 'better-sqlite3';
import { SqliteDatabase } from '../db/sqlite-adapter';
import { Node } from '../types';
import { TextEmbedder, EMBEDDING_DIMENSION } from './embedder';
@@ -29,11 +29,11 @@ export interface VectorSearchOptions {
* Handles vector storage and similarity search for semantic code search.
*/
export class VectorSearchManager {
private db: Database.Database;
private db: SqliteDatabase;
private vssEnabled = false;
private embeddingDimension: number;
constructor(db: Database.Database, dimension: number = EMBEDDING_DIMENSION) {
constructor(db: SqliteDatabase, dimension: number = EMBEDDING_DIMENSION) {
this.db = db;
this.embeddingDimension = dimension;
}
@@ -75,10 +75,11 @@ export class VectorSearchManager {
const vss = await import('sqlite-vss');
// Use the load function which loads both vector0 and vss0
// VSS extension expects the raw better-sqlite3 Database instance
if (typeof vss.load === 'function') {
vss.load(this.db);
vss.load(this.db as any);
} else if (typeof vss.default?.load === 'function') {
vss.default.load(this.db);
vss.default.load(this.db as any);
} else {
throw new Error('sqlite-vss load function not found');
}
@@ -464,7 +465,7 @@ export class VectorSearchManager {
* Create a vector search manager
*/
export function createVectorSearch(
db: Database.Database,
db: SqliteDatabase,
dimension?: number
): VectorSearchManager {
return new VectorSearchManager(db, dimension);
+182
View File
@@ -0,0 +1,182 @@
/**
* Local type override for web-tree-sitter.
*
* The upstream types declare children/namedChildren as (Node | null)[],
* but in practice they never contain null entries. This override uses
* non-nullable arrays to match native tree-sitter's API and avoid
* pervasive null-check changes across the extraction pipeline.
*
* This file takes precedence over node_modules/web-tree-sitter/web-tree-sitter.d.ts
* because TypeScript resolves local declarations first.
*/
declare module 'web-tree-sitter' {
export interface Point {
row: number;
column: number;
}
export interface Range {
startPosition: Point;
endPosition: Point;
startIndex: number;
endIndex: number;
}
export interface Edit {
startPosition: Point;
oldEndPosition: Point;
newEndPosition: Point;
startIndex: number;
oldEndIndex: number;
newEndIndex: number;
}
export type ParseCallback = (index: number, position: Point) => string | undefined;
export interface ParseOptions {
includedRanges?: Range[];
progressCallback?: (state: { currentOffset: number; hasError: boolean }) => void;
}
export interface EmscriptenModule {
[key: string]: any;
}
export class Parser {
language: Language | null;
static init(moduleOptions?: EmscriptenModule): Promise<void>;
constructor();
delete(): void;
setLanguage(language: Language | null): this;
parse(callback: string | ParseCallback, oldTree?: Tree | null, options?: ParseOptions): Tree | null;
reset(): void;
getIncludedRanges(): Range[];
getTimeoutMicros(): number;
setTimeoutMicros(timeout: number): void;
setLogger(callback: ((message: string, isLex: boolean) => void) | boolean | null): this;
getLogger(): ((message: string, isLex: boolean) => void) | null;
}
export class Language {
types: string[];
fields: (string | null)[];
get name(): string | null;
get version(): number;
get abiVersion(): number;
get fieldCount(): number;
get stateCount(): number;
fieldIdForName(fieldName: string): number | null;
fieldNameForId(fieldId: number): string | null;
idForNodeType(type: string, named: boolean): number | null;
get nodeTypeCount(): number;
nodeTypeForId(typeId: number): string | null;
nodeTypeIsNamed(typeId: number): boolean;
nodeTypeIsVisible(typeId: number): boolean;
get supertypes(): number[];
subtypes(supertype: number): number[];
nextState(stateId: number, typeId: number): number;
lookaheadIterator(stateId: number): any;
query(source: string): any;
static load(input: string | Uint8Array): Promise<Language>;
}
export class Tree {
language: Language;
copy(): Tree;
delete(): void;
get rootNode(): Node;
rootNodeWithOffset(offsetBytes: number, offsetExtent: Point): Node;
edit(edit: Edit): void;
walk(): TreeCursor;
getChangedRanges(other: Tree): Range[];
getIncludedRanges(): Range[];
}
export class Node {
id: number;
startIndex: number;
startPosition: Point;
tree: Tree;
get typeId(): number;
get grammarId(): number;
get type(): string;
get grammarType(): string;
get isNamed(): boolean;
get isExtra(): boolean;
get isError(): boolean;
get isMissing(): boolean;
get hasChanges(): boolean;
get hasError(): boolean;
get endIndex(): number;
get endPosition(): Point;
get text(): string;
get parseState(): number;
get nextParseState(): number;
equals(other: Node): boolean;
child(index: number): Node | null;
namedChild(index: number): Node | null;
childForFieldId(fieldId: number): Node | null;
childForFieldName(fieldName: string): Node | null;
fieldNameForChild(index: number): string | null;
fieldNameForNamedChild(index: number): string | null;
childrenForFieldName(fieldName: string): Node[];
childrenForFieldId(fieldId: number): Node[];
firstChildForIndex(index: number): Node | null;
firstNamedChildForIndex(index: number): Node | null;
get childCount(): number;
get namedChildCount(): number;
get firstChild(): Node | null;
get firstNamedChild(): Node | null;
get lastChild(): Node | null;
get lastNamedChild(): Node | null;
// Override: non-nullable arrays (tree-sitter never returns null in these)
get children(): Node[];
get namedChildren(): Node[];
descendantsOfType(types: string | string[], startPosition?: Point, endPosition?: Point): Node[];
get nextSibling(): Node | null;
get previousSibling(): Node | null;
get nextNamedSibling(): Node | null;
get previousNamedSibling(): Node | null;
get descendantCount(): number;
get parent(): Node | null;
childWithDescendant(descendant: Node): Node | null;
descendantForIndex(start: number, end?: number): Node | null;
namedDescendantForIndex(start: number, end?: number): Node | null;
descendantForPosition(start: Point, end?: Point): Node | null;
namedDescendantForPosition(start: Point, end?: Point): Node | null;
walk(): TreeCursor;
edit(edit: Edit): void;
toString(): string;
}
export class TreeCursor {
copy(): TreeCursor;
delete(): void;
get currentNode(): Node;
get currentFieldId(): number;
get currentFieldName(): string | null;
get currentDepth(): number;
get currentDescendantIndex(): number;
get nodeType(): string;
get nodeTypeId(): number;
get nodeStateId(): number;
get nodeId(): number;
get nodeIsNamed(): boolean;
get nodeIsMissing(): boolean;
get nodeText(): string;
get startPosition(): Point;
get endPosition(): Point;
get startIndex(): number;
get endIndex(): number;
gotoFirstChild(): boolean;
gotoLastChild(): boolean;
gotoParent(): boolean;
gotoNextSibling(): boolean;
gotoPreviousSibling(): boolean;
gotoDescendant(goalDescendantIndex: number): void;
gotoFirstChildForIndex(goalIndex: number): boolean;
gotoFirstChildForPosition(goalPosition: Point): boolean;
reset(node: Node): void;
resetTo(cursor: TreeCursor): void;
}
}
+4 -1
View File
@@ -24,7 +24,10 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"paths": {
"web-tree-sitter": ["./src/web-tree-sitter.d.ts"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "__tests__"]