/** * CodeGraph * * A local-first code intelligence system that builds a semantic * knowledge graph from any codebase. */ import * as path from 'path'; import { CodeGraphConfig, Node, Edge, FileRecord, ExtractionResult, Subgraph, TraversalOptions, SearchOptions, SearchResult, Context, GraphStats, TaskInput, TaskContext, BuildContextOptions, FindRelevantContextOptions, } from './types'; import { DatabaseConnection, getDatabasePath } from './db'; import { QueryBuilder } from './db/queries'; import { loadConfig, saveConfig, createDefaultConfig } from './config'; import { isInitialized, createDirectory, removeDirectory, validateDirectory, } from './directory'; import { ExtractionOrchestrator, IndexProgress, IndexResult, SyncResult, extractFromSource, initGrammars, } from './extraction'; import { ReferenceResolver, createResolver, ResolutionResult, } from './resolution'; import { GraphTraverser, GraphQueryManager } from './graph'; import { ContextBuilder, createContextBuilder } from './context'; import { Mutex, FileLock } from './utils'; import { FileWatcher, WatchOptions } from './sync'; // Re-export types for consumers export * from './types'; export { getDatabasePath } from './db'; export { getConfigPath } from './config'; export { getCodeGraphDir, isInitialized, findNearestCodeGraphRoot, CODEGRAPH_DIR, } from './directory'; export { IndexProgress, IndexResult, SyncResult } from './extraction'; export { detectLanguage, isLanguageSupported, isGrammarLoaded, getSupportedLanguages, initGrammars, loadGrammarsForLanguages, loadAllGrammars } from './extraction'; export { ResolutionResult } from './resolution'; export { CodeGraphError, FileError, ParseError, DatabaseError, SearchError, VectorError, ConfigError, Logger, setLogger, getLogger, silentLogger, defaultLogger, } from './errors'; export { Mutex, FileLock, processInBatches, debounce, throttle, MemoryMonitor } from './utils'; export { FileWatcher, WatchOptions } from './sync'; export { MCPServer } from './mcp'; /** * Options for initializing a new CodeGraph project */ export interface InitOptions { /** Custom configuration overrides */ config?: Partial; /** Whether to run initial indexing after init */ index?: boolean; /** Progress callback for indexing */ onProgress?: (progress: IndexProgress) => void; } /** * Options for opening an existing CodeGraph project */ export interface OpenOptions { /** Whether to run sync if files have changed */ sync?: boolean; /** Whether to run in read-only mode */ readOnly?: boolean; } /** * Options for indexing */ export interface IndexOptions { /** Progress callback */ onProgress?: (progress: IndexProgress) => void; /** Abort signal for cancellation */ signal?: AbortSignal; /** Enable verbose logging (worker lifecycle, memory, timeouts) */ verbose?: boolean; } /** * Main CodeGraph class * * Provides the primary interface for interacting with the code knowledge graph. */ export class CodeGraph { private db: DatabaseConnection; private queries: QueryBuilder; private config: CodeGraphConfig; private projectRoot: string; private orchestrator: ExtractionOrchestrator; private resolver: ReferenceResolver; private graphManager: GraphQueryManager; private traverser: GraphTraverser; private contextBuilder: ContextBuilder; // Mutex for preventing concurrent indexing operations (in-process) private indexMutex = new Mutex(); // File lock for preventing concurrent writes across processes (CLI, MCP, git hooks) private fileLock: FileLock; // File watcher for auto-sync on file changes private watcher: FileWatcher | null = null; private constructor( db: DatabaseConnection, queries: QueryBuilder, config: CodeGraphConfig, projectRoot: string ) { this.db = db; this.queries = queries; this.config = config; this.projectRoot = projectRoot; this.fileLock = new FileLock( path.join(projectRoot, '.codegraph', 'codegraph.lock') ); this.orchestrator = new ExtractionOrchestrator(projectRoot, config, queries); this.resolver = createResolver(projectRoot, queries); this.graphManager = new GraphQueryManager(queries); this.traverser = new GraphTraverser(queries); this.contextBuilder = createContextBuilder( projectRoot, queries, this.traverser ); } // =========================================================================== // Lifecycle Methods // =========================================================================== /** * Initialize a new CodeGraph project * * Creates the .CodeGraph directory, database, and configuration. * * @param projectRoot - Path to the project root directory * @param options - Initialization options * @returns A new CodeGraph instance */ static async init(projectRoot: string, options: InitOptions = {}): Promise { await initGrammars(); const resolvedRoot = path.resolve(projectRoot); // Check if already initialized if (isInitialized(resolvedRoot)) { throw new Error(`CodeGraph already initialized in ${resolvedRoot}`); } // Create directory structure createDirectory(resolvedRoot); // Create and save configuration const config = createDefaultConfig(resolvedRoot); if (options.config) { Object.assign(config, options.config); } saveConfig(resolvedRoot, config); // Initialize database const dbPath = getDatabasePath(resolvedRoot); const db = DatabaseConnection.initialize(dbPath); const queries = new QueryBuilder(db.getDb()); const instance = new CodeGraph(db, queries, config, resolvedRoot); // Run initial indexing if requested if (options.index) { await instance.indexAll({ onProgress: options.onProgress }); } return instance; } /** * Initialize synchronously (without indexing) */ static initSync(projectRoot: string, options: Omit = {}): CodeGraph { const resolvedRoot = path.resolve(projectRoot); // Check if already initialized if (isInitialized(resolvedRoot)) { throw new Error(`CodeGraph already initialized in ${resolvedRoot}`); } // Create directory structure createDirectory(resolvedRoot); // Create and save configuration const config = createDefaultConfig(resolvedRoot); if (options.config) { Object.assign(config, options.config); } saveConfig(resolvedRoot, config); // Initialize database const dbPath = getDatabasePath(resolvedRoot); const db = DatabaseConnection.initialize(dbPath); const queries = new QueryBuilder(db.getDb()); return new CodeGraph(db, queries, config, resolvedRoot); } /** * Open an existing CodeGraph project * * @param projectRoot - Path to the project root directory * @param options - Open options * @returns A CodeGraph instance */ static async open(projectRoot: string, options: OpenOptions = {}): Promise { await initGrammars(); const resolvedRoot = path.resolve(projectRoot); // Check if initialized if (!isInitialized(resolvedRoot)) { throw new Error(`CodeGraph not initialized in ${resolvedRoot}. Run init() first.`); } // Validate directory structure const validation = validateDirectory(resolvedRoot); if (!validation.valid) { throw new Error(`Invalid CodeGraph directory: ${validation.errors.join(', ')}`); } // Load configuration const config = loadConfig(resolvedRoot); // Open database const dbPath = getDatabasePath(resolvedRoot); const db = DatabaseConnection.open(dbPath); const queries = new QueryBuilder(db.getDb()); const instance = new CodeGraph(db, queries, config, resolvedRoot); // Sync if requested if (options.sync) { await instance.sync(); } return instance; } /** * Open synchronously (without sync) */ static openSync(projectRoot: string): CodeGraph { const resolvedRoot = path.resolve(projectRoot); // Check if initialized if (!isInitialized(resolvedRoot)) { throw new Error(`CodeGraph not initialized in ${resolvedRoot}. Run init() first.`); } // Validate directory structure const validation = validateDirectory(resolvedRoot); if (!validation.valid) { throw new Error(`Invalid CodeGraph directory: ${validation.errors.join(', ')}`); } // Load configuration const config = loadConfig(resolvedRoot); // Open database const dbPath = getDatabasePath(resolvedRoot); const db = DatabaseConnection.open(dbPath); const queries = new QueryBuilder(db.getDb()); return new CodeGraph(db, queries, config, resolvedRoot); } /** * Check if a directory has been initialized as a CodeGraph project */ static isInitialized(projectRoot: string): boolean { return isInitialized(path.resolve(projectRoot)); } /** * Close the CodeGraph instance and release resources */ close(): void { this.unwatch(); // Release file lock if held this.fileLock.release(); this.db.close(); } // =========================================================================== // Configuration // =========================================================================== /** * Get the current configuration */ getConfig(): CodeGraphConfig { return { ...this.config }; } /** * Update configuration */ updateConfig(updates: Partial): void { Object.assign(this.config, updates); saveConfig(this.projectRoot, this.config); // Recreate orchestrator and resolver with new config this.orchestrator = new ExtractionOrchestrator( this.projectRoot, this.config, this.queries ); this.resolver = createResolver(this.projectRoot, this.queries); } /** * Get the project root directory */ getProjectRoot(): string { return this.projectRoot; } // =========================================================================== // Indexing // =========================================================================== /** * Index all files in the project * * Uses a mutex to prevent concurrent indexing operations. */ async indexAll(options: IndexOptions = {}): Promise { return this.indexMutex.withLock(async () => { try { this.fileLock.acquire(); } catch { return { success: false, filesIndexed: 0, filesSkipped: 0, filesErrored: 0, nodesCreated: 0, edgesCreated: 0, errors: [{ message: 'Could not acquire file lock - another process may be indexing', severity: 'error' as const }], durationMs: 0 }; } try { const result = await this.orchestrator.indexAll(options.onProgress, options.signal, options.verbose); // Resolve references to create call/import/extends edges if (result.success && result.filesIndexed > 0) { // Get count without loading all refs into memory const unresolvedCount = this.queries.getUnresolvedReferencesCount(); options.onProgress?.({ phase: 'resolving', current: 0, total: unresolvedCount, }); await this.resolveReferencesBatched((current, total) => { options.onProgress?.({ phase: 'resolving', current, total, }); }); } return result; } finally { this.fileLock.release(); } }); } /** * Index specific files * * Uses a mutex to prevent concurrent indexing operations. */ async indexFiles(filePaths: string[]): Promise { return this.indexMutex.withLock(async () => { try { this.fileLock.acquire(); } catch { return { success: false, filesIndexed: 0, filesSkipped: 0, filesErrored: 0, nodesCreated: 0, edgesCreated: 0, errors: [{ message: 'Could not acquire file lock - another process may be indexing', severity: 'error' as const }], durationMs: 0 }; } try { return this.orchestrator.indexFiles(filePaths); } finally { this.fileLock.release(); } }); } /** * Sync with current file state (incremental update) * * Uses a mutex to prevent concurrent indexing operations. */ async sync(options: IndexOptions = {}): Promise { return this.indexMutex.withLock(async () => { try { this.fileLock.acquire(); } catch { return { filesChecked: 0, filesAdded: 0, filesModified: 0, filesRemoved: 0, nodesUpdated: 0, durationMs: 0 }; } try { const result = await this.orchestrator.sync(options.onProgress); // Resolve references if files were updated if (result.filesAdded > 0 || result.filesModified > 0) { if (result.changedFilePaths) { // Scope resolution to changed files (git fast path — bounded set) const unresolvedRefs = this.queries.getUnresolvedReferencesByFiles(result.changedFilePaths); options.onProgress?.({ phase: 'resolving', current: 0, total: unresolvedRefs.length, }); this.resolver.resolveAndPersist(unresolvedRefs, (current, total) => { options.onProgress?.({ phase: 'resolving', current, total, }); }); } else { // No git info — use batched resolution to avoid OOM const unresolvedCount = this.queries.getUnresolvedReferencesCount(); options.onProgress?.({ phase: 'resolving', current: 0, total: unresolvedCount, }); await this.resolveReferencesBatched((current, total) => { options.onProgress?.({ phase: 'resolving', current, total, }); }); } } return result; } finally { this.fileLock.release(); } }); } /** * Check if an indexing operation is currently in progress */ isIndexing(): boolean { return this.indexMutex.isLocked(); } // =========================================================================== // File Watching // =========================================================================== /** * Start watching for file changes and auto-syncing. * * Uses native OS file events (FSEvents on macOS, inotify on Linux 19+, * ReadDirectoryChangesW on Windows) with debouncing to avoid thrashing. * * @param options - Watch options (debounce delay, callbacks) * @returns true if watching started successfully */ watch(options: WatchOptions = {}): boolean { if (this.watcher?.isActive()) return true; this.watcher = new FileWatcher( this.projectRoot, this.config, async () => { const result = await this.sync(); const filesChanged = result.filesAdded + result.filesModified + result.filesRemoved; return { filesChanged, durationMs: result.durationMs }; }, options ); return this.watcher.start(); } /** * Stop watching for file changes. */ unwatch(): void { if (this.watcher) { this.watcher.stop(); this.watcher = null; } } /** * Check if the file watcher is active. */ isWatching(): boolean { return this.watcher?.isActive() ?? false; } /** * Get files that have changed since last index */ getChangedFiles(): { added: string[]; modified: string[]; removed: string[] } { return this.orchestrator.getChangedFiles(); } /** * Extract nodes and edges from source code (without storing) */ extractFromSource(filePath: string, source: string): ExtractionResult { return extractFromSource(filePath, source); } // =========================================================================== // Reference Resolution // =========================================================================== /** * Resolve unresolved references and create edges * * This method takes unresolved references from extraction and attempts * to resolve them using multiple strategies: * - Framework-specific patterns (React, Express, Laravel) * - Import-based resolution * - Name-based symbol matching */ resolveReferences(onProgress?: (current: number, total: number) => void): ResolutionResult { // Get all unresolved references from the database const unresolvedRefs = this.queries.getUnresolvedReferences(); return this.resolver.resolveAndPersist(unresolvedRefs, onProgress); } /** * Resolve references in batches to keep memory bounded on large codebases. * Processes chunks of unresolved refs, persisting results after each batch. */ async resolveReferencesBatched(onProgress?: (current: number, total: number) => void): Promise { return this.resolver.resolveAndPersistBatched(onProgress); } /** * Get detected frameworks in the project */ getDetectedFrameworks(): string[] { return this.resolver.getDetectedFrameworks(); } /** * Re-initialize the resolver (useful after adding new files) */ reinitializeResolver(): void { this.resolver.initialize(); } // =========================================================================== // Graph Statistics // =========================================================================== /** * Get statistics about the knowledge graph */ getStats(): GraphStats { const stats = this.queries.getStats(); stats.dbSizeBytes = this.db.getSize(); return stats; } /** * Active SQLite backend for this project's connection. `wasm` means * the native better-sqlite3 install failed and the WASM fallback is * serving requests at 5-10x the latency. Surfaced via `codegraph * status` and the `codegraph_status` MCP tool. */ getBackend(): import('./db').SqliteBackend { return this.db.getBackend(); } // =========================================================================== // Node Operations // =========================================================================== /** * Get a node by ID */ getNode(id: string): Node | null { return this.queries.getNodeById(id); } /** * Get all nodes in a file */ getNodesInFile(filePath: string): Node[] { return this.queries.getNodesByFile(filePath); } /** * Get all nodes of a specific kind */ getNodesByKind(kind: Node['kind']): Node[] { return this.queries.getNodesByKind(kind); } /** * Search nodes by text */ searchNodes(query: string, options?: SearchOptions): SearchResult[] { return this.queries.searchNodes(query, options); } // =========================================================================== // Edge Operations // =========================================================================== /** * Get outgoing edges from a node */ getOutgoingEdges(nodeId: string): Edge[] { return this.queries.getOutgoingEdges(nodeId); } /** * Get incoming edges to a node */ getIncomingEdges(nodeId: string): Edge[] { return this.queries.getIncomingEdges(nodeId); } // =========================================================================== // File Operations // =========================================================================== /** * Get a file record by path */ getFile(filePath: string): FileRecord | null { return this.queries.getFileByPath(filePath); } /** * Get all tracked files */ getFiles(): FileRecord[] { return this.queries.getAllFiles(); } // =========================================================================== // Graph Query Methods // =========================================================================== /** * Get the context for a node (ancestors, children, references) * * Returns comprehensive context about a node including its containment * hierarchy, children, incoming/outgoing references, type information, * and relevant imports. * * @param nodeId - ID of the focal node * @returns Context object with all related information */ getContext(nodeId: string): Context { return this.graphManager.getContext(nodeId); } /** * Traverse the graph from a starting node * * Uses breadth-first search by default. Supports filtering by edge types, * node types, and traversal direction. * * @param startId - Starting node ID * @param options - Traversal options * @returns Subgraph containing traversed nodes and edges */ traverse(startId: string, options?: TraversalOptions): Subgraph { return this.traverser.traverseBFS(startId, options); } /** * Get the call graph for a function * * Returns both callers (functions that call this function) and * callees (functions called by this function) up to the specified depth. * * @param nodeId - ID of the function/method node * @param depth - Maximum depth in each direction (default: 2) * @returns Subgraph containing the call graph */ getCallGraph(nodeId: string, depth: number = 2): Subgraph { return this.traverser.getCallGraph(nodeId, depth); } /** * Get the type hierarchy for a class/interface * * Returns both ancestors (types this extends/implements) and * descendants (types that extend/implement this). * * @param nodeId - ID of the class/interface node * @returns Subgraph containing the type hierarchy */ getTypeHierarchy(nodeId: string): Subgraph { return this.traverser.getTypeHierarchy(nodeId); } /** * Find all usages of a symbol * * Returns all nodes that reference the specified symbol through * any edge type (calls, references, type_of, etc.). * * @param nodeId - ID of the symbol node * @returns Array of nodes and edges that reference this symbol */ findUsages(nodeId: string): Array<{ node: Node; edge: Edge }> { return this.traverser.findUsages(nodeId); } /** * Get callers of a function/method * * @param nodeId - ID of the function/method node * @param maxDepth - Maximum depth to traverse (default: 1) * @returns Array of nodes that call this function */ getCallers(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> { return this.traverser.getCallers(nodeId, maxDepth); } /** * Get callees of a function/method * * @param nodeId - ID of the function/method node * @param maxDepth - Maximum depth to traverse (default: 1) * @returns Array of nodes called by this function */ getCallees(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> { return this.traverser.getCallees(nodeId, maxDepth); } /** * Calculate the impact radius of a node * * Returns all nodes that could be affected by changes to this node. * * @param nodeId - ID of the node * @param maxDepth - Maximum depth to traverse (default: 3) * @returns Subgraph containing potentially impacted nodes */ getImpactRadius(nodeId: string, maxDepth: number = 3): Subgraph { return this.traverser.getImpactRadius(nodeId, maxDepth); } /** * Find the shortest path between two nodes * * @param fromId - Starting node ID * @param toId - Target node ID * @param edgeKinds - Edge types to consider (all if empty) * @returns Array of nodes and edges forming the path, or null if no path exists */ findPath( fromId: string, toId: string, edgeKinds?: Edge['kind'][] ): Array<{ node: Node; edge: Edge | null }> | null { return this.traverser.findPath(fromId, toId, edgeKinds); } /** * Get ancestors of a node in the containment hierarchy * * @param nodeId - ID of the node * @returns Array of ancestor nodes from immediate parent to root */ getAncestors(nodeId: string): Node[] { return this.traverser.getAncestors(nodeId); } /** * Get immediate children of a node * * @param nodeId - ID of the node * @returns Array of child nodes */ getChildren(nodeId: string): Node[] { return this.traverser.getChildren(nodeId); } /** * Get dependencies of a file * * @param filePath - Path to the file * @returns Array of file paths this file depends on */ getFileDependencies(filePath: string): string[] { return this.graphManager.getFileDependencies(filePath); } /** * Get dependents of a file * * @param filePath - Path to the file * @returns Array of file paths that depend on this file */ getFileDependents(filePath: string): string[] { return this.graphManager.getFileDependents(filePath); } /** * Find circular dependencies in the codebase * * @returns Array of cycles, each cycle is an array of file paths */ findCircularDependencies(): string[][] { return this.graphManager.findCircularDependencies(); } /** * Find dead code (unreferenced symbols) * * @param kinds - Node kinds to check (default: functions, methods, classes) * @returns Array of unreferenced nodes */ findDeadCode(kinds?: Node['kind'][]): Node[] { return this.graphManager.findDeadCode(kinds); } /** * Get complexity metrics for a node * * @param nodeId - ID of the node * @returns Object containing various complexity metrics */ getNodeMetrics(nodeId: string): { incomingEdgeCount: number; outgoingEdgeCount: number; callCount: number; callerCount: number; childCount: number; depth: number; } { return this.graphManager.getNodeMetrics(nodeId); } // =========================================================================== // Context Building // =========================================================================== /** * Get the source code for a node * * Reads the file and extracts the code between startLine and endLine. * * @param nodeId - ID of the node * @returns Code string or null if not found */ async getCode(nodeId: string): Promise { return this.contextBuilder.getCode(nodeId); } /** * Find relevant subgraph for a query * * Combines semantic search with graph traversal to find the most * relevant nodes and their relationships for a given query. * * @param query - Natural language query describing the task * @param options - Search and traversal options * @returns Subgraph of relevant nodes and edges */ async findRelevantContext( query: string, options?: FindRelevantContextOptions ): Promise { return this.contextBuilder.findRelevantContext(query, options); } /** * Build context for a task * * Creates comprehensive context by: * 1. Running FTS search to find entry points * 2. Expanding the graph around entry points * 3. Extracting code blocks for key nodes * 4. Formatting output for Claude * * @param input - Task description (string or {title, description}) * @param options - Build options (maxNodes, includeCode, format, etc.) * @returns TaskContext object or formatted string (markdown/JSON) */ async buildContext( input: TaskInput, options?: BuildContextOptions ): Promise { return this.contextBuilder.buildContext(input, options); } // =========================================================================== // Database Management // =========================================================================== /** * Optimize the database (vacuum and analyze) */ optimize(): void { this.db.optimize(); } /** * Clear all data from the graph */ clear(): void { this.queries.clear(); } /** * Alias for close() for backwards compatibility. * @deprecated Use close() instead */ destroy(): void { this.close(); } /** * Completely remove CodeGraph from the project. * This closes the database and deletes the .CodeGraph directory. * * WARNING: This permanently deletes all CodeGraph data for the project. */ uninitialize(): void { this.close(); removeDirectory(this.projectRoot); } } // Default export export default CodeGraph;