UNPKG

mnemos-coder

Version:

CLI-based coding agent with graph-based execution loop and terminal UI

270 lines 9.66 kB
/** * SQLite database for codebase context storage * Uses better-sqlite3 for performance and simplicity */ import Database from 'better-sqlite3'; import * as path from 'path'; import * as fs from 'fs'; import { createHash } from 'crypto'; export class CodebaseDatabase { db; dbPath; constructor(projectRoot) { const mnemosDir = path.join(projectRoot, '.mnemos'); if (!fs.existsSync(mnemosDir)) { fs.mkdirSync(mnemosDir, { recursive: true }); } this.dbPath = path.join(mnemosDir, 'codebase.db'); this.db = new Database(this.dbPath); this.initializeTables(); } initializeTables() { // Enable WAL mode for better concurrent access this.db.pragma('journal_mode = WAL'); this.db.pragma('synchronous = NORMAL'); this.db.pragma('cache_size = 1000000'); this.db.pragma('temp_store = memory'); // File information table this.db.exec(` CREATE TABLE IF NOT EXISTS files ( file_path TEXT PRIMARY KEY, content_hash TEXT NOT NULL, last_modified DATETIME NOT NULL, file_size INTEGER NOT NULL, language TEXT NOT NULL, chunk_count INTEGER DEFAULT 0, indexed_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Code chunks table this.db.exec(` CREATE TABLE IF NOT EXISTS code_chunks ( id TEXT PRIMARY KEY, file_path TEXT NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, content TEXT NOT NULL, chunk_type TEXT NOT NULL, language TEXT NOT NULL, content_hash TEXT NOT NULL, embedding BLOB, metadata TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (file_path) REFERENCES files (file_path) ON DELETE CASCADE ) `); // Indexes for performance this.db.exec(` CREATE INDEX IF NOT EXISTS idx_chunks_file_path ON code_chunks(file_path); CREATE INDEX IF NOT EXISTS idx_chunks_type ON code_chunks(chunk_type); CREATE INDEX IF NOT EXISTS idx_chunks_language ON code_chunks(language); CREATE INDEX IF NOT EXISTS idx_chunks_hash ON code_chunks(content_hash); CREATE INDEX IF NOT EXISTS idx_files_modified ON files(last_modified); CREATE INDEX IF NOT EXISTS idx_files_language ON files(language); `); // Search index table for text search this.db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS chunks_search USING fts5( id UNINDEXED, file_path UNINDEXED, content, chunk_type UNINDEXED, language UNINDEXED ); `); } /** * Store file information */ storeFile(fileInfo) { const stmt = this.db.prepare(` INSERT OR REPLACE INTO files (file_path, content_hash, last_modified, file_size, language, chunk_count, indexed_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `); stmt.run(fileInfo.file_path, fileInfo.content_hash, fileInfo.last_modified.toISOString(), fileInfo.file_size, fileInfo.language, fileInfo.chunk_count); } /** * Store code chunk with optional embedding */ storeChunk(chunk) { const stmt = this.db.prepare(` INSERT OR REPLACE INTO code_chunks (id, file_path, start_line, end_line, content, chunk_type, language, content_hash, embedding, metadata, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `); const embeddingBlob = chunk.embedding ? Buffer.from(new Float32Array(chunk.embedding).buffer) : null; const metadataJson = chunk.metadata ? JSON.stringify(chunk.metadata) : null; stmt.run(chunk.id, chunk.file_path, chunk.start_line, chunk.end_line, chunk.content, chunk.chunk_type, chunk.language, chunk.content_hash, embeddingBlob, metadataJson); // Update FTS index const ftsStmt = this.db.prepare(` INSERT OR REPLACE INTO chunks_search (id, file_path, content, chunk_type, language) VALUES (?, ?, ?, ?, ?) `); ftsStmt.run(chunk.id, chunk.file_path, chunk.content, chunk.chunk_type, chunk.language); } /** * Get file info by path */ getFileInfo(filePath) { const stmt = this.db.prepare(` SELECT * FROM files WHERE file_path = ? `); const row = stmt.get(filePath); if (!row) return null; return { file_path: row.file_path, content_hash: row.content_hash, last_modified: new Date(row.last_modified), file_size: row.file_size, language: row.language, chunk_count: row.chunk_count, indexed_at: new Date(row.indexed_at) }; } /** * Check if file needs reindexing */ needsReindexing(filePath, currentHash, currentModified) { const fileInfo = this.getFileInfo(filePath); if (!fileInfo) return true; return fileInfo.content_hash !== currentHash || fileInfo.last_modified.getTime() !== currentModified.getTime(); } /** * Get chunks by file path */ getChunksByFile(filePath) { const stmt = this.db.prepare(` SELECT * FROM code_chunks WHERE file_path = ? ORDER BY start_line ASC `); const rows = stmt.all(filePath); return rows.map(this.rowToChunk); } /** * Text search using FTS */ textSearch(query, limit = 10, fileTypes) { let sql = ` SELECT c.*, rank FROM chunks_search cs JOIN code_chunks c ON cs.id = c.id WHERE chunks_search MATCH ? `; const params = [query]; if (fileTypes && fileTypes.length > 0) { sql += ` AND c.language IN (${fileTypes.map(() => '?').join(',')})`; params.push(...fileTypes); } sql += ` ORDER BY rank LIMIT ?`; params.push(limit); const stmt = this.db.prepare(sql); const rows = stmt.all(...params); return rows.map(row => ({ chunk: this.rowToChunk(row), rank: row.rank })); } /** * Get chunks by type */ getChunksByType(chunkType, limit = 20) { const stmt = this.db.prepare(` SELECT * FROM code_chunks WHERE chunk_type = ? ORDER BY updated_at DESC LIMIT ? `); const rows = stmt.all(chunkType, limit); return rows.map(this.rowToChunk); } /** * Get all embeddings for vector search */ getAllEmbeddings() { const stmt = this.db.prepare(` SELECT id, embedding FROM code_chunks WHERE embedding IS NOT NULL `); const rows = stmt.all(); return rows.map(row => ({ id: row.id, embedding: Array.from(new Float32Array(row.embedding.buffer)) })); } /** * Get chunk by ID */ getChunkById(id) { const stmt = this.db.prepare(` SELECT * FROM code_chunks WHERE id = ? `); const row = stmt.get(id); return row ? this.rowToChunk(row) : null; } /** * Remove chunks for a file */ removeFileChunks(filePath) { const transaction = this.db.transaction(() => { // Remove from FTS index this.db.prepare(`DELETE FROM chunks_search WHERE file_path = ?`).run(filePath); // Remove chunks this.db.prepare(`DELETE FROM code_chunks WHERE file_path = ?`).run(filePath); // Remove file info this.db.prepare(`DELETE FROM files WHERE file_path = ?`).run(filePath); }); transaction(); } /** * Get database statistics */ getStats() { const totalFiles = this.db.prepare(`SELECT COUNT(*) as count FROM files`).get(); const totalChunks = this.db.prepare(`SELECT COUNT(*) as count FROM code_chunks`).get(); const embeddedChunks = this.db.prepare(`SELECT COUNT(*) as count FROM code_chunks WHERE embedding IS NOT NULL`).get(); const languages = this.db.prepare(`SELECT language, COUNT(*) as count FROM files GROUP BY language`).all(); const chunkTypes = this.db.prepare(`SELECT chunk_type, COUNT(*) as count FROM code_chunks GROUP BY chunk_type`).all(); return { totalFiles: totalFiles.count, totalChunks: totalChunks.count, embeddedChunks: embeddedChunks.count, languages: Object.fromEntries(languages.map((l) => [l.language, l.count])), chunkTypes: Object.fromEntries(chunkTypes.map((c) => [c.chunk_type, c.count])) }; } /** * Convert database row to CodeChunk */ rowToChunk(row) { return { id: row.id, file_path: row.file_path, start_line: row.start_line, end_line: row.end_line, content: row.content, chunk_type: row.chunk_type, language: row.language, content_hash: row.content_hash, embedding: row.embedding ? Array.from(new Float32Array(row.embedding.buffer)) : undefined, metadata: row.metadata ? JSON.parse(row.metadata) : undefined, created_at: new Date(row.created_at), updated_at: new Date(row.updated_at) }; } /** * Create content hash */ static createContentHash(content) { return createHash('sha256').update(content).digest('hex').substring(0, 16); } /** * Close database connection */ close() { this.db.close(); } } //# sourceMappingURL=database.js.map