UNPKG

orchestry-mcp

Version:

Orchestry MCP Server for multi-session task management

504 lines 18.6 kB
import BetterSqlite3 from 'better-sqlite3'; import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export class Database { db; constructor(dbPath) { const defaultPath = path.join(__dirname, '..', 'orchestry.db'); this.db = new BetterSqlite3(dbPath || defaultPath); this.db.pragma('journal_mode = WAL'); } async initialize() { // Projects table this.db.exec(` CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, session_id TEXT, llm_context TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL ) `); // Workspaces table this.db.exec(` CREATE TABLE IF NOT EXISTS workspaces ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT, color TEXT, icon TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE ) `); // Goals table this.db.exec(` CREATE TABLE IF NOT EXISTS goals ( id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, title TEXT NOT NULL, description TEXT, priority TEXT DEFAULT 'medium', target_date DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL, FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE ) `); // Tasks table this.db.exec(` CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, goal_id TEXT NOT NULL, parent_task_id TEXT, title TEXT NOT NULL, description TEXT, status TEXT DEFAULT 'backlog', priority TEXT DEFAULT 'medium', assignee_id TEXT, due_date DATE, start_date DATE, completed_date DATE, estimated_hours REAL, actual_hours REAL, session_notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL, FOREIGN KEY (goal_id) REFERENCES goals(id) ON DELETE CASCADE, FOREIGN KEY (parent_task_id) REFERENCES tasks(id) ON DELETE CASCADE ) `); // Sessions table this.db.exec(` CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL, name TEXT NOT NULL, type TEXT DEFAULT 'human', llm_model TEXT, context TEXT, is_active BOOLEAN DEFAULT 1, started_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_activity_at DATETIME DEFAULT CURRENT_TIMESTAMP, ended_at DATETIME, data TEXT NOT NULL, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE ) `); // Documents table this.db.exec(` CREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, entity_id TEXT NOT NULL, entity_type TEXT NOT NULL, type TEXT NOT NULL, title TEXT NOT NULL, content TEXT, url TEXT, version INTEGER DEFAULT 1, author_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL ) `); // Comments table this.db.exec(` CREATE TABLE IF NOT EXISTS comments ( id TEXT PRIMARY KEY, entity_id TEXT NOT NULL, entity_type TEXT NOT NULL, author_id TEXT NOT NULL, content TEXT NOT NULL, session_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL ) `); // Team members table this.db.exec(` CREATE TABLE IF NOT EXISTS team_members ( id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, role TEXT, avatar TEXT, is_bot BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL ) `); // Create indexes this.db.exec(` CREATE INDEX IF NOT EXISTS idx_workspaces_project ON workspaces(project_id); CREATE INDEX IF NOT EXISTS idx_goals_workspace ON goals(workspace_id); CREATE INDEX IF NOT EXISTS idx_tasks_goal ON tasks(goal_id); CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id); CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority); CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id); CREATE INDEX IF NOT EXISTS idx_sessions_active ON sessions(is_active); CREATE INDEX IF NOT EXISTS idx_documents_entity ON documents(entity_id, entity_type); CREATE INDEX IF NOT EXISTS idx_comments_entity ON comments(entity_id, entity_type); `); } // Project CRUD createProject(name, description, sessionId) { const project = { id: uuidv4(), name, description, sessionId, createdAt: new Date(), updatedAt: new Date(), workspaces: [], tags: [], team: [], }; const stmt = this.db.prepare(` INSERT INTO projects (id, name, description, session_id, data) VALUES (?, ?, ?, ?, ?) `); stmt.run(project.id, name, description, sessionId, JSON.stringify(project)); return project; } getProject(id) { const row = this.db.prepare('SELECT * FROM projects WHERE id = ?').get(id); if (!row) return null; const project = JSON.parse(row.data); project.workspaces = this.getWorkspacesByProject(id); return project; } getAllProjects() { const rows = this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all(); return rows.map(row => { const project = JSON.parse(row.data); project.workspaces = this.getWorkspacesByProject(project.id); return project; }); } createWorkspace(projectIdOrData, data) { // Handle both call signatures let projectId; let workspaceData; if (typeof projectIdOrData === 'string') { projectId = projectIdOrData; workspaceData = data || {}; } else { projectId = projectIdOrData.projectId; workspaceData = { name: projectIdOrData.name, description: projectIdOrData.description, }; } const workspace = { id: uuidv4(), projectId, name: workspaceData.name || 'New Workspace', description: workspaceData.description || '', color: workspaceData.color, icon: workspaceData.icon, goals: [], createdAt: new Date(), updatedAt: new Date(), }; const stmt = this.db.prepare(` INSERT INTO workspaces (id, project_id, name, description, color, icon, data) VALUES (?, ?, ?, ?, ?, ?, ?) `); stmt.run(workspace.id, projectId, workspace.name, workspace.description, workspace.color, workspace.icon, JSON.stringify(workspace)); return workspace; } getWorkspacesByProject(projectId) { const rows = this.db.prepare('SELECT * FROM workspaces WHERE project_id = ?').all(projectId); return rows.map(row => { const workspace = JSON.parse(row.data); workspace.goals = this.getGoalsByWorkspace(workspace.id); return workspace; }); } createGoal(workspaceIdOrData, data) { let workspaceId; let goalData; if (typeof workspaceIdOrData === 'string') { workspaceId = workspaceIdOrData; goalData = data || {}; } else { workspaceId = workspaceIdOrData.workspaceId; goalData = { title: workspaceIdOrData.title, description: workspaceIdOrData.description, }; } const goal = { id: uuidv4(), workspaceId, title: goalData.title || '', description: goalData.description || '', priority: goalData.priority || 'medium', targetDate: goalData.targetDate, successCriteria: goalData.successCriteria || [], tasks: [], documents: [], createdAt: new Date(), updatedAt: new Date(), }; const stmt = this.db.prepare(` INSERT INTO goals (id, workspace_id, title, description, priority, target_date, data) VALUES (?, ?, ?, ?, ?, ?, ?) `); stmt.run(goal.id, workspaceId, goal.title, goal.description, goal.priority, goal.targetDate?.toISOString(), JSON.stringify(goal)); return goal; } getGoalsByWorkspace(workspaceId) { const rows = this.db.prepare('SELECT * FROM goals WHERE workspace_id = ?').all(workspaceId); return rows.map(row => { const goal = JSON.parse(row.data); goal.tasks = this.getTasksByGoal(goal.id); return goal; }); } createTask(goalIdOrData, data) { let goalId; let taskData; if (typeof goalIdOrData === 'string') { goalId = goalIdOrData; taskData = data || {}; } else { goalId = goalIdOrData.goalId; taskData = { title: goalIdOrData.title, description: goalIdOrData.description, priority: goalIdOrData.priority, }; } const task = { id: uuidv4(), goalId, parentTaskId: taskData.parentTaskId, title: taskData.title || '', description: taskData.description || '', status: taskData.status || 'backlog', priority: taskData.priority || 'medium', assignee: taskData.assignee, dueDate: taskData.dueDate, startDate: taskData.startDate, completedDate: taskData.completedDate, estimatedHours: taskData.estimatedHours, actualHours: taskData.actualHours, checklist: taskData.checklist || [], dependencies: taskData.dependencies || [], comments: [], attachments: [], tags: taskData.tags || [], sessionNotes: taskData.sessionNotes, llmSuggestions: taskData.llmSuggestions || [], createdAt: new Date(), updatedAt: new Date(), }; const stmt = this.db.prepare(` INSERT INTO tasks ( id, goal_id, parent_task_id, title, description, status, priority, assignee_id, due_date, start_date, completed_date, estimated_hours, actual_hours, session_notes, data ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run(task.id, goalId, task.parentTaskId, task.title, task.description, task.status, task.priority, task.assignee?.id, task.dueDate?.toISOString(), task.startDate?.toISOString(), task.completedDate?.toISOString(), task.estimatedHours, task.actualHours, task.sessionNotes, JSON.stringify(task)); return task; } getTasksByGoal(goalId) { const rows = this.db.prepare('SELECT * FROM tasks WHERE goal_id = ? AND parent_task_id IS NULL').all(goalId); return rows.map(row => { const task = JSON.parse(row.data); // Get subtasks task.subtasks = this.getSubtasks(task.id); return task; }); } getSubtasks(parentTaskId) { const rows = this.db.prepare('SELECT * FROM tasks WHERE parent_task_id = ?').all(parentTaskId); return rows.map(row => JSON.parse(row.data)); } getTask(id) { const row = this.db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); if (!row) return null; return JSON.parse(row.data); } updateTaskStatus(id, status) { const stmt = this.db.prepare(` UPDATE tasks SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(status, id); // Update data column too const task = this.getTask(id); if (task) { task.status = status; task.updatedAt = new Date(); if (status === 'done' && !task.completedDate) { task.completedDate = new Date(); } this.updateTaskData(id, task); } } updateTaskData(id, task) { const stmt = this.db.prepare(` UPDATE tasks SET data = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(JSON.stringify(task), id); } createSession(projectIdOrData, data) { let projectId; let sessionData; if (typeof projectIdOrData === 'string') { projectId = projectIdOrData; sessionData = data || {}; } else { projectId = projectIdOrData.projectId; sessionData = { name: projectIdOrData.name, }; } const session = { id: uuidv4(), projectId, name: sessionData.name || 'New Session', type: sessionData.type || 'human', llmModel: sessionData.llmModel, context: sessionData.context, isActive: true, startedAt: new Date(), lastActivityAt: new Date(), }; const stmt = this.db.prepare(` INSERT INTO sessions ( id, project_id, name, type, llm_model, context, is_active, data ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run(session.id, projectId, session.name, session.type, session.llmModel, session.context, session.isActive ? 1 : 0, JSON.stringify(session)); return session; } getActiveSessions(projectId) { const rows = this.db.prepare('SELECT * FROM sessions WHERE project_id = ? AND is_active = 1').all(projectId); return rows.map(row => JSON.parse(row.data)); } // Kanban board data getKanbanBoard(projectId) { const project = this.getProject(projectId); if (!project) { return { backlog: [], todo: [], in_progress: [], review: [], done: [], blocked: [], }; } const allTasks = []; project.workspaces.forEach(workspace => { workspace.goals.forEach(goal => { allTasks.push(...goal.tasks); // Include subtasks goal.tasks.forEach(task => { if (task.subtasks) { allTasks.push(...task.subtasks); } }); }); }); // Group by status const columns = { backlog: allTasks.filter(t => t.status === 'backlog'), todo: allTasks.filter(t => t.status === 'todo'), in_progress: allTasks.filter(t => t.status === 'in_progress'), review: allTasks.filter(t => t.status === 'review'), done: allTasks.filter(t => t.status === 'done'), blocked: allTasks.filter(t => t.status === 'blocked'), }; return columns; } // Get project with all details async getProjectWithDetails(projectId) { return this.getProject(projectId); } // Search tasks across the project async searchTasks(query, filters) { const allProjects = this.getAllProjects(); const tasks = []; for (const project of allProjects) { project.workspaces.forEach(workspace => { workspace.goals.forEach(goal => { goal.tasks.forEach(task => { const matchesQuery = !query || task.title.toLowerCase().includes(query.toLowerCase()) || (task.description && task.description.toLowerCase().includes(query.toLowerCase())); const matchesStatus = !filters?.status || task.status === filters.status; const matchesPriority = !filters?.priority || task.priority === filters.priority; if (matchesQuery && matchesStatus && matchesPriority) { tasks.push(task); } }); }); }); } return tasks; } // Project statistics getProjectStats(projectId) { const project = this.getProject(projectId); if (!project) return null; let totalTasks = 0; let completedTasks = 0; let inProgressTasks = 0; let overdueTasks = 0; project.workspaces.forEach(workspace => { workspace.goals.forEach(goal => { goal.tasks.forEach(task => { totalTasks++; if (task.status === 'done') completedTasks++; if (task.status === 'in_progress') inProgressTasks++; if (task.dueDate && new Date(task.dueDate) < new Date() && task.status !== 'done') { overdueTasks++; } }); }); }); const sessions = this.db.prepare('SELECT COUNT(*) as total FROM sessions WHERE project_id = ?').get(projectId); const activeSessions = this.db.prepare('SELECT COUNT(*) as total FROM sessions WHERE project_id = ? AND is_active = 1').get(projectId); return { totalWorkspaces: project.workspaces.length, totalGoals: project.workspaces.reduce((acc, w) => acc + w.goals.length, 0), totalTasks, completedTasks, inProgressTasks, overdueTasks, totalSessions: sessions.total, activeSessions: activeSessions.total, }; } } //# sourceMappingURL=database.js.map