UNPKG

@ever_cheng/memory-task-mcp

Version:

Memory and task management MCP Server

803 lines 29.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TaskManager = void 0; /** * Task Management Module * * Refactored to use unified CacheService instead of duplicate SimpleCache implementation. * Maintains the existing API while eliminating code duplication. */ const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const fs_1 = require("fs"); const nanoid_1 = require("nanoid"); const logger_1 = __importDefault(require("./logger")); /** * Task Manager Class * * Combines the functionality of TaskService and TaskStorage into a single class, * simplifies the architecture, reduces layers and dependencies. */ class TaskManager { /** * Constructor * @param storagePath Storage path * @param cacheService Unified cache service instance */ constructor(storagePath, cacheService) { this.storagePath = storagePath; this.cache = cacheService; } /** * Set manager references for event logging * @param memoryManager MemoryManager instance */ setManagerReferences(memoryManager) { this.memoryManager = memoryManager; } /** * Initialize */ async initialize() { try { if (!(0, fs_1.existsSync)(this.storagePath)) { await fs.mkdir(this.storagePath, { recursive: true }); logger_1.default.info(`Created storage directory: ${this.storagePath}`); } } catch (error) { const errorMessage = `Failed to initialize storage: ${error instanceof Error ? error.message : String(error)}`; logger_1.default.error(errorMessage); throw new Error(errorMessage); } } /** * Get next sequential task ID */ async getNextTaskId() { try { return `ts-${(0, nanoid_1.nanoid)(8)}`; } catch (error) { logger_1.default.error('Failed to generate next task ID:', error); return `ts-${(0, nanoid_1.nanoid)(8)}`; } } /** * Create task * @param args Create task parameters */ async createTask(args) { try { // Basic validation if (!args.title) throw new Error('Title is required'); if (!args.description) throw new Error('Description is required'); // Simplified parameter handling const title = args.title.substring(0, 100).replace(/[<>]/g, ''); const description = args.description.substring(0, 1000).replace(/[<>]/g, ''); // Validate priority let priority = 'medium'; if (args.priority) { if (!['low', 'medium', 'high'].includes(args.priority)) { throw new Error('Priority must be one of: low, medium, or high'); } priority = args.priority; } // Validate due date let due_date = undefined; if (args.due_date) { try { const date = new Date(args.due_date); if (isNaN(date.getTime())) { throw new Error('Invalid date'); } due_date = date.toISOString(); } catch (error) { throw new Error('Due date must be a valid date format'); } } // Validate linked memories let linked_memories = []; if (args.linked_memories && Array.isArray(args.linked_memories)) { linked_memories = args.linked_memories.map(id => String(id)); } // Validate dependencies let depends_on = []; if (args.depends_on && Array.isArray(args.depends_on)) { depends_on = args.depends_on.map(id => String(id)); await this.validateDependencies(depends_on); } const taskId = await this.getNextTaskId(); const task = { id: taskId, title, description, status: 'planning', priority, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), due_date, linked_memories, progress_notes: [], depends_on }; await this.save(task.id, task); return task; } catch (error) { logger_1.default.error('Failed to create task:', error); throw error; } } /** * Update task * @param args Update task parameters */ async updateTask(args) { try { if (!args) throw new Error('Arguments are required'); if (!args.id) throw new Error('ID is required'); const task = await this.load(args.id); if (!task) return null; // Track events for changes const events = []; // Update task properties if (args.status) { if (!['planning', 'ready', 'executing', 'completed', 'pending', 'aborted'].includes(args.status)) { throw new Error('Status must be one of: planning, ready, executing, completed, pending, or aborted'); } if (task.status !== args.status) { const event = { action: 'status_changed', field: 'status', summary: `Task狀態從 ${task.status} 變更為 ${args.status}`, details: { old_value: task.status, new_value: args.status } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); task.status = args.status; } } if (args.title) { const newTitle = args.title.substring(0, 100).replace(/[<>]/g, ''); if (task.title !== newTitle) { const event = { action: 'updated', field: 'title', summary: 'Task標題已更新', details: { old_value: task.title, new_value: newTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); task.title = newTitle; } } if (args.description) { const newDescription = args.description.substring(0, 1000).replace(/[<>]/g, ''); if (task.description !== newDescription) { const event = { action: 'updated', field: 'description', summary: 'Task描述已更新' }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); task.description = newDescription; } } if (args.priority) { if (!['low', 'medium', 'high'].includes(args.priority)) { throw new Error('Priority must be one of: low, medium, or high'); } if (task.priority !== args.priority) { const event = { action: 'updated', field: 'priority', summary: `Task優先級從 ${task.priority} 變更為 ${args.priority}`, details: { old_value: task.priority, new_value: args.priority } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); task.priority = args.priority; } } if (args.progress_note) { const note = args.progress_note.substring(0, 1000).replace(/[<>]/g, ''); task.progress_notes.push(`${new Date().toISOString()}: ${note}`); } if (args.depends_on !== undefined) { if (Array.isArray(args.depends_on)) { const depends_on = args.depends_on.map(id => String(id)); await this.validateDependencies(depends_on); const oldDeps = new Set(task.depends_on); const newDeps = new Set(depends_on); // Check for newly added dependencies for (const depId of newDeps) { if (!oldDeps.has(depId)) { let depTitle = depId; try { const depTask = await this.load(depId); if (depTask) depTitle = depTask.title; } catch (error) { logger_1.default.warn(`Failed to get dependency task title for ${depId}:`, error); } const event = { action: 'linked', field: 'depends_on', summary: `Task已添加依賴`, details: { title: depTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); } } // Check for removed dependencies for (const depId of oldDeps) { if (!newDeps.has(depId)) { let depTitle = depId; try { const depTask = await this.load(depId); if (depTask) depTitle = depTask.title; } catch (error) { logger_1.default.warn(`Failed to get dependency task title for ${depId}:`, error); } const event = { action: 'de-linked', field: 'depends_on', summary: `Task已移除依賴`, details: { title: depTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); } } task.depends_on = depends_on; } } if (args.linked_memories !== undefined) { if (Array.isArray(args.linked_memories)) { const newLinkedMemories = args.linked_memories.map(id => String(id)); const oldMemories = new Set(task.linked_memories); const newMemories = new Set(newLinkedMemories); // Check for newly linked memories for (const memoryId of newMemories) { if (!oldMemories.has(memoryId)) { let memoryTitle = memoryId; if (this.memoryManager) { try { const memory = await this.memoryManager.getMemory(memoryId); if (memory) memoryTitle = memory.summary; } catch (error) { logger_1.default.warn(`Failed to get memory title for ${memoryId}:`, error); } } const event = { action: 'linked', field: 'linked_memories', summary: `Memory已關聯到Task`, details: { title: memoryTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); } } // Check for de-linked memories for (const memoryId of oldMemories) { if (!newMemories.has(memoryId)) { let memoryTitle = memoryId; if (this.memoryManager) { try { const memory = await this.memoryManager.getMemory(memoryId); if (memory) memoryTitle = memory.summary; } catch (error) { logger_1.default.warn(`Failed to get memory title for ${memoryId}:`, error); } } const event = { action: 'de-linked', field: 'linked_memories', summary: `Memory已從Task移除關聯`, details: { title: memoryTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); } } task.linked_memories = newLinkedMemories; } } // Add all events to progress notes task.progress_notes.push(...events); task.updated_at = new Date().toISOString(); await this.save(task.id, task); return task; } catch (error) { logger_1.default.error('Failed to update task:', error); throw error; } } /** * Get task status * @param args Get task status parameters */ async getTaskStatus(args) { try { if (!args.id) throw new Error('ID is required'); return this.load(args.id); } catch (error) { logger_1.default.error('Failed to get task status:', error); throw error; } } /** * List tasks * @param args List tasks parameters */ async listTasks(args) { try { let tasks = await this.list(); // Apply filters if (args?.status) { if (!['planning', 'ready', 'executing', 'completed', 'pending', 'aborted'].includes(args.status)) { throw new Error('Status must be one of: planning, ready, executing, completed, pending, or aborted'); } tasks = tasks.filter(task => task.status === args.status); } if (args?.priority) { if (!['low', 'medium', 'high'].includes(args.priority)) { throw new Error('Priority must be one of: low, medium, or high'); } tasks = tasks.filter(task => task.priority === args.priority); } // Sort by update time (newest first) tasks.sort((a, b) => { const dateA = new Date(a.updated_at).getTime(); const dateB = new Date(b.updated_at).getTime(); return dateB - dateA; }); return tasks; } catch (error) { logger_1.default.error('Failed to list tasks:', error); throw error; } } /** * Delete task * @param args Delete task parameters */ async deleteTask(args) { try { if (!args.id) throw new Error('ID is required'); return this.delete(args.id); } catch (error) { logger_1.default.error('Failed to delete task:', error); throw error; } } /** * Search tasks * @param args Search parameters */ async searchTask(args) { try { if (!args.query) throw new Error('Query is required'); const query = args.query.toLowerCase(); const limit = args.limit || 10; const tasks = await this.list(); const results = tasks .map(task => { const titleMatch = task.title.toLowerCase().includes(query); const descMatch = task.description.toLowerCase().includes(query); const notesMatch = task.progress_notes.some(note => note.toLowerCase().includes(query)); if (titleMatch || descMatch || notesMatch) { return { task, similarity: 0.9 }; } return { task, similarity: this.calculateSimpleSimilarity(query, `${task.title} ${task.description} ${task.progress_notes.join(' ')}`) }; }) .filter(result => result.similarity > 0.01) .sort((a, b) => b.similarity - a.similarity) .slice(0, limit); return results; } catch (error) { logger_1.default.error('Failed to search tasks:', error); throw error; } } /** * Calculate simple similarity score */ calculateSimpleSimilarity(query, text) { const queryWords = new Set(query.split(/\s+/).filter(word => word.length > 0)); const textWords = new Set(text.toLowerCase().split(/\s+/).filter(word => word.length > 0)); let overlap = 0; queryWords.forEach(word => { if (textWords.has(word)) overlap++; }); return queryWords.size > 0 ? overlap / queryWords.size : 0; } /** * Add progress note * @param id Task ID * @param note Progress note */ async addProgressNote(id, note) { try { if (!id) throw new Error('ID is required'); if (!note) throw new Error('Note is required'); const task = await this.load(id); if (!task) return false; const sanitizedNote = note.substring(0, 1000).replace(/[<>]/g, ''); task.progress_notes.push(`${new Date().toISOString()}: ${sanitizedNote}`); task.updated_at = new Date().toISOString(); await this.save(id, task); return true; } catch (error) { logger_1.default.error('Failed to add progress note:', error); throw error; } } /** * Update task status * @param id Task ID * @param status New status */ async updateStatus(id, status) { try { if (!id) throw new Error('ID is required'); if (!['planning', 'ready', 'executing', 'completed', 'pending', 'aborted'].includes(status)) { throw new Error('Status must be one of: planning, ready, executing, completed, pending, or aborted'); } const task = await this.load(id); if (!task) return false; task.status = status; task.updated_at = new Date().toISOString(); await this.save(id, task); return true; } catch (error) { logger_1.default.error('Failed to update task status:', error); throw error; } } /** * Get overdue tasks */ async getOverdueTasks() { try { const tasks = await this.list(); const now = new Date().getTime(); return tasks.filter(task => { if (!task.due_date) return false; if (task.status === 'completed' || task.status === 'aborted') return false; const dueDate = new Date(task.due_date).getTime(); return dueDate < now; }); } catch (error) { logger_1.default.error('Failed to get overdue tasks:', error); throw error; } } /** * Get tasks by linked memory * @param memoryId Memory ID */ async getTasksByLinkedMemory(memoryId) { try { if (!memoryId) throw new Error('Memory ID is required'); const tasks = await this.list(); return tasks.filter(task => task.linked_memories.includes(memoryId)); } catch (error) { logger_1.default.error('Failed to get tasks by linked memory:', error); throw error; } } /** * Batch get tasks * @param ids Array of task IDs */ async batchGetTasks(ids) { try { if (!Array.isArray(ids)) throw new Error('IDs must be an array'); const tasks = []; for (const id of ids) { const task = await this.load(id); if (task) tasks.push(task); } return tasks; } catch (error) { logger_1.default.error('Failed to batch get tasks:', error); throw error; } } /** * Update task's linked memories by adding a new memory ID * @param taskId Task ID to update * @param memoryId Memory ID to add to linked_memories */ async updateTaskLinkedMemories(taskId, memoryId) { try { const task = await this.load(taskId); if (!task) { throw new Error(`Task ${taskId} does not exist`); } // Add memory ID to linked_memories if not already present if (!task.linked_memories.includes(memoryId)) { task.linked_memories.push(memoryId); task.updated_at = new Date().toISOString(); await this.save(task.id, task); } } catch (error) { logger_1.default.error('Failed to update task linked memories:', error); throw error; } } /** * Validate task dependencies * @param dependsOn Array of task IDs */ async validateDependencies(dependsOn) { if (!dependsOn || dependsOn.length === 0) { return; } for (const taskId of dependsOn) { const dependentTask = await this.load(taskId); if (!dependentTask) { throw new Error(`Dependent task ${taskId} does not exist`); } } } /** * Get tasks that are ready to execute (dependencies satisfied) */ async getExecutableTasks() { try { const allTasks = await this.list(); const executableTasks = []; for (const task of allTasks) { if (task.status === 'completed' || task.status === 'aborted') { continue; } const canExecute = await this.canTaskExecute(task); if (canExecute) { executableTasks.push(task); } } return executableTasks; } catch (error) { logger_1.default.error('Failed to get executable tasks:', error); return []; } } /** * Check if a task can be executed (all dependencies completed) * @param task Task to check */ async canTaskExecute(task) { if (!task.depends_on || task.depends_on.length === 0) { return true; } for (const dependentId of task.depends_on) { const dependentTask = await this.load(dependentId); if (!dependentTask || dependentTask.status !== 'completed') { return false; } } return true; } /** * Get tasks ordered by numeric ID */ async getTasksInOrder() { try { const tasks = await this.list(); return tasks.sort((a, b) => { const idA = parseInt(a.id); const idB = parseInt(b.id); return idA - idB; }); } catch (error) { logger_1.default.error('Failed to get tasks in order:', error); return []; } } /** * Get cache statistics */ getCacheStats() { return this.cache.getStats(); } // Private methods - Storage layer functionality /** * Save task * @param id Task ID * @param task Task object */ async save(id, task) { const filePath = path.join(this.storagePath, `${id}.json`); try { await fs.writeFile(filePath, JSON.stringify(task, null, 2)); this.cache.set(id, task); } catch (error) { throw new Error(`Failed to save task: ${error instanceof Error ? error.message : String(error)}`); } } /** * Load task * @param id Task ID */ async load(id) { // Check cache const cachedTask = this.cache.get(id); if (cachedTask) { return cachedTask; } try { const filePath = path.join(this.storagePath, `${id}.json`); const content = await fs.readFile(filePath, 'utf-8'); const task = JSON.parse(content); // Update cache this.cache.set(id, task); return task; } catch (error) { if (error.code === 'ENOENT') { return null; } logger_1.default.error(`Failed to load task ${id}:`, error); return null; } } /** * Delete task * @param id Task ID */ async delete(id) { const filePath = path.join(this.storagePath, `${id}.json`); try { await fs.unlink(filePath); this.cache.delete(id); return true; } catch (error) { if (error.code === 'ENOENT') { return false; } logger_1.default.error(`Failed to delete task ${id}:`, error); return false; } } /** * List all tasks */ async list() { try { const files = await fs.readdir(this.storagePath); const tasks = []; for (const file of files) { if (file.endsWith('.json')) { const id = file.replace('.json', ''); const task = await this.load(id); if (task) tasks.push(task); } } return tasks; } catch (error) { logger_1.default.error('Failed to list tasks:', error); return []; } } /** * Sort tasks by priority * @param tasks Tasks to sort * @param highFirst Whether high priority comes first */ sortByPriority(tasks, highFirst = true) { const priorityValue = { high: 3, medium: 2, low: 1 }; return [...tasks].sort((a, b) => { const valueA = priorityValue[a.priority]; const valueB = priorityValue[b.priority]; return highFirst ? valueB - valueA : valueA - valueB; }); } /** * Sort tasks by due date * @param tasks Tasks to sort * @param ascending Whether to sort in ascending order */ sortByDueDate(tasks, ascending = true) { return [...tasks].sort((a, b) => { if (!a.due_date && !b.due_date) return 0; if (!a.due_date) return 1; if (!b.due_date) return -1; const dateA = new Date(a.due_date).getTime(); const dateB = new Date(b.due_date).getTime(); return ascending ? dateA - dateB : dateB - dateA; }); } } exports.TaskManager = TaskManager; //# sourceMappingURL=task.js.map