From df937ceca1752ed809cf5ac64e20e2c24fb7235b Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 10 Feb 2026 16:43:10 -0600 Subject: [PATCH 01/10] Add site-packages and dist-packages to default exclude patterns Fixes #28 - Python site-packages directories (e.g. audio_tools/python/Lib/site-packages/) were not excluded by default, causing massive index bloat and FOREIGN KEY failures when indexing large libraries like tensorflow. The FK crash itself was already fixed via INSERT OR IGNORE, but excluding these directories prevents the bloat in the first place. --- src/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types.ts b/src/types.ts index 0c17ed7..a010afa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -569,6 +569,8 @@ export const DEFAULT_CONFIG: CodeGraphConfig = { '**/__pycache__/**', '**/.venv/**', '**/venv/**', + '**/site-packages/**', + '**/dist-packages/**', '**/.pytest_cache/**', '**/.mypy_cache/**', '**/.ruff_cache/**', From acd971363205c75a0910bae5d5a9bcca968a04bd Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 10 Feb 2026 18:14:26 -0600 Subject: [PATCH 02/10] Optimize reference resolution with in-memory caches The resolving refs phase stalled on large projects (3400+ files, 38k+ nodes) because matchFuzzy loaded ALL functions/methods/classes per ref, import mappings were re-extracted per ref, and fileExists hit disk every call. Add kindCache, lowerNameCache, importMappingCache, and knownFiles set to warmCaches(). Rewrite matchFuzzy to use O(1) lowercase index lookup instead of 3x getNodesByKind scans. Cache import mappings per file. Pre-build file existence set from the index for O(1) fileExists checks. --- src/resolution/import-resolver.ts | 8 ++-- src/resolution/index.ts | 68 ++++++++++++++++++++++++++++++- src/resolution/name-matcher.ts | 35 ++++------------ src/resolution/types.ts | 4 ++ 4 files changed, 82 insertions(+), 33 deletions(-) diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index d68fcc7..9418415 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -432,14 +432,12 @@ export function resolveViaImport( ref: UnresolvedRef, context: ResolutionContext ): ResolvedRef | null { - // Read the source file to extract 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; } - const imports = extractImportMappings(ref.filePath, content, ref.language); - // Check if the reference name matches any import for (const imp of imports) { if (imp.localName === ref.referenceName || ref.referenceName.startsWith(imp.localName + '.')) { diff --git a/src/resolution/index.ts b/src/resolution/index.ts index d3056a6..ad78a25 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -15,9 +15,10 @@ import { ResolutionResult, ResolutionContext, FrameworkResolver, + ImportMapping, } from './types'; import { matchReference } from './name-matcher'; -import { resolveViaImport } from './import-resolver'; +import { resolveViaImport, extractImportMappings } from './import-resolver'; import { detectFrameworks } from './frameworks'; import { logDebug } from '../errors'; @@ -39,6 +40,10 @@ export class ReferenceResolver { private nameCache: Map = new Map(); private qualifiedNameCache: Map = new Map(); private nodeByIdCache: Map = new Map(); + private kindCache: Map = new Map(); + private lowerNameCache: Map = new Map(); + private importMappingCache: Map = new Map(); + private knownFiles: Set | null = null; private cachesWarmed = false; constructor(projectRoot: string, queries: QueryBuilder) { @@ -82,8 +87,28 @@ export class ReferenceResolver { // Index by ID this.nodeByIdCache.set(node.id, node); + + // Index by kind + const byKind = this.kindCache.get(node.kind); + if (byKind) { + byKind.push(node); + } else { + this.kindCache.set(node.kind, [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; } @@ -96,6 +121,10 @@ export class ReferenceResolver { this.nameCache.clear(); this.qualifiedNameCache.clear(); this.nodeByIdCache.clear(); + this.kindCache.clear(); + this.lowerNameCache.clear(); + this.importMappingCache.clear(); + this.knownFiles = null; this.cachesWarmed = false; } @@ -131,10 +160,21 @@ export class ReferenceResolver { }, getNodesByKind: (kind: Node['kind']) => { + if (this.cachesWarmed) { + return this.kindCache.get(kind) ?? []; + } return this.queries.getNodesByKind(kind); }, 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); @@ -168,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; + }, }; } diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 47595bf..668dd11 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -197,43 +197,24 @@ export function matchFuzzy( ref: UnresolvedRef, context: ResolutionContext ): ResolvedRef | null { - // Try case-insensitive match - const allNodes = [ - ...context.getNodesByKind('function'), - ...context.getNodesByKind('method'), - ...context.getNodesByKind('class'), - ]; - const lowerName = ref.referenceName.toLowerCase(); - // Exact case-insensitive match - const caseInsensitive = allNodes.filter( - (n) => n.name.toLowerCase() === lowerName - ); + // Use pre-built lowercase index for O(1) lookup instead of scanning all nodes + const candidates = context.getNodesByLowerName(lowerName); - if (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', }; } - // Try prefix match (e.g., "get" matches "getUser") - const prefixMatches = allNodes.filter((n) => - n.name.toLowerCase().startsWith(lowerName) - ); - - if (prefixMatches.length === 1) { - return { - original: ref, - targetNodeId: prefixMatches[0]!.id, - confidence: 0.3, - resolvedBy: 'fuzzy', - }; - } - return null; } diff --git a/src/resolution/types.ts b/src/resolution/types.ts index dc4ca56..cd498db 100644 --- a/src/resolution/types.ts +++ b/src/resolution/types.ts @@ -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[]; } /** From 4c7827675ad9118a9892136e74e93d7b661b74cc Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 10 Feb 2026 18:16:06 -0600 Subject: [PATCH 03/10] Auto stash before merge of "main" and "origin/main" --- README.md | 80 +++++++++++++++++++++++++++++++++-------------- package-lock.json | 4 +-- package.json | 2 +- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c1b7423..b84bf32 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Know exactly what breaks before you change it. Trace callers, callees, and the f ### 🌍 17+ Languages -TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte—all with the same API. +TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte, Liquid—all with the same API. @@ -145,7 +145,7 @@ No data leaves your machine. No API keys. No external services. Everything runs ### ⚡ 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. @@ -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 symbols +codegraph files [path] # Show project file structure codegraph context # 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 ` @@ -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 ` 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 | ## 🔧 Troubleshooting diff --git a/package-lock.json b/package-lock.json index f7f4e76..11a9662 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@colbymchenry/codegraph", - "version": "0.4.7", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@colbymchenry/codegraph", - "version": "0.4.7", + "version": "0.5.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6bd64ee..f930fb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@colbymchenry/codegraph", - "version": "0.4.7", + "version": "0.5.0", "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", From 09a8d24bd87661b6020bd93784080a56c3f2d67d Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 10 Feb 2026 18:18:51 -0600 Subject: [PATCH 04/10] version bump --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11a9662..668eedf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@colbymchenry/codegraph", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@colbymchenry/codegraph", - "version": "0.5.0", + "version": "0.5.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f930fb2..0da7670 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@colbymchenry/codegraph", - "version": "0.5.0", + "version": "0.5.1", "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", From eee081e2a758d6f4f098d0966270bc78b8bc387c Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 11 Feb 2026 02:58:59 -0600 Subject: [PATCH 05/10] detached process --- src/bin/codegraph.ts | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 6a161e1..6679ac4 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -25,6 +25,7 @@ import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; +import { spawn } from 'child_process'; import CodeGraph, { getCodeGraphDir, findNearestCodeGraphRoot } from '../index'; import type { IndexProgress } from '../index'; import { runInstaller } from '../installer'; @@ -957,8 +958,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 +976,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 +987,24 @@ program try { fs.unlinkSync(dirtyPath); } catch { /* ignore */ } // If not fully initialized (no DB), exit - if (!CodeGraph.isInitialized(projectRoot)) { + if (!CodeGraph.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 } From a7fc5853a2c489deb9db1ad4529afe7cb01a7907 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 11 Feb 2026 15:15:42 -0600 Subject: [PATCH 06/10] Exit child processes on windows --- src/mcp/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 6ae6f69..c57e11a 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -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()); } /** From ce504c642ac000b09c823a4d4e77b0777670d79c Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 11 Feb 2026 16:11:50 -0600 Subject: [PATCH 07/10] Fix performance issues. --- __tests__/sync.test.ts | 109 ++++++++++++++ package-lock.json | 4 +- package.json | 2 +- src/db/queries.ts | 24 +++ src/extraction/index.ts | 315 +++++++++++++++++++++++++++++++--------- src/index.ts | 9 +- 6 files changed, 388 insertions(+), 75 deletions(-) diff --git a/__tests__/sync.test.ts b/__tests__/sync.test.ts index 909c079..8365f63 100644 --- a/__tests__/sync.test.ts +++ b/__tests__/sync.test.ts @@ -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(); + }); + }); }); diff --git a/package-lock.json b/package-lock.json index 668eedf..cfbad97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@colbymchenry/codegraph", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@colbymchenry/codegraph", - "version": "0.5.1", + "version": "0.5.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 0da7670..0fe7757 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@colbymchenry/codegraph", - "version": "0.5.1", + "version": "0.5.2", "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", diff --git a/src/db/queries.ts b/src/db/queries.ts index aed2a96..83702de 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -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) */ diff --git a/src/extraction/index.ts b/src/extraction/index.ts index b3e1b2c..4573bb6 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -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 { +function getGitVisibleFiles(rootDir: string): Set | 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(); + 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(); + + 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(); + return null; } } @@ -135,21 +189,49 @@ function getGitIgnoredDirectories(rootDir: string): Set { 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(); - 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); } } } @@ -611,7 +678,8 @@ 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 { const startTime = Date.now(); @@ -620,53 +688,107 @@ export class ExtractionOrchestrator { 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 currentFiles = new Set(scanDirectory(this.rootDir, this.config)); - filesChecked = currentFiles.size; - - // Get tracked files from database - const trackedFiles = this.queries.getAllFiles(); - - // Find files to remove (in DB but not on disk) - for (const tracked of trackedFiles) { - if (!currentFiles.has(tracked.path)) { - this.queries.deleteFile(tracked.path); - filesRemoved++; - } - } - - // Find files to add or update const filesToIndex: string[] = []; + const gitChanges = getGitChangedFiles(this.rootDir, this.config); - for (const filePath of currentFiles) { - 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; + 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++; + } } - const contentHash = hashContent(content); - const tracked = trackedFiles.find((f) => f.path === filePath); + // 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; + } - if (!tracked) { - // New file + 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 if (tracked.contentHash !== contentHash) { - // Modified file - filesToIndex.push(filePath); - filesModified++; + } + } else { + // === Fallback: full scan (non-git project or git failure) === + const currentFiles = new Set(scanDirectory(this.rootDir, this.config)); + filesChecked = currentFiles.size; + + // Build Map for O(1) lookups instead of .find() per file + const trackedFiles = this.queries.getAllFiles(); + const trackedMap = new Map(); + 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) { + if (!currentFiles.has(tracked.path)) { + this.queries.deleteFile(tracked.path); + filesRemoved++; + } + } + + // Find files to add or update + for (const filePath of currentFiles) { + 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 = trackedMap.get(filePath); + + if (!tracked) { + filesToIndex.push(filePath); + changedFilePaths.push(filePath); + filesAdded++; + } else if (tracked.contentHash !== contentHash) { + filesToIndex.push(filePath); + changedFilePaths.push(filePath); + filesModified++; + } } } @@ -692,16 +814,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(); + for (const f of trackedFiles) { + trackedMap.set(f.path, f); + } + const added: string[] = []; const modified: string[] = []; const removed: string[] = []; @@ -726,7 +903,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); diff --git a/src/index.ts b/src/index.ts index 5bd43a2..4758e91 100644 --- a/src/index.ts +++ b/src/index.ts @@ -449,15 +449,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, From 429359c25fe776f9d3dd255b716a80c9d3ad000d Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 11 Feb 2026 16:12:45 -0600 Subject: [PATCH 08/10] version bump --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfbad97..0843d38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@colbymchenry/codegraph", - "version": "0.5.2", + "version": "0.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@colbymchenry/codegraph", - "version": "0.5.2", + "version": "0.5.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 0fe7757..7c164ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@colbymchenry/codegraph", - "version": "0.5.2", + "version": "0.5.3", "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", From 8346440592678feeafc662f20770df48bbb5f873 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sat, 14 Feb 2026 00:56:15 -0600 Subject: [PATCH 09/10] Add WASM fallbacks for tree-sitter and SQLite, fix installer Replace native tree-sitter with web-tree-sitter + tree-sitter-wasms for universal cross-platform support. Add node-sqlite3-wasm as a fallback when better-sqlite3 native bindings aren't available. Move better-sqlite3 and sqlite-vss to optionalDependencies so installs never fail. Fix installer to use npx fallback when global npm install fails, so MCP config, hooks, and quick-start instructions all work without the bare codegraph command in PATH. Fix tests: update schema version expectation, fix db test paths and method names, extract MAX_OUTPUT_LENGTH as module constant, normalize Windows path separators in import resolver. --- __tests__/extraction.test.ts | 8 +- __tests__/foundation.test.ts | 2 +- __tests__/pr19-improvements.test.ts | 48 +-- package-lock.json | 565 ++-------------------------- package.json | 25 +- scripts/postinstall.js | 3 - src/bin/codegraph.ts | 57 ++- src/db/index.ts | 14 +- src/db/migrations.ts | 16 +- src/db/queries.ts | 48 +-- src/db/sqlite-adapter.ts | 227 +++++++++++ src/extraction/grammars.ts | 172 ++++----- src/extraction/index.ts | 6 +- src/extraction/tree-sitter.ts | 17 +- src/index.ts | 5 +- src/installer/banner.ts | 5 +- src/installer/config-writer.ts | 19 +- src/installer/index.ts | 29 +- src/mcp/tools.ts | 14 +- src/resolution/import-resolver.ts | 2 +- src/vectors/manager.ts | 6 +- src/vectors/search.ts | 13 +- src/web-tree-sitter.d.ts | 182 +++++++++ tsconfig.json | 5 +- 24 files changed, 707 insertions(+), 781 deletions(-) create mode 100644 src/db/sqlite-adapter.ts create mode 100644 src/web-tree-sitter.d.ts diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 0a5c46b..9f666f6 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -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-')); diff --git a/__tests__/foundation.test.ts b/__tests__/foundation.test.ts index f7729cd..6aa01ad 100644 --- a/__tests__/foundation.test.ts +++ b/__tests__/foundation.test.ts @@ -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(); }); diff --git a/__tests__/pr19-improvements.test.ts b/__tests__/pr19-improvements.test.ts index dcaa564..1d0889b 100644 --- a/__tests__/pr19-improvements.test.ts +++ b/__tests__/pr19-improvements.test.ts @@ -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) - .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(); }); }); diff --git a/package-lock.json b/package-lock.json index 0843d38..1766a8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,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" }, "bin": { "codegraph": "dist/bin/codegraph.js" @@ -32,21 +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-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": { @@ -870,50 +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, - "peer": 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, - "peer": 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", @@ -1228,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" @@ -1238,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" } @@ -1549,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", @@ -1628,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", @@ -1729,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", @@ -2336,442 +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", + "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-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": { @@ -2961,20 +2460,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": { diff --git a/package.json b/package.json index 7c164ca..ea4bdde 100644 --- a/package.json +++ b/package.json @@ -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,26 +50,10 @@ "vitest": "^2.1.9" }, "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-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" } } diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 7a89b2e..3725a5d 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -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); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 6679ac4..75a78dd 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -26,11 +26,25 @@ import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; import { spawn } from 'child_process'; -import CodeGraph, { getCodeGraphDir, findNearestCodeGraphRoot } from '../index'; -import type { IndexProgress } from '../index'; -import { runInstaller } from '../installer'; +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 { + 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 = (() => { @@ -41,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 { @@ -117,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; } @@ -131,7 +147,7 @@ function resolveProjectPath(pathArg?: string): string { if (parent === current) break; current = parent; - if (CodeGraph.isInitialized(current)) { + if (isInitialized(current)) { return current; } } @@ -240,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 }); @@ -295,7 +312,7 @@ program const projectPath = resolveProjectPath(pathArg); try { - if (!CodeGraph.isInitialized(projectPath)) { + if (!isInitialized(projectPath)) { warn(`CodeGraph is not initialized in ${projectPath}`); return; } @@ -318,6 +335,7 @@ program } } + const { default: CodeGraph } = await loadCodeGraph(); const cg = CodeGraph.openSync(projectPath); cg.uninitialize(); @@ -341,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) { @@ -408,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({ @@ -467,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; @@ -479,6 +499,7 @@ program return; } + const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.open(projectPath); const stats = cg.getStats(); const changes = cg.getChangedFiles(); @@ -579,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); @@ -652,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(); @@ -854,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, { @@ -987,7 +1011,7 @@ program try { fs.unlinkSync(dirtyPath); } catch { /* ignore */ } // If not fully initialized (no DB), exit - if (!CodeGraph.isInitialized(projectRoot!)) { + if (!isInitialized(projectRoot!)) { process.exit(0); } @@ -1018,6 +1042,7 @@ program .command('install') .description('Run interactive installer for Claude Code integration') .action(async () => { + const { runInstaller } = await import('../installer'); await runInstaller(); }); diff --git a/src/db/index.ts b/src/db/index.ts index 66892d6..34e9933 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -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; } diff --git a/src/db/migrations.ts b/src/db/migrations.ts index 08510b3..307cc38 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -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') diff --git a/src/db/queries.ts b/src/db/queries.ts index 83702de..e3cea84 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -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 = 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; } diff --git a/src/db/sqlite-adapter.ts b/src/db/sqlite-adapter.ts new file mode 100644 index 0000000..01155fc --- /dev/null +++ b/src/db/sqlite-adapter.ts @@ -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(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(); + + 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(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}` + ); +} diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 5ef676b..44a4d20 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -1,87 +1,37 @@ /** * 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 { Parser, Language as WasmLanguage } from 'web-tree-sitter'; import { Language } from '../types'; -type GrammarLoader = () => unknown; type GrammarLanguage = Exclude; /** - * 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 = { - 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'); - }, - // 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 = { + 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', }; /** @@ -122,55 +72,62 @@ export const EXTENSION_MAP: Record = { * Caches for loaded grammars and parsers */ const parserCache = new Map(); -const grammarCache = new Map(); +const languageCache = new Map(); const unavailableGrammarErrors = new Map(); +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 { + if (grammarsInitialized) return; - const loader = grammarLoaders[language as GrammarLanguage]; - if (!loader) { - grammarCache.set(language, null); - return null; - } + await Parser.init(); - try { - const grammar = loader(); - if (!grammar) { - throw new Error(`Grammar loader returned empty value for ${language}`); - } - grammarCache.set(language, grammar); - return grammar; - } 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; - } + // 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 wasmPath = 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 ${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[0]); + parser.setLanguage(lang); parserCache.set(language, parser); return parser; } @@ -190,15 +147,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']; } @@ -207,7 +164,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(); } diff --git a/src/extraction/index.ts b/src/extraction/index.ts index 4573bb6..1abb0aa 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -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'; @@ -342,6 +342,7 @@ export class ExtractionOrchestrator { onProgress?: (progress: IndexProgress) => void, signal?: AbortSignal ): Promise { + await initGrammars(); const startTime = Date.now(); const errors: ExtractionError[] = []; let filesIndexed = 0; @@ -682,6 +683,7 @@ export class ExtractionOrchestrator { * Uses git status as a fast path when available, falling back to full scan. */ async sync(onProgress?: (progress: IndexProgress) => void): Promise { + await initGrammars(); const startTime = Date.now(); let filesChecked = 0; let filesAdded = 0; @@ -918,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'; diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index b19b7a7..ee8d6e4 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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 { @@ -875,7 +875,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 = { @@ -1710,9 +1713,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, { diff --git a/src/index.ts b/src/index.ts index 4758e91..ccc22ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { + 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 { + await initGrammars(); const resolvedRoot = path.resolve(projectRoot); // Check if initialized diff --git a/src/installer/banner.ts b/src/installer/banner.ts index 952f6e0..1f3f811 100644 --- a/src/installer/banner.ts +++ b/src/installer/banner.ts @@ -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!')); } diff --git a/src/installer/config-writer.ts b/src/installer/config-writer.ts index 1d9b5cd..d37edd1 100644 --- a/src/installer/config-writer.ts +++ b/src/installer/config-writer.ts @@ -97,19 +97,29 @@ function writeJsonFile(filePath: string, data: Record): 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 { - 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 { - 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 { ], Stop: [ { + matcher: '.*', hooks: [ { type: 'command', diff --git a/src/installer/index.ts b/src/installer/index.ts index e35f084..1cc6f9d 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -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 { // 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 { 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 { } // 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 { async function initializeLocalProject(): Promise { 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'); diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index ad7fea4..7d3e46c 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -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)'; } diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index 9418415..16307d1 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -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) { diff --git a/src/vectors/manager.ts b/src/vectors/manager.ts index 6be1b29..3f8ac3d 100644 --- a/src/vectors/manager.ts +++ b/src/vectors/manager.ts @@ -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 { diff --git a/src/vectors/search.ts b/src/vectors/search.ts index d9d87e9..bdde94d 100644 --- a/src/vectors/search.ts +++ b/src/vectors/search.ts @@ -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); diff --git a/src/web-tree-sitter.d.ts b/src/web-tree-sitter.d.ts new file mode 100644 index 0000000..aaafd5a --- /dev/null +++ b/src/web-tree-sitter.d.ts @@ -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; + 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; + } + + 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; + } +} diff --git a/tsconfig.json b/tsconfig.json index 2cc280e..4969d06 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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__"] From a9148e674e29757e94820d4c0e5fd2b4c9960b82 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sat, 14 Feb 2026 02:14:48 -0600 Subject: [PATCH 10/10] Fix git issue --- package-lock.json | 4 ++-- package.json | 2 +- src/directory.ts | 10 ++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1766a8e..819db7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@colbymchenry/codegraph", - "version": "0.5.3", + "version": "0.5.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@colbymchenry/codegraph", - "version": "0.5.3", + "version": "0.5.5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ea4bdde..9fb2dbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@colbymchenry/codegraph", - "version": "0.5.3", + "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", diff --git a/src/directory.ts b/src/directory.ts index 09db71e..588911c 100644 --- a/src/directory.ts +++ b/src/directory.ts @@ -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 {