@ever_cheng/memory-task-mcp
Version:
Memory and task management MCP Server
601 lines • 23.2 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.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
;