claudemaster
Version:
Task management MCP server optimized for Claude Code - no API keys required
228 lines (202 loc) • 6.25 kB
JavaScript
/**
* 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;