memtask
Version:
Memory and task management MCP Server with Goal-Task-Memory architecture
629 lines • 22 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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GoalManager = void 0;
/**
* Goal Management Module
*
* Provides comprehensive goal management functionality including CRUD operations,
* progress tracking, and task linkage management.
*/
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const fs_1 = require("fs");
/**
* Goal Manager Class
*
* Manages goal lifecycle with task linkage, progress tracking, and success criteria.
* Follows the same patterns as TaskManager for consistency.
*/
class GoalManager {
/**
* Constructor
* @param storagePath Storage path
* @param cacheService Unified cache service instance
* @param console Console for logging (optional)
*/
constructor(storagePath, cacheService, console = global.console) {
this.storagePath = storagePath;
this.cache = cacheService;
this.console = console;
}
/**
* Initialize
*/
async initialize() {
try {
if (!(0, fs_1.existsSync)(this.storagePath)) {
await fs.mkdir(this.storagePath, { recursive: true });
this.console.info(`Created storage directory: ${this.storagePath}`);
}
}
catch (error) {
const errorMessage = `Failed to initialize goal storage: ${error instanceof Error ? error.message : String(error)}`;
this.console.error(errorMessage);
throw new Error(errorMessage);
}
}
/**
* Get next sequential goal ID
*/
async getNextGoalId() {
try {
const goals = await this.list();
if (goals.length === 0) {
return 'goal-1';
}
// Find the highest numeric ID
const maxId = Math.max(...goals
.map(goal => {
const match = goal.id.match(/goal-(\d+)/);
return match ? parseInt(match[1]) : 0;
})
.filter(id => !isNaN(id)));
return `goal-${maxId + 1}`;
}
catch (error) {
this.console.error('Failed to generate next goal ID:', error);
return 'goal-1';
}
}
/**
* Create 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');
// Sanitize parameters
const title = args.title.substring(0, 100).replace(/[<>]/g, '');
const description = args.description.substring(0, 2000).replace(/[<>]/g, '');
const tags = Array.isArray(args.tags) ? args.tags.map(tag => String(tag).substring(0, 50).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 target date
let target_date = undefined;
if (args.target_date) {
try {
const date = new Date(args.target_date);
if (isNaN(date.getTime())) {
throw new Error('Invalid date');
}
target_date = date.toISOString();
}
catch (error) {
throw new Error('Target date must be a valid date format');
}
}
// Validate success criteria
let success_criteria = [];
if (args.success_criteria && Array.isArray(args.success_criteria)) {
success_criteria = args.success_criteria.map(criteria => String(criteria).substring(0, 200).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));
await this.validateLinkedTasks(linked_tasks);
}
const goalId = await this.getNextGoalId();
const goal = {
id: goalId,
title,
description,
status: 'planning',
priority,
tags,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
target_date,
progress_notes: [],
linked_tasks,
success_criteria
};
await this.save(goal.id, goal);
return goal;
}
catch (error) {
this.console.error('Failed to create goal:', error);
throw error;
}
}
/**
* Update goal
* @param args Update goal parameters
*/
async updateGoal(args) {
try {
if (!args)
throw new Error('Arguments are required');
if (!args.id)
throw new Error('ID is required');
const goal = await this.load(args.id);
if (!goal)
return null;
// Update goal properties
if (args.status) {
if (!['planning', 'active', 'completed', 'on_hold', 'cancelled'].includes(args.status)) {
throw new Error('Status must be one of: planning, active, completed, on_hold, or cancelled');
}
goal.status = args.status;
}
if (args.title) {
goal.title = args.title.substring(0, 100).replace(/[<>]/g, '');
}
if (args.description) {
goal.description = args.description.substring(0, 2000).replace(/[<>]/g, '');
}
if (args.priority) {
if (!['low', 'medium', 'high'].includes(args.priority)) {
throw new Error('Priority must be one of: low, medium, or high');
}
goal.priority = args.priority;
}
if (args.target_date) {
try {
const date = new Date(args.target_date);
if (isNaN(date.getTime())) {
throw new Error('Invalid date');
}
goal.target_date = date.toISOString();
}
catch (error) {
throw new Error('Target date must be a valid date format');
}
}
if (args.progress_note) {
const note = args.progress_note.substring(0, 1000).replace(/[<>]/g, '');
goal.progress_notes.push(`${new Date().toISOString()}: ${note}`);
}
if (args.success_criteria !== undefined) {
if (Array.isArray(args.success_criteria)) {
goal.success_criteria = args.success_criteria.map(criteria => String(criteria).substring(0, 200).replace(/[<>]/g, ''));
}
}
if (args.linked_tasks !== undefined) {
if (Array.isArray(args.linked_tasks)) {
const linked_tasks = args.linked_tasks.map(id => String(id));
await this.validateLinkedTasks(linked_tasks);
goal.linked_tasks = linked_tasks;
}
}
goal.updated_at = new Date().toISOString();
await this.save(goal.id, goal);
return goal;
}
catch (error) {
this.console.error('Failed to update goal:', error);
throw error;
}
}
/**
* Get goal
* @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) {
this.console.error('Failed to get goal:', error);
throw error;
}
}
/**
* List goals
* @param args List goals parameters
*/
async listGoals(args) {
try {
let goals = await this.list();
// Apply filters
if (args?.status) {
if (!['planning', 'active', 'completed', 'on_hold', 'cancelled'].includes(args.status)) {
throw new Error('Status must be one of: planning, active, completed, on_hold, or cancelled');
}
goals = goals.filter(goal => goal.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');
}
goals = goals.filter(goal => goal.priority === args.priority);
}
if (args?.tags && args.tags.length > 0) {
goals = goals.filter(goal => args.tags.some(tag => goal.tags.includes(tag)));
}
// 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) {
this.console.error('Failed to list goals:', error);
throw error;
}
}
/**
* Delete 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) {
this.console.error('Failed to delete goal:', error);
throw error;
}
}
/**
* Search goals
* @param args Search parameters
*/
async searchGoal(args) {
try {
if (!args.query)
throw new Error('Query is required');
const query = args.query.toLowerCase();
const limit = args.limit || 10;
const goals = await this.list();
const results = goals
.map(goal => {
const titleMatch = goal.title.toLowerCase().includes(query);
const descMatch = goal.description.toLowerCase().includes(query);
const tagMatch = goal.tags.some(tag => tag.toLowerCase().includes(query));
const notesMatch = goal.progress_notes.some(note => note.toLowerCase().includes(query));
const criteriaMatch = goal.success_criteria.some(criteria => criteria.toLowerCase().includes(query));
if (titleMatch || descMatch || tagMatch || notesMatch || criteriaMatch) {
return {
goal,
similarity: 0.9
};
}
return {
goal,
similarity: this.calculateSimpleSimilarity(query, `${goal.title} ${goal.description} ${goal.tags.join(' ')} ${goal.progress_notes.join(' ')} ${goal.success_criteria.join(' ')}`)
};
})
.filter(result => result.similarity > 0.01)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit);
return results;
}
catch (error) {
this.console.error('Failed to search goals:', 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 Goal 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 goal = await this.load(id);
if (!goal)
return false;
const sanitizedNote = note.substring(0, 1000).replace(/[<>]/g, '');
goal.progress_notes.push(`${new Date().toISOString()}: ${sanitizedNote}`);
goal.updated_at = new Date().toISOString();
await this.save(id, goal);
return true;
}
catch (error) {
this.console.error('Failed to add progress note:', error);
throw error;
}
}
/**
* Update goal status
* @param id Goal ID
* @param status New status
*/
async updateStatus(id, status) {
try {
if (!id)
throw new Error('ID is required');
if (!['planning', 'active', 'completed', 'on_hold', 'cancelled'].includes(status)) {
throw new Error('Status must be one of: planning, active, completed, on_hold, or cancelled');
}
const goal = await this.load(id);
if (!goal)
return false;
goal.status = status;
goal.updated_at = new Date().toISOString();
await this.save(id, goal);
return true;
}
catch (error) {
this.console.error('Failed to update goal status:', error);
throw error;
}
}
/**
* Get overdue goals
*/
async getOverdueGoals() {
try {
const goals = await this.list();
const now = new Date().getTime();
return goals.filter(goal => {
if (!goal.target_date)
return false;
if (goal.status === 'completed' || goal.status === 'cancelled')
return false;
const targetDate = new Date(goal.target_date).getTime();
return targetDate < now;
});
}
catch (error) {
this.console.error('Failed to get overdue goals:', 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) {
this.console.error('Failed to get goals by linked task:', error);
throw error;
}
}
/**
* Batch get goals
* @param ids Array of goal IDs
*/
async batchGetGoals(ids) {
try {
if (!Array.isArray(ids))
throw new Error('IDs must be an array');
const goals = [];
for (const id of ids) {
const goal = await this.load(id);
if (goal)
goals.push(goal);
}
return goals;
}
catch (error) {
this.console.error('Failed to batch get goals:', error);
throw error;
}
}
/**
* Validate linked tasks
* @param linkedTasks Array of task IDs
*/
async validateLinkedTasks(linkedTasks) {
// Note: In a real implementation, we would check if tasks exist
// For now, we'll assume task validation will be handled by the task manager
if (!linkedTasks || linkedTasks.length === 0) {
return;
}
// Basic validation - just check that they are strings
for (const taskId of linkedTasks) {
if (typeof taskId !== 'string' || !taskId.trim()) {
throw new Error(`Invalid task ID: ${taskId}`);
}
}
}
/**
* Get goals ordered by ID
*/
async getGoalsInOrder() {
try {
const goals = await this.list();
return goals.sort((a, b) => {
const idA = parseInt(a.id.replace('goal-', ''));
const idB = parseInt(b.id.replace('goal-', ''));
return idA - idB;
});
}
catch (error) {
this.console.error('Failed to get goals in order:', error);
return [];
}
}
/**
* Get cache statistics
*/
getCacheStats() {
return this.cache.getStats();
}
// Private methods - Storage layer functionality
/**
* Save goal
* @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
* @param id Goal ID
*/
async load(id) {
// Check cache
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;
}
this.console.error(`Failed to load goal ${id}:`, error);
return null;
}
}
/**
* Delete goal
* @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;
}
this.console.error(`Failed to delete goal ${id}:`, error);
return false;
}
}
/**
* List all goals
*/
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) {
this.console.error('Failed to list goals:', error);
return [];
}
}
/**
* Sort goals by priority
* @param goals Goals to sort
* @param highFirst Whether high priority comes first
*/
sortByPriority(goals, highFirst = true) {
const priorityValue = {
high: 3,
medium: 2,
low: 1
};
return [...goals].sort((a, b) => {
const valueA = priorityValue[a.priority];
const valueB = priorityValue[b.priority];
return highFirst ? valueB - valueA : valueA - valueB;
});
}
/**
* Sort goals by target date
* @param goals Goals to sort
* @param ascending Whether to sort in ascending order
*/
sortByTargetDate(goals, ascending = true) {
return [...goals].sort((a, b) => {
if (!a.target_date && !b.target_date)
return 0;
if (!a.target_date)
return 1;
if (!b.target_date)
return -1;
const dateA = new Date(a.target_date).getTime();
const dateB = new Date(b.target_date).getTime();
return ascending ? dateA - dateB : dateB - dateA;
});
}
}
exports.GoalManager = GoalManager;
//# sourceMappingURL=goal.js.map