orchestry-mcp
Version:
Orchestry MCP Server for multi-session task management
197 lines • 6.73 kB
JavaScript
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 {
databases = new Map();
dbDir;
metaDb;
constructor(baseDir) {
// 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();
}
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) {
// 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);
let dbPath;
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, description) {
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() {
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();
return projects;
}
/**
* Switch to a different project
*/
async switchProject(projectId) {
const db = await this.getProjectDatabase(projectId);
this.updateLastAccessed(projectId);
return db;
}
/**
* Archive a project (soft delete)
*/
archiveProject(projectId) {
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) {
const projectMeta = this.metaDb.prepare('SELECT db_path FROM projects_meta WHERE id = ?').get(projectId);
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() {
const result = this.metaDb.prepare(`
SELECT id FROM projects_meta
WHERE is_active = 1
ORDER BY last_accessed DESC
LIMIT 1
`).get();
return result ? result.id : null;
}
/**
* Close all database connections
*/
closeAll() {
for (const [_, db] of this.databases) {
// Database will be closed when object is destroyed
}
this.databases.clear();
this.metaDb.close();
}
updateLastAccessed(projectId) {
this.metaDb.prepare(`
UPDATE projects_meta
SET last_accessed = CURRENT_TIMESTAMP
WHERE id = ?
`).run(projectId);
}
generateProjectId(name) {
// Generate a URL-safe project ID from the name
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.substring(0, 50);
}
}
//# sourceMappingURL=database-manager.js.map