UNPKG

@context-sync/server

Version:

MCP server for AI context sync with persistent memory, workspace file access, and intelligent code operations

238 lines (235 loc) 7.81 kB
import Database from 'better-sqlite3'; import { randomUUID } from 'crypto'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; import { createTodoTable } from './todo-schema.js'; export class Storage { db; // Prepared statement cache for 2-5x faster queries preparedStatements = new Map(); constructor(dbPath) { // Default to user's home directory const defaultPath = path.join(os.homedir(), '.context-sync', 'data.db'); const actualPath = dbPath || defaultPath; // Ensure directory exists const dir = path.dirname(actualPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } this.db = new Database(actualPath); this.initDatabase(); createTodoTable(this.db); } initDatabase() { // Create tables this.db.exec(` CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL, path TEXT, architecture TEXT, tech_stack TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, is_current INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS conversations ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL, tool TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, timestamp INTEGER NOT NULL, metadata TEXT, FOREIGN KEY (project_id) REFERENCES projects(id) ); CREATE TABLE IF NOT EXISTS decisions ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL, type TEXT NOT NULL, description TEXT NOT NULL, reasoning TEXT, timestamp INTEGER NOT NULL, FOREIGN KEY (project_id) REFERENCES projects(id) ); CREATE INDEX IF NOT EXISTS idx_conversations_project ON conversations(project_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_decisions_project ON decisions(project_id, timestamp DESC); `); } /** * Get or create a prepared statement for faster queries (2-5x performance improvement) */ getStatement(sql) { if (this.preparedStatements.has(sql)) { return this.preparedStatements.get(sql); } const statement = this.db.prepare(sql); this.preparedStatements.set(sql, statement); return statement; } createProject(name, projectPath) { const id = randomUUID(); const now = Date.now(); const techStack = []; // Set this as current project (unset others) - use cached prepared statement this.getStatement('UPDATE projects SET is_current = 0').run(); // Insert new project - use cached prepared statement this.getStatement(` INSERT INTO projects (id, name, path, tech_stack, created_at, updated_at, is_current) VALUES (?, ?, ?, ?, ?, ?, 1) `).run(id, name, projectPath || null, JSON.stringify(techStack), now, now); return { id, name, path: projectPath, techStack, createdAt: new Date(now), updatedAt: new Date(now), }; } getProject(id) { const row = this.getStatement(` SELECT * FROM projects WHERE id = ? `).get(id); if (!row) return null; return this.rowToProject(row); } getCurrentProject() { const row = this.getStatement(` SELECT * FROM projects WHERE is_current = 1 LIMIT 1 `).get(); if (!row) return null; return this.rowToProject(row); } updateProject(id, updates) { const sets = []; const values = []; if (updates.name) { sets.push('name = ?'); values.push(updates.name); } if (updates.architecture) { sets.push('architecture = ?'); values.push(updates.architecture); } if (updates.techStack) { sets.push('tech_stack = ?'); values.push(JSON.stringify(updates.techStack)); } sets.push('updated_at = ?'); values.push(Date.now()); values.push(id); this.getStatement(` UPDATE projects SET ${sets.join(', ')} WHERE id = ? `).run(...values); } addConversation(conv) { const id = randomUUID(); const timestamp = Date.now(); this.getStatement(` INSERT INTO conversations (id, project_id, tool, role, content, timestamp, metadata) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(id, conv.projectId, conv.tool, conv.role, conv.content, timestamp, conv.metadata ? JSON.stringify(conv.metadata) : null); return { id, ...conv, timestamp: new Date(timestamp), }; } findProjectByPath(projectPath) { const row = this.getStatement(` SELECT * FROM projects WHERE path = ? `).get(projectPath); if (!row) return null; return this.rowToProject(row); } getRecentConversations(projectId, limit = 10) { const rows = this.getStatement(` SELECT * FROM conversations WHERE project_id = ? ORDER BY timestamp DESC LIMIT ? `).all(projectId, limit); return rows.map(row => ({ id: row.id, projectId: row.project_id, tool: row.tool, role: row.role, content: row.content, timestamp: new Date(row.timestamp), metadata: row.metadata ? JSON.parse(row.metadata) : undefined, })); } addDecision(decision) { const id = randomUUID(); const timestamp = Date.now(); this.getStatement(` INSERT INTO decisions (id, project_id, type, description, reasoning, timestamp) VALUES (?, ?, ?, ?, ?, ?) `).run(id, decision.projectId, decision.type, decision.description, decision.reasoning || null, timestamp); return { id, ...decision, timestamp: new Date(timestamp), }; } getDecisions(projectId) { const rows = this.getStatement(` SELECT * FROM decisions WHERE project_id = ? ORDER BY timestamp DESC `).all(projectId); return rows.map(row => ({ id: row.id, projectId: row.project_id, type: row.type, description: row.description, reasoning: row.reasoning, timestamp: new Date(row.timestamp), })); } getContextSummary(projectId) { const project = this.getProject(projectId); if (!project) { throw new Error(`Project ${projectId} not found`); } const recentDecisions = this.getDecisions(projectId).slice(0, 5); const recentConversations = this.getRecentConversations(projectId, 20); // Extract key points from decisions const keyPoints = [ project.architecture ? `Architecture: ${project.architecture}` : null, ...project.techStack.map(tech => `Using: ${tech}`), ...recentDecisions.map(d => d.description), ].filter(Boolean); return { project, recentDecisions, recentConversations, keyPoints, }; } rowToProject(row) { return { id: row.id, name: row.name, path: row.path, architecture: row.architecture, techStack: row.tech_stack ? JSON.parse(row.tech_stack) : [], createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), }; } getDb() { return this.db; } close() { this.db.close(); } } //# sourceMappingURL=storage.js.map