UNPKG

claudemaster

Version:

Task management MCP server optimized for Claude Code - no API keys required

228 lines (202 loc) 6.25 kB
/** * task-engine.js * Core task management engine for Claudemaster * Pure task operations without AI dependencies */ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { v4 as uuidv4 } from 'uuid'; export class TaskEngine { constructor(projectRoot) { this.projectRoot = projectRoot; this.tasksPath = join(projectRoot, '.taskmaster', 'tasks.json'); this.tasksDir = dirname(this.tasksPath); this.ensureTasksDirectory(); } ensureTasksDirectory() { if (!existsSync(this.tasksDir)) { mkdirSync(this.tasksDir, { recursive: true }); } } loadTasks() { if (!existsSync(this.tasksPath)) { return []; } try { const data = readFileSync(this.tasksPath, 'utf8'); return JSON.parse(data); } catch (error) { console.warn(`Warning: Could not load tasks from ${this.tasksPath}:`, error.message); return []; } } saveTasks(tasks) { this.ensureTasksDirectory(); writeFileSync(this.tasksPath, JSON.stringify(tasks, null, 2)); } addTask(task) { const tasks = this.loadTasks(); const newTask = { id: task.id || this.generateTaskId(tasks), title: task.title || 'New Task', description: task.description || '', status: task.status || 'pending', priority: task.priority || 'medium', dependencies: task.dependencies || [], subtasks: task.subtasks || [], details: task.details || '', testStrategy: task.testStrategy || '', createdAt: new Date().toISOString(), ...task }; tasks.push(newTask); this.saveTasks(tasks); return newTask; } updateTask(id, updates) { const tasks = this.loadTasks(); const taskIndex = tasks.findIndex(t => t.id === id); if (taskIndex === -1) { throw new Error(`Task with id ${id} not found`); } tasks[taskIndex] = { ...tasks[taskIndex], ...updates, updatedAt: new Date().toISOString() }; this.saveTasks(tasks); return tasks[taskIndex]; } removeTask(id) { const tasks = this.loadTasks(); const initialLength = tasks.length; const filteredTasks = tasks.filter(t => t.id !== id); if (filteredTasks.length === initialLength) { throw new Error(`Task with id ${id} not found`); } this.saveTasks(filteredTasks); return true; } getTask(id) { const tasks = this.loadTasks(); const task = tasks.find(t => t.id === id); if (!task) { throw new Error(`Task with id ${id} not found`); } return task; } listTasks(filters = {}) { const tasks = this.loadTasks(); let filtered = tasks; if (filters.status) { filtered = filtered.filter(t => t.status === filters.status); } if (filters.priority) { filtered = filtered.filter(t => t.priority === filters.priority); } return filtered.sort((a, b) => { // Sort by priority first (high, medium, low), then by id const priorityOrder = { high: 3, medium: 2, low: 1 }; const aPriority = priorityOrder[a.priority] || 2; const bPriority = priorityOrder[b.priority] || 2; if (aPriority !== bPriority) { return bPriority - aPriority; } return a.id - b.id; }); } getNextTask() { const tasks = this.loadTasks(); const availableTasks = tasks.filter(task => { if (task.status === 'done' || task.status === 'cancelled') { return false; } // Check if all dependencies are completed if (task.dependencies && task.dependencies.length > 0) { return task.dependencies.every(depId => { const depTask = tasks.find(t => t.id === depId); return depTask && depTask.status === 'done'; }); } return true; }); if (availableTasks.length === 0) { return null; } // Sort by priority and return the first one return availableTasks.sort((a, b) => { const priorityOrder = { high: 3, medium: 2, low: 1 }; const aPriority = priorityOrder[a.priority] || 2; const bPriority = priorityOrder[b.priority] || 2; if (aPriority !== bPriority) { return bPriority - aPriority; } return a.id - b.id; })[0]; } generateTaskId(existingTasks) { const maxId = existingTasks.reduce((max, task) => Math.max(max, task.id || 0), 0); return maxId + 1; } setTaskStatus(id, status) { const validStatuses = ['pending', 'in-progress', 'done', 'cancelled', 'deferred']; if (!validStatuses.includes(status)) { throw new Error(`Invalid status: ${status}. Valid statuses: ${validStatuses.join(', ')}`); } return this.updateTask(id, { status }); } addSubtask(taskId, subtask) { const task = this.getTask(taskId); const newSubtask = { id: subtask.id || uuidv4(), title: subtask.title || 'New Subtask', description: subtask.description || '', status: subtask.status || 'pending', ...subtask }; task.subtasks = task.subtasks || []; task.subtasks.push(newSubtask); return this.updateTask(taskId, { subtasks: task.subtasks }); } removeSubtask(taskId, subtaskId) { const task = this.getTask(taskId); if (!task.subtasks) { throw new Error(`Task ${taskId} has no subtasks`); } const initialLength = task.subtasks.length; task.subtasks = task.subtasks.filter(st => st.id !== subtaskId); if (task.subtasks.length === initialLength) { throw new Error(`Subtask ${subtaskId} not found in task ${taskId}`); } return this.updateTask(taskId, { subtasks: task.subtasks }); } validateDependencies() { const tasks = this.loadTasks(); const issues = []; tasks.forEach(task => { if (task.dependencies) { task.dependencies.forEach(depId => { const depTask = tasks.find(t => t.id === depId); if (!depTask) { issues.push({ type: 'missing_dependency', taskId: task.id, missingDependency: depId }); } }); } }); return issues; } getProjectStats() { const tasks = this.loadTasks(); const stats = { total: tasks.length, pending: tasks.filter(t => t.status === 'pending').length, inProgress: tasks.filter(t => t.status === 'in-progress').length, done: tasks.filter(t => t.status === 'done').length, cancelled: tasks.filter(t => t.status === 'cancelled').length, deferred: tasks.filter(t => t.status === 'deferred').length }; stats.completion = tasks.length > 0 ? Math.round((stats.done / stats.total) * 100) : 0; return stats; } } export default TaskEngine;