@ever_cheng/memory-task-mcp
Version:
Memory and task management MCP Server
803 lines • 29.9 kB
JavaScript
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
;