orchestry-mcp
Version:
Orchestry MCP Server for multi-session task management
243 lines (208 loc) • 6.7 kB
text/typescript
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);
}
}