| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import { existsSync } from "node:fs";
|
| import { dirname, join } from "node:path";
|
| import { fileURLToPath } from "node:url";
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| let _db: unknown = null;
|
|
|
| function getDbPath(): string | null {
|
|
|
| const candidates = [
|
| join(process.cwd(), ".codegraph", "codegraph.db"),
|
| join(process.cwd(), "..", ".codegraph", "codegraph.db"),
|
| ];
|
|
|
|
|
| const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
| 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" };
|
| }
|
|
|
| _db = null;
|
|
|
|
|
| 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",
|
| };
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
| |
| |
|
|
| 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 ?
|
| `;
|
|
|
|
|
| const sanitized = query.replace(/[^a-zA-Z0-9_]/g, " ").trim();
|
| if (!sanitized) {
|
|
|
| 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]);
|
| }
|
|
|
| |
| |
|
|
| 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]
|
| );
|
| }
|
|
|
| |
| |
|
|
| 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]
|
| );
|
| }
|
|
|
| |
| |
|
|
| export function getFileContext(filePath: string): CodeGraphQueryResult {
|
|
|
| return queryDb(
|
| `SELECT * FROM nodes
|
| WHERE file_path LIKE ? OR file_path = ?
|
| ORDER BY start_line
|
| LIMIT 100`,
|
| [`%${filePath}`, filePath]
|
| );
|
| }
|
|
|
| |
| |
|
|
| 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]);
|
| }
|
|
|
| |
| |
|
|
| export function getImpactAnalysis(symbolName: string, depth = 1): CodeGraphQueryResult {
|
| if (depth <= 0)
|
| return { success: false, data: null, error: "Depth must be >= 1", engine: "none" };
|
|
|
|
|
| const directCallers = findCallers(symbolName);
|
| if (depth === 1) return directCallers;
|
|
|
|
|
|
|
| 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.`,
|
| })),
|
| };
|
| }
|
|
|
| |
| |
|
|
| export function isCodeGraphAvailable(): boolean {
|
| return getDbPath() !== null;
|
| }
|
|
|
| |
| |
|
|
| 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`);
|
| }
|
|
|