mnemos-coder
Version:
CLI-based coding agent with graph-based execution loop and terminal UI
270 lines • 9.66 kB
JavaScript
/**
* 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