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