UNPKG

@ever_cheng/memory-task-mcp

Version:

Memory and task management MCP Server

601 lines 23.2 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.GoalManager = void 0; /** * Goal Management Module * * Provides CRUD operations for Goals with local JSON file storage and caching. */ 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")); /** * Goal Manager Class * * Manages Goal operations with local file storage and caching. */ class GoalManager { /** * Constructor * @param storagePath Storage path for goals * @param cacheService Unified cache service instance */ constructor(storagePath, cacheService) { this.storagePath = storagePath; this.cache = cacheService; } /** * Set manager references for event logging * @param taskManager TaskManager instance * @param memoryManager MemoryManager instance */ setManagerReferences(taskManager, memoryManager) { this.taskManager = taskManager; this.memoryManager = memoryManager; } /** * Initialize goal storage */ async initialize() { try { if (!(0, fs_1.existsSync)(this.storagePath)) { await fs.mkdir(this.storagePath, { recursive: true }); logger_1.default.info(`Created goal storage directory: ${this.storagePath}`); } } catch (error) { const errorMessage = `Failed to initialize goal storage: ${error instanceof Error ? error.message : String(error)}`; logger_1.default.error(errorMessage); throw new Error(errorMessage); } } /** * Get next sequential goal ID */ async getNextGoalId() { try { return `go-${(0, nanoid_1.nanoid)(8)}`; } catch (error) { logger_1.default.error('Failed to generate next goal ID:', error); return `go-${(0, nanoid_1.nanoid)(8)}`; } } /** * Create a new goal * @param args Create goal parameters */ async createGoal(args) { try { // Basic validation if (!args.title) throw new Error('Title is required'); if (!args.description) throw new Error('Description is required'); if (!args.mode) throw new Error('Mode is required'); // Validate mode if (!['planning', 'acting', 'pending', 'completed'].includes(args.mode)) { throw new Error('Mode must be one of: planning, acting, pending, completed'); } // Generate unique goal ID const goalId = await this.getNextGoalId(); // Sanitize input const title = args.title.substring(0, 100).replace(/[<>]/g, ''); const description = args.description.substring(0, 1000).replace(/[<>]/g, ''); // Validate linked tasks let linked_tasks = []; if (args.linked_tasks && Array.isArray(args.linked_tasks)) { linked_tasks = args.linked_tasks.map(id => String(id)); } // 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 progress notes let progress_notes = []; if (args.progress_notes && Array.isArray(args.progress_notes)) { progress_notes = args.progress_notes.map(note => String(note).substring(0, 500).replace(/[<>]/g, '')); } const goal = { id: goalId, title, description, mode: args.mode, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), linked_tasks, linked_memories, progress_notes }; await this.save(goal.id, goal); return goal; } catch (error) { logger_1.default.error('Failed to create goal:', error); throw error; } } /** * Get a goal by ID * @param args Get goal parameters */ async getGoal(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 goal:', error); throw error; } } /** * Update an existing goal * @param args Update goal parameters */ async updateGoal(args) { try { if (!args.id) throw new Error('ID is required'); const goal = await this.load(args.id); if (!goal) return null; // Track events for changes const events = []; // Update fields if provided if (args.title) { const newTitle = args.title.substring(0, 100).replace(/[<>]/g, ''); if (goal.title !== newTitle) { const event = { action: 'updated', field: 'title', summary: 'Goal標題已更新', details: { old_value: goal.title, new_value: newTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); goal.title = newTitle; } } if (args.description) { const newDescription = args.description.substring(0, 1000).replace(/[<>]/g, ''); if (goal.description !== newDescription) { const event = { action: 'updated', field: 'description', summary: 'Goal描述已更新' }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); goal.description = newDescription; } } if (args.mode) { if (!['planning', 'acting', 'pending', 'completed'].includes(args.mode)) { throw new Error('Mode must be one of: planning, acting, pending, completed'); } if (goal.mode !== args.mode) { const event = { action: 'status_changed', field: 'mode', summary: `Goal狀態從 ${goal.mode} 變更為 ${args.mode}`, details: { old_value: goal.mode, new_value: args.mode } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); goal.mode = args.mode; } } if (args.linked_tasks !== undefined) { if (Array.isArray(args.linked_tasks)) { const newLinkedTasks = args.linked_tasks.map(id => String(id)); const oldTasks = new Set(goal.linked_tasks); const newTasks = new Set(newLinkedTasks); // Check for newly linked tasks for (const taskId of newTasks) { if (!oldTasks.has(taskId)) { let taskTitle = taskId; if (this.taskManager) { try { const task = await this.taskManager.getTaskStatus({ id: taskId }); if (task) taskTitle = task.title; } catch (error) { logger_1.default.warn(`Failed to get task title for ${taskId}:`, error); } } const event = { action: 'linked', field: 'linked_tasks', summary: `Task已關聯到Goal`, details: { title: taskTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); } } // Check for de-linked tasks for (const taskId of oldTasks) { if (!newTasks.has(taskId)) { let taskTitle = taskId; if (this.taskManager) { try { const task = await this.taskManager.getTaskStatus({ id: taskId }); if (task) taskTitle = task.title; } catch (error) { logger_1.default.warn(`Failed to get task title for ${taskId}:`, error); } } const event = { action: 'de-linked', field: 'linked_tasks', summary: `Task已從Goal移除關聯`, details: { title: taskTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); } } goal.linked_tasks = newLinkedTasks; } } if (args.linked_memories !== undefined) { if (Array.isArray(args.linked_memories)) { const newLinkedMemories = args.linked_memories.map(id => String(id)); const oldMemories = new Set(goal.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已關聯到Goal`, 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已從Goal移除關聯`, details: { title: memoryTitle } }; events.push(`[${new Date().toISOString()}] EVENT: ${JSON.stringify(event)}`); } } goal.linked_memories = newLinkedMemories; } } if (args.progress_notes !== undefined) { if (Array.isArray(args.progress_notes)) { goal.progress_notes = args.progress_notes.map(note => String(note).substring(0, 500).replace(/[<>]/g, '')); } } // Add all events to progress notes goal.progress_notes.push(...events); goal.updated_at = new Date().toISOString(); await this.save(goal.id, goal); return goal; } catch (error) { logger_1.default.error('Failed to update goal:', error); throw error; } } /** * Update goal's linked tasks by adding a new task ID * @param goalId Goal ID to update * @param taskId Task ID to add to linked_tasks */ async updateGoalLinkedTasks(goalId, taskId) { try { const goal = await this.load(goalId); if (!goal) { throw new Error(`Goal ${goalId} does not exist`); } // Add task ID to linked_tasks if not already present if (!goal.linked_tasks.includes(taskId)) { // Get task title for event logging let taskTitle = taskId; if (this.taskManager) { try { const task = await this.taskManager.getTaskStatus({ id: taskId }); if (task) { taskTitle = task.title; } } catch (error) { // If we can't get the task title, use the ID } } // Use updateGoal to trigger event flow const updatedLinkedTasks = [...goal.linked_tasks, taskId]; await this.updateGoal({ id: goalId, linked_tasks: updatedLinkedTasks }); } } catch (error) { logger_1.default.error('Failed to update goal linked tasks:', error); throw error; } } /** * List goals with optional filtering * @param args List goals parameters */ async listGoals(args) { try { let goals = await this.list(); // Apply mode filter if provided if (args?.mode) { if (!['planning', 'acting', 'pending', 'completed'].includes(args.mode)) { throw new Error('Mode must be one of: planning, acting, pending, completed'); } goals = goals.filter(goal => goal.mode === args.mode); } // Sort by update time (newest first) goals.sort((a, b) => { const dateA = new Date(a.updated_at).getTime(); const dateB = new Date(b.updated_at).getTime(); return dateB - dateA; }); return goals; } catch (error) { logger_1.default.error('Failed to list goals:', error); throw error; } } /** * Delete a goal * @param args Delete goal parameters */ async deleteGoal(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 goal:', error); throw error; } } /** * Get goals by linked task * @param taskId Task ID */ async getGoalsByLinkedTask(taskId) { try { if (!taskId) throw new Error('Task ID is required'); const goals = await this.list(); return goals.filter(goal => goal.linked_tasks.includes(taskId)); } catch (error) { logger_1.default.error('Failed to get goals by linked task:', error); throw error; } } /** * Update goal's linked memories (convenience method) * @param goalId Goal ID * @param memoryId Memory ID to link */ async updateGoalLinkedMemories(goalId, memoryId) { try { const goal = await this.load(goalId); if (!goal) { throw new Error(`Goal ${goalId} does not exist`); } // Add memory ID to linked_memories if not already present if (!goal.linked_memories.includes(memoryId)) { goal.linked_memories.push(memoryId); goal.updated_at = new Date().toISOString(); await this.save(goal.id, goal); } } catch (error) { logger_1.default.error('Failed to update goal linked memories:', error); throw error; } } /** * Add progress note to goal * @param goalId Goal ID * @param note Progress note to add */ async addGoalProgressNote(goalId, note) { try { const goal = await this.load(goalId); if (!goal) { throw new Error(`Goal ${goalId} does not exist`); } // Add progress note with timestamp const sanitizedNote = String(note).substring(0, 500).replace(/[<>]/g, ''); const timestampedNote = `[${new Date().toISOString()}] ${sanitizedNote}`; goal.progress_notes.push(timestampedNote); goal.updated_at = new Date().toISOString(); await this.save(goal.id, goal); } catch (error) { logger_1.default.error('Failed to add goal progress note:', error); throw error; } } /** * Get goals by linked memory * @param memoryId Memory ID */ async getGoalsByLinkedMemory(memoryId) { try { if (!memoryId) throw new Error('Memory ID is required'); const goals = await this.list(); return goals.filter(goal => goal.linked_memories.includes(memoryId)); } catch (error) { logger_1.default.error('Failed to get goals by linked memory:', error); throw error; } } /** * Get cache statistics */ getCacheStats() { return this.cache.getStats(); } // Private methods - Storage layer functionality /** * Save goal to storage * @param id Goal ID * @param goal Goal object */ async save(id, goal) { const filePath = path.join(this.storagePath, `${id}.json`); try { await fs.writeFile(filePath, JSON.stringify(goal, null, 2)); this.cache.set(id, goal); } catch (error) { throw new Error(`Failed to save goal: ${error instanceof Error ? error.message : String(error)}`); } } /** * Load goal from storage * @param id Goal ID */ async load(id) { // Check cache first const cachedGoal = this.cache.get(id); if (cachedGoal) { return cachedGoal; } try { const filePath = path.join(this.storagePath, `${id}.json`); const content = await fs.readFile(filePath, 'utf-8'); const goal = JSON.parse(content); // Update cache this.cache.set(id, goal); return goal; } catch (error) { if (error.code === 'ENOENT') { return null; } logger_1.default.error(`Failed to load goal ${id}:`, error); return null; } } /** * Delete goal from storage * @param id Goal 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 goal ${id}:`, error); return false; } } /** * List all goals from storage */ async list() { try { const files = await fs.readdir(this.storagePath); const goals = []; for (const file of files) { if (file.endsWith('.json')) { const id = file.replace('.json', ''); const goal = await this.load(id); if (goal) goals.push(goal); } } return goals; } catch (error) { logger_1.default.error('Failed to list goals:', error); return []; } } } exports.GoalManager = GoalManager; //# sourceMappingURL=goal.js.map