Spaces:
Runtime error
Runtime error
| /** | |
| * Copilot CodeGraph Knowledge Module | |
| * | |
| * Provides the Copilot with read-only access to the project's CodeGraph index. | |
| * Queries the `.codegraph/codegraph.db` SQLite database to find symbols, | |
| * explore relationships, list files, and search documentation. | |
| * | |
| * Falls back gracefully if the CodeGraph DB does not exist (e.g., production installs). | |
| */ | |
| import { existsSync } from "node:fs"; | |
| import { dirname, join } from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| // --------------------------------------------------------------------------- | |
| // Types | |
| // --------------------------------------------------------------------------- | |
| export interface CodeGraphNode { | |
| id: string; | |
| kind: string; | |
| name: string; | |
| qualifiedName: string; | |
| filePath: string; | |
| language: string; | |
| startLine: number; | |
| endLine: number; | |
| signature?: string; | |
| docstring?: string; | |
| isExported: boolean; | |
| visibility?: string; | |
| } | |
| export interface CodeGraphEdge { | |
| id: number; | |
| source: string; | |
| target: string; | |
| kind: string; | |
| line?: number; | |
| metadata?: Record<string, unknown>; | |
| } | |
| export interface CodeGraphFile { | |
| path: string; | |
| language: string; | |
| nodeCount: number; | |
| modifiedAt: number; | |
| } | |
| export interface CodeGraphSearchResult { | |
| nodes: CodeGraphNode[]; | |
| total: number; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Database access (lazy loaded) | |
| // --------------------------------------------------------------------------- | |
| let _db: unknown = null; | |
| function getDbPath(): string | null { | |
| // Try project root first (dev), then cwd, then DATA_DIR | |
| const candidates = [ | |
| join(process.cwd(), ".codegraph", "codegraph.db"), | |
| join(process.cwd(), "..", ".codegraph", "codegraph.db"), | |
| ]; | |
| // Try to resolve from the project root | |
| const __dirname = dirname(fileURLToPath(import.meta.url)); | |
| // Walk up to find .codegraph/ | |
| let dir = __dirname; | |
| for (let i = 0; i < 10; i++) { | |
| const candidate = join(dir, ".codegraph", "codegraph.db"); | |
| if (existsSync(candidate)) return candidate; | |
| const parent = join(dir, ".."); | |
| if (parent === dir) break; | |
| dir = parent; | |
| } | |
| for (const c of candidates) { | |
| if (existsSync(c)) return c; | |
| } | |
| return null; | |
| } | |
| export interface CodeGraphQueryResult { | |
| success: boolean; | |
| data: unknown; | |
| error?: string; | |
| engine: "sqlite" | "cli" | "none"; | |
| } | |
| function queryDb(query: string, params: unknown[] = []): CodeGraphQueryResult { | |
| try { | |
| if (!_db) { | |
| const dbPath = getDbPath(); | |
| if (!dbPath) { | |
| return { success: false, data: null, error: "CodeGraph DB not found", engine: "none" }; | |
| } | |
| // Dynamic import to avoid hard dependency on better-sqlite3 | |
| _db = null; | |
| // Use better-sqlite3 if available | |
| try { | |
| const Database = require("better-sqlite3"); | |
| _db = new Database(dbPath, { readonly: true }); | |
| } catch { | |
| return { | |
| success: false, | |
| data: null, | |
| error: "better-sqlite3 not available", | |
| engine: "none", | |
| }; | |
| } | |
| } | |
| const stmt = ( | |
| _db as { prepare: (sql: string) => { all: (params: unknown[]) => unknown[] } } | |
| ).prepare(query); | |
| const rows = stmt.all(params); | |
| return { success: true, data: rows, engine: "sqlite" }; | |
| } catch (err) { | |
| return { | |
| success: false, | |
| data: null, | |
| error: err instanceof Error ? err.message : "Unknown error", | |
| engine: "none", | |
| }; | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Search operations | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Search symbols by name (exact or partial match via FTS). | |
| */ | |
| export function searchSymbols(query: string, limit = 20): CodeGraphQueryResult { | |
| const sql = ` | |
| SELECT n.* | |
| FROM nodes n | |
| JOIN nodes_fts fts ON n.id = fts.id | |
| WHERE nodes_fts MATCH ? | |
| ORDER BY rank | |
| LIMIT ? | |
| `; | |
| // Escape FTS special chars and create prefix query | |
| const sanitized = query.replace(/[^a-zA-Z0-9_]/g, " ").trim(); | |
| if (!sanitized) { | |
| // Fallback to LIKE if query is empty after sanitization | |
| return queryDb(`SELECT * FROM nodes WHERE lower(name) LIKE ? ORDER BY kind, name LIMIT ?`, [ | |
| `%${query.toLowerCase()}%`, | |
| limit, | |
| ]); | |
| } | |
| const ftsQuery = sanitized | |
| .split(/\s+/) | |
| .map((w) => `"${w}"*`) | |
| .join(" AND "); | |
| return queryDb(sql, [ftsQuery, limit]); | |
| } | |
| /** | |
| * Find callers of a symbol (edges where target matches). | |
| */ | |
| export function findCallers(symbolName: string, limit = 20): CodeGraphQueryResult { | |
| return queryDb( | |
| `SELECT e.id as edgeId, e.kind as edgeKind, e.line, e.col, | |
| s.id as sourceId, s.name as sourceName, s.kind as sourceKind, | |
| s.file_path as sourceFile, s.start_line as sourceLine, | |
| t.name as targetName, t.file_path as targetFile | |
| FROM edges e | |
| JOIN nodes s ON e.source = s.id | |
| JOIN nodes t ON e.target = t.id | |
| WHERE t.name = ? | |
| ORDER BY e.kind | |
| LIMIT ?`, | |
| [symbolName, limit] | |
| ); | |
| } | |
| /** | |
| * Find callees of a symbol (edges where source matches). | |
| */ | |
| export function findCallees(symbolName: string, limit = 20): CodeGraphQueryResult { | |
| return queryDb( | |
| `SELECT e.id as edgeId, e.kind as edgeKind, e.line, e.col, | |
| s.name as sourceName, | |
| t.id as targetId, t.name as targetName, t.kind as targetKind, | |
| t.file_path as targetFile, t.start_line as targetLine | |
| FROM edges e | |
| JOIN nodes s ON e.source = s.id | |
| JOIN nodes t ON e.target = t.id | |
| WHERE s.name = ? | |
| ORDER BY e.kind | |
| LIMIT ?`, | |
| [symbolName, limit] | |
| ); | |
| } | |
| /** | |
| * Get context for a file: all symbols defined in it. | |
| */ | |
| export function getFileContext(filePath: string): CodeGraphQueryResult { | |
| // Try matching on suffix of file_path (many nodes store paths relative to root) | |
| return queryDb( | |
| `SELECT * FROM nodes | |
| WHERE file_path LIKE ? OR file_path = ? | |
| ORDER BY start_line | |
| LIMIT 100`, | |
| [`%${filePath}`, filePath] | |
| ); | |
| } | |
| /** | |
| * List all indexed files, optionally filtered by language. | |
| */ | |
| export function listFiles(language?: string, limit = 50): CodeGraphQueryResult { | |
| if (language) { | |
| return queryDb(`SELECT * FROM files WHERE language = ? ORDER BY path LIMIT ?`, [ | |
| language, | |
| limit, | |
| ]); | |
| } | |
| return queryDb(`SELECT * FROM files ORDER BY path LIMIT ?`, [limit]); | |
| } | |
| /** | |
| * Get impact analysis: find symbols that depend on a given symbol (transitively). | |
| */ | |
| export function getImpactAnalysis(symbolName: string, depth = 1): CodeGraphQueryResult { | |
| if (depth <= 0) | |
| return { success: false, data: null, error: "Depth must be >= 1", engine: "none" }; | |
| // Direct callers (depth 1) | |
| const directCallers = findCallers(symbolName); | |
| if (depth === 1) return directCallers; | |
| // For depth > 1, we'd need recursive CTE or multiple queries. | |
| // For now, just return direct callers with a note. | |
| const result = directCallers; | |
| return { | |
| ...result, | |
| data: (result.data as Record<string, unknown>[])?.map((r) => ({ | |
| ...r, | |
| _depth: 1, | |
| _note: `Depth > 1 requires multiple queries. Use searchSymbols() + findCallers() iteratively for deeper analysis.`, | |
| })), | |
| }; | |
| } | |
| /** | |
| * Check if CodeGraph DB is available. | |
| */ | |
| export function isCodeGraphAvailable(): boolean { | |
| return getDbPath() !== null; | |
| } | |
| /** | |
| * Get summary stats from the index. | |
| */ | |
| export function getCodeGraphStats(): CodeGraphQueryResult { | |
| return queryDb(`SELECT 'total_nodes' as key, COUNT(*) as value FROM nodes UNION ALL | |
| SELECT 'total_edges', COUNT(*) FROM edges UNION ALL | |
| SELECT 'total_files', COUNT(*) FROM files UNION ALL | |
| SELECT 'languages', GROUP_CONCAT(DISTINCT language) FROM files UNION ALL | |
| SELECT 'node_kinds', GROUP_CONCAT(DISTINCT kind) FROM nodes`); | |
| } | |