cntx-ui
Version:
Autonomous Repository Intelligence engine with web UI and MCP server. Unified semantic code understanding, local RAG, and agent working memory.
326 lines (319 loc) • 11.8 kB
JavaScript
/**
* Database Manager for cntx-ui
* Handles SQLite operations for bundle and file data
*/
import Database from 'better-sqlite3';
import { join } from 'path';
import { statSync } from 'fs';
export default class DatabaseManager {
dbPath;
db;
verbose;
constructor(dbDir, options = {}) {
this.dbPath = join(dbDir, 'bundles.db');
this.verbose = options.verbose || false;
try {
this.db = new Database(this.dbPath);
if (this.verbose) {
console.log(`📊 SQLite database initialized: ${this.dbPath}`);
}
this.initSchema();
}
catch (error) {
console.error('Failed to initialize SQLite database:', error.message);
throw error;
}
}
initSchema() {
this.db.exec(`
-- Existing bundles table
CREATE TABLE IF NOT EXISTS bundles (
name TEXT PRIMARY KEY,
patterns TEXT NOT NULL,
files TEXT NOT NULL,
size INTEGER DEFAULT 0,
file_count INTEGER DEFAULT 0,
generated_at TEXT,
changed BOOLEAN DEFAULT FALSE
);
-- Persistent Semantic Chunks
CREATE TABLE IF NOT EXISTS semantic_chunks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
file_path TEXT NOT NULL,
type TEXT,
subtype TEXT,
content TEXT NOT NULL,
start_line INTEGER,
complexity_score INTEGER,
purpose TEXT,
metadata TEXT, -- JSON string for tags, imports, types, etc.
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Vector Embeddings (Persistence for RAG)
CREATE TABLE IF NOT EXISTS vector_embeddings (
chunk_id TEXT PRIMARY KEY,
embedding BLOB NOT NULL, -- Stored as Float32Array blob
model_name TEXT NOT NULL,
FOREIGN KEY(chunk_id) REFERENCES semantic_chunks(id) ON DELETE CASCADE
);
-- Agent Working Memory & Sessions
CREATE TABLE IF NOT EXISTS agent_sessions (
id TEXT PRIMARY KEY,
title TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_active_at DATETIME DEFAULT CURRENT_TIMESTAMP,
context_summary TEXT -- High-level summary of what was being worked on
);
CREATE TABLE IF NOT EXISTS agent_memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL, -- 'user' or 'agent'
content TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
metadata TEXT, -- JSON for tools used, files referenced, etc.
FOREIGN KEY(session_id) REFERENCES agent_sessions(id) ON DELETE CASCADE
);
-- UMAP Projection Cache
CREATE TABLE IF NOT EXISTS umap_projections (
chunk_id TEXT PRIMARY KEY,
x REAL NOT NULL,
y REAL NOT NULL,
computed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
embedding_count INTEGER NOT NULL,
FOREIGN KEY(chunk_id) REFERENCES semantic_chunks(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_bundles_changed ON bundles(changed);
CREATE INDEX IF NOT EXISTS idx_chunks_file ON semantic_chunks(file_path);
CREATE INDEX IF NOT EXISTS idx_chunks_purpose ON semantic_chunks(purpose);
CREATE INDEX IF NOT EXISTS idx_memory_session ON agent_memory(session_id);
`);
}
/**
* Run a raw SELECT query against the database
*/
query(sql) {
try {
if (!sql.trim().toUpperCase().startsWith('SELECT')) {
throw new Error('Only SELECT queries are allowed');
}
const stmt = this.db.prepare(sql);
return stmt.all();
}
catch (error) {
console.error('Query failed:', error.message);
throw error;
}
}
// Get database info for debugging
getInfo() {
try {
const bundleCountRow = this.db.prepare('SELECT COUNT(*) as count FROM bundles').get();
const chunkCountRow = this.db.prepare('SELECT COUNT(*) as count FROM semantic_chunks').get();
const embeddingCountRow = this.db.prepare('SELECT COUNT(*) as count FROM vector_embeddings').get();
const sessionCountRow = this.db.prepare('SELECT COUNT(*) as count FROM agent_sessions').get();
const dbSize = statSync(this.dbPath).size;
return {
path: this.dbPath,
bundleCount: bundleCountRow.count,
chunkCount: chunkCountRow.count,
embeddingCount: embeddingCountRow.count,
sessionCount: sessionCountRow.count,
sizeBytes: dbSize,
sizeFormatted: (dbSize / 1024).toFixed(1) + ' KB',
tables: ['bundles', 'semantic_chunks', 'vector_embeddings', 'agent_sessions', 'agent_memory']
};
}
catch (error) {
return { error: error.message };
}
}
// Save semantic chunks
saveChunks(chunks) {
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO semantic_chunks (id, name, file_path, type, subtype, content, start_line, complexity_score, purpose, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const transaction = this.db.transaction(() => {
for (const chunk of chunks) {
// Generate a stable ID if not provided (org/repo:path:name)
const id = chunk.id || `${chunk.filePath}:${chunk.name}:${chunk.startLine}`;
stmt.run(id, chunk.name, chunk.filePath, chunk.type || 'unknown', chunk.subtype || 'unknown', chunk.code || chunk.content || '', chunk.startLine || 0, chunk.complexity?.score || 0, chunk.purpose || 'Utility function', JSON.stringify({
tags: chunk.tags || [],
businessDomain: chunk.businessDomain || [],
technicalPatterns: chunk.technicalPatterns || [],
imports: chunk.includes?.imports || [],
types: chunk.includes?.types || [],
bundles: chunk.bundles || []
}));
}
});
try {
transaction();
return true;
}
catch (error) {
console.error('Failed to save chunks to SQLite:', error.message);
return false;
}
}
// Get chunks for a specific file
getChunksByFile(filePath) {
try {
const rows = this.db.prepare('SELECT * FROM semantic_chunks WHERE file_path = ?').all(filePath);
return rows.map(row => this.mapChunkRow(row));
}
catch (error) {
return [];
}
}
// Search chunks by name or purpose
searchChunks(query) {
try {
const rows = this.db.prepare(`
SELECT * FROM semantic_chunks
WHERE name LIKE ? OR purpose LIKE ?
LIMIT 50
`).all(`%${query}%`, `%${query}%`);
return rows.map(row => this.mapChunkRow(row));
}
catch (error) {
return [];
}
}
mapChunkRow(row) {
const metadata = JSON.parse(row.metadata || '{}');
return {
id: row.id,
name: row.name,
filePath: row.file_path,
type: row.type,
subtype: row.subtype,
code: row.content,
startLine: row.start_line,
complexity: {
score: row.complexity_score,
level: row.complexity_score < 5 ? 'low' : row.complexity_score < 15 ? 'medium' : 'high'
},
purpose: row.purpose,
tags: metadata.tags || [],
businessDomain: metadata.businessDomain || [],
technicalPatterns: metadata.technicalPatterns || [],
includes: {
imports: metadata.imports || [],
types: metadata.types || []
},
bundles: metadata.bundles || []
};
}
// Vector Embedding Persistence
saveEmbedding(chunkId, embedding, modelName) {
try {
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO vector_embeddings (chunk_id, embedding, model_name)
VALUES (?, ?, ?)
`);
// Convert Float32Array to Buffer for SQLite BLOB
const buffer = Buffer.from(embedding.buffer);
stmt.run(chunkId, buffer, modelName);
return true;
}
catch (error) {
console.error(`Failed to save embedding for ${chunkId}:`, error.message);
return false;
}
}
getEmbedding(chunkId) {
try {
const row = this.db.prepare('SELECT embedding FROM vector_embeddings WHERE chunk_id = ?').get(chunkId);
if (!row)
return null;
// Convert Buffer back to Float32Array
return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
}
catch (error) {
return null;
}
}
// Agent Memory Methods
createSession(id, title) {
try {
const stmt = this.db.prepare('INSERT OR REPLACE INTO agent_sessions (id, title) VALUES (?, ?)');
stmt.run(id, title);
return true;
}
catch (error) {
return false;
}
}
addMessage(sessionId, role, content, metadata = {}) {
try {
const stmt = this.db.prepare(`
INSERT INTO agent_memory (session_id, role, content, metadata)
VALUES (?, ?, ?, ?)
`);
stmt.run(sessionId, role, content, JSON.stringify(metadata));
// Update session last_active_at
this.db.prepare('UPDATE agent_sessions SET last_active_at = CURRENT_TIMESTAMP WHERE id = ?').run(sessionId);
return true;
}
catch (error) {
return false;
}
}
getSessionHistory(sessionId) {
try {
return this.db.prepare('SELECT * FROM agent_memory WHERE session_id = ? ORDER BY timestamp ASC').all(sessionId);
}
catch (error) {
return [];
}
}
// UMAP Projection Cache
saveProjections(projections, embeddingCount) {
const transaction = this.db.transaction(() => {
this.db.prepare('DELETE FROM umap_projections').run();
const stmt = this.db.prepare('INSERT INTO umap_projections (chunk_id, x, y, embedding_count) VALUES (?, ?, ?, ?)');
for (const p of projections) {
stmt.run(p.chunkId, p.x, p.y, embeddingCount);
}
});
try {
transaction();
return true;
}
catch (error) {
console.error('Failed to save projections:', error.message);
return false;
}
}
getProjections() {
try {
const rows = this.db.prepare('SELECT chunk_id, x, y, embedding_count FROM umap_projections').all();
if (rows.length === 0)
return null;
return rows.map(r => ({ chunkId: r.chunk_id, x: r.x, y: r.y, embeddingCount: r.embedding_count }));
}
catch (error) {
return null;
}
}
getProjectionEmbeddingCount() {
try {
const row = this.db.prepare('SELECT embedding_count FROM umap_projections LIMIT 1').get();
return row?.embedding_count ?? 0;
}
catch (error) {
return 0;
}
}
// Close database connection
close() {
if (this.db) {
this.db.close();
if (this.verbose) {
console.log('📊 SQLite database connection closed');
}
}
}
}