UNPKG

orchestry-mcp

Version:

Orchestry MCP Server for multi-session task management

243 lines (208 loc) 6.7 kB
import BetterSqlite3 from 'better-sqlite3'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { Database } from './database.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** * Manages multiple project databases * Each project gets its own isolated SQLite database */ export class DatabaseManager { private databases: Map<string, Database> = new Map(); private dbDir: string; private metaDb: BetterSqlite3.Database; constructor(baseDir?: string) { // Create directory for project databases this.dbDir = baseDir || path.join(__dirname, '..', 'project-data'); if (!fs.existsSync(this.dbDir)) { fs.mkdirSync(this.dbDir, { recursive: true }); } // Meta database to track all projects const metaDbPath = path.join(this.dbDir, 'projects-meta.db'); this.metaDb = new BetterSqlite3(metaDbPath); this.metaDb.pragma('journal_mode = WAL'); this.initializeMetaDb(); } private initializeMetaDb() { // Store metadata about all projects this.metaDb.exec(` CREATE TABLE IF NOT EXISTS projects_meta ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, db_path TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT 1 ) `); } /** * Get or create a database for a specific project */ async getProjectDatabase(projectId: string): Promise<Database> { // Check if we already have this database open if (this.databases.has(projectId)) { this.updateLastAccessed(projectId); return this.databases.get(projectId)!; } // Check if project exists in meta database const projectMeta = this.metaDb.prepare( 'SELECT * FROM projects_meta WHERE id = ?' ).get(projectId) as any; let dbPath: string; if (projectMeta) { dbPath = projectMeta.db_path; this.updateLastAccessed(projectId); } else { // Create new project database dbPath = path.join(this.dbDir, `project-${projectId}.db`); this.metaDb.prepare(` INSERT INTO projects_meta (id, name, description, db_path) VALUES (?, ?, ?, ?) `).run(projectId, `Project ${projectId}`, '', dbPath); } // Create and initialize the database const db = new Database(dbPath); await db.initialize(); // Cache it this.databases.set(projectId, db); return db; } /** * Create a new project with its own database */ async createProject(name: string, description?: string): Promise<string> { const projectId = this.generateProjectId(name); const dbPath = path.join(this.dbDir, `project-${projectId}.db`); // Check if project already exists const existing = this.metaDb.prepare( 'SELECT id FROM projects_meta WHERE id = ?' ).get(projectId); if (existing) { throw new Error(`Project ${projectId} already exists`); } // Create meta entry this.metaDb.prepare(` INSERT INTO projects_meta (id, name, description, db_path) VALUES (?, ?, ?, ?) `).run(projectId, name, description || '', dbPath); // Create and initialize the project database const db = new Database(dbPath); await db.initialize(); // Create initial project entry in the project's own database await db.createProject(name, description || ''); // Cache it this.databases.set(projectId, db); return projectId; } /** * List all available projects */ listProjects(): Array<{ id: string; name: string; description: string; createdAt: string; lastAccessed: string; isActive: boolean; }> { const projects = this.metaDb.prepare(` SELECT id, name, description, created_at as createdAt, last_accessed as lastAccessed, is_active as isActive FROM projects_meta WHERE is_active = 1 ORDER BY last_accessed DESC `).all() as any[]; return projects; } /** * Switch to a different project */ async switchProject(projectId: string): Promise<Database> { const db = await this.getProjectDatabase(projectId); this.updateLastAccessed(projectId); return db; } /** * Archive a project (soft delete) */ archiveProject(projectId: string): void { this.metaDb.prepare(` UPDATE projects_meta SET is_active = 0 WHERE id = ? `).run(projectId); // Remove from cache if (this.databases.has(projectId)) { const db = this.databases.get(projectId); // Close the database connection if possible this.databases.delete(projectId); } } /** * Delete a project permanently */ deleteProject(projectId: string): void { const projectMeta = this.metaDb.prepare( 'SELECT db_path FROM projects_meta WHERE id = ?' ).get(projectId) as any; if (projectMeta) { // Remove from cache and close connection if (this.databases.has(projectId)) { this.databases.delete(projectId); } // Delete the database file if (fs.existsSync(projectMeta.db_path)) { fs.unlinkSync(projectMeta.db_path); // Also delete WAL and SHM files if they exist const walPath = projectMeta.db_path + '-wal'; const shmPath = projectMeta.db_path + '-shm'; if (fs.existsSync(walPath)) fs.unlinkSync(walPath); if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath); } // Remove from meta database this.metaDb.prepare( 'DELETE FROM projects_meta WHERE id = ?' ).run(projectId); } } /** * Get current active project */ getCurrentProject(): string | null { const result = this.metaDb.prepare(` SELECT id FROM projects_meta WHERE is_active = 1 ORDER BY last_accessed DESC LIMIT 1 `).get() as any; return result ? result.id : null; } /** * Close all database connections */ closeAll(): void { for (const [_, db] of this.databases) { // Database will be closed when object is destroyed } this.databases.clear(); this.metaDb.close(); } private updateLastAccessed(projectId: string): void { this.metaDb.prepare(` UPDATE projects_meta SET last_accessed = CURRENT_TIMESTAMP WHERE id = ? `).run(projectId); } private generateProjectId(name: string): string { // Generate a URL-safe project ID from the name return name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .substring(0, 50); } }