orchestry-mcp
Version:
Orchestry MCP Server for multi-session task management
504 lines • 18.6 kB
JavaScript
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