UNPKG

@cabbages-pre/memory-pickle-mcp-pre

Version:

Simplified MCP server for AI agent project management - 8 essential tools for session memory and task tracking

1,085 lines 61.9 kB
import { InMemoryStore, ProjectService, TaskService, MemoryService, RecallService, ExportService } from '../services/index.js'; import { ValidationUtils } from '../utils/ValidationUtils.js'; import { ProjectNotFoundError, ValidationError } from '../utils/errors.js'; /** * Core business logic for Memory Pickle MCP Server with robust error handling * Contains all the business methods without MCP-specific concerns * Implements defensive programming practices and comprehensive validation */ export class MemoryPickleCore { inMemoryStore; projectService; taskService; memoryService; sessionStartTime; taskIndex; isShuttingDown = false; // Enhanced session tracking sessionActivity; constructor(inMemoryStore, projectService, taskService, memoryService) { this.inMemoryStore = inMemoryStore; this.projectService = projectService; this.taskService = taskService; this.memoryService = memoryService; this.sessionStartTime = new Date(); this.taskIndex = new Map(); this.sessionActivity = { tasksCreated: [], tasksUpdated: [], tasksCompleted: [], memoriesCreated: [], projectsCreated: [], projectSwitches: [], keyDecisions: [], toolUsageCount: new Map() }; this.buildTaskIndex(); } static async create() { const inMemoryStore = new InMemoryStore(); const projectService = new ProjectService(); const taskService = new TaskService(); const memoryService = new MemoryService(); return new MemoryPickleCore(inMemoryStore, projectService, taskService, memoryService); } buildTaskIndex() { if (this.isShuttingDown) return; const database = this.inMemoryStore.getDatabase(); this.taskIndex.clear(); for (const task of database.tasks) { this.taskIndex.set(task.id, task); } } /** * Recalculates and updates project completion percentage based on current tasks */ async _recalculateProjectCompletion(projectId) { await this.inMemoryStore.runExclusive(async (db) => { const project = this.projectService.findProjectById(db.projects, projectId); if (!project) { return { result: undefined, commit: false }; } // Use the existing ProjectService logic for consistency this.projectService.updateProjectCompletion(project, db.tasks); return { result: undefined, commit: true, changedParts: new Set(['projects']) }; }); } /** * Safely executes operations with comprehensive error handling */ async safeExecute(operation, executor) { if (this.isShuttingDown) { throw new Error(`${operation}: System is shutting down`); } try { return await executor(); } catch (error) { // Log error details for debugging (without console output) const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Re-throw with operation context throw new Error(`${operation}: ${errorMessage}`); } } /** * Validates that the current project exists and is accessible */ validateCurrentProject() { // Always get fresh database reference to avoid stale data const currentDatabase = this.inMemoryStore.getDatabase(); const currentProjectId = currentDatabase.meta?.current_project_id; if (!currentProjectId) { throw new ValidationError('current_project_id', undefined, 'No current project set. Use set_current_project or provide project_id parameter.'); } const project = this.projectService.findProjectById(currentDatabase.projects, currentProjectId); if (!project) { // Auto-fix: clear invalid current project currentDatabase.meta.current_project_id = undefined; const availableProjects = currentDatabase.projects.map(p => p.id); throw new ProjectNotFoundError(currentProjectId, availableProjects); } return currentProjectId; } // Project Management Methods async create_project(args) { return this.safeExecute('create_project', async () => { this.trackToolUsage('create_project'); // Basic input validation (MCP schemas handle structure, but we need null checks) if (!args || typeof args !== 'object') { throw new Error('Invalid arguments - expected object'); } const { name, description = '', status = 'planning', dry_run = false } = args; // Check required fields and sanitize if (name === undefined || name === null) { throw new Error("Missing required field 'name'"); } const sanitizedName = ValidationUtils.sanitizeString(name); const sanitizedDescription = ValidationUtils.sanitizeString(description); if (!sanitizedName) { throw new Error("Field 'name' cannot be empty"); } if (sanitizedName.length > 200) { throw new Error('Project name cannot exceed 200 characters'); } if (sanitizedDescription.length > 20000) { throw new Error('Project description cannot exceed 20000 characters'); } if (status && !['planning', 'in_progress', 'blocked', 'completed', 'archived'].includes(status)) { throw new Error('Invalid project status'); } // Handle dry run if (dry_run) { return { content: [{ type: "text", text: `[DRY RUN] create_project: Would create project '${sanitizedName}' with description '${sanitizedDescription}' and set it as current project. No changes made.` }], isError: false }; } const result = await this.inMemoryStore.runExclusive(async (db) => { const newProject = this.projectService.createProject({ name: sanitizedName, description: sanitizedDescription, status: status }); db.projects.push(newProject); // Automatically set this as the current project if (!db.meta) { db.meta = { last_updated: new Date().toISOString(), version: "2.0.0", session_count: 0, current_project_id: newProject.id }; } else { db.meta.current_project_id = newProject.id; db.meta.last_updated = new Date().toISOString(); } return { result: newProject, commit: true, changedParts: new Set(['projects', 'meta']) }; }); // Track session activity this.trackToolUsage('create_project', 'project_created', result.id); return { content: [{ type: "text", text: `[OK] **Project Created Successfully!**\n\n**Name:** ${result.name}\n**ID:** ${result.id}\n**Status:** ${result.status}\n**Description:** ${result.description || 'No description provided'}\n\n[OK] **This project is now your current project.** You can add tasks using the \`create_task\` tool without specifying a project_id.\n\n[INFO] **Note:** Data is stored in memory only. Consider creating markdown files to document your project progress for future reference.` }] }; }); } async get_project_status(args) { const { project_id } = args; // Always get fresh database reference const currentDatabase = this.inMemoryStore.getDatabase(); // If no project_id provided, try to use the current project from meta let targetProjectId = project_id; if (!targetProjectId && currentDatabase.meta?.current_project_id) { targetProjectId = currentDatabase.meta.current_project_id; } // If still no project_id, show all projects if (!targetProjectId) { return this.getAllProjectsStatus(); } const project = this.projectService.findProjectById(currentDatabase.projects, targetProjectId); if (!project) { throw new Error(`Project not found: ${targetProjectId}`); } const projectTasks = currentDatabase.tasks.filter(task => task.project_id === targetProjectId); const completedTasks = projectTasks.filter(task => task.completed); const activeTasks = projectTasks.filter(task => !task.completed); // Calculate completion percentage for display (read-only, no side effects) const displayCompletion = projectTasks.length > 0 ? Math.round((completedTasks.length / projectTasks.length) * 100) : 0; let statusText = `# Project Status: **${project.name}**\n\n`; statusText += `**ID:** ${project.id}\n`; statusText += `**Status:** ${project.status}\n`; statusText += `**Completion:** ${displayCompletion}%\n`; statusText += `**Description:** ${project.description || 'No description'}\n\n`; statusText += `## Tasks Summary\n`; statusText += `- **Total Tasks:** ${projectTasks.length}\n`; statusText += `- **Completed:** ${completedTasks.length}\n`; statusText += `- **Active:** ${activeTasks.length}\n\n`; if (activeTasks.length > 0) { statusText += `### Active Tasks:\n`; activeTasks.slice(0, 5).forEach(task => { statusText += `- **${task.title}** (${task.priority} priority)\n`; }); if (activeTasks.length > 5) { statusText += `- ... and ${activeTasks.length - 5} more\n`; } } return { content: [{ type: "text", text: statusText }] }; } /** * Get status for all projects when no specific project is requested */ getAllProjectsStatus() { const currentDatabase = this.inMemoryStore.getDatabase(); const projects = currentDatabase.projects; if (projects.length === 0) { return { content: [{ type: "text", text: `[INFO] **No Projects Found**\n\nYou haven't created any projects yet. Use the \`create_project\` tool to get started!` }] }; } let statusText = `[INFO] **All Projects Overview**\n\n`; projects.forEach(project => { const projectTasks = currentDatabase.tasks.filter(task => task.project_id === project.id); const completedTasks = projectTasks.filter(task => task.completed); const completion = projectTasks.length > 0 ? Math.round((completedTasks.length / projectTasks.length) * 100) : 0; const isCurrentProject = currentDatabase.meta?.current_project_id === project.id; const currentMarker = isCurrentProject ? ' [CURRENT]' : ''; statusText += `**${project.name}**${currentMarker}\n`; statusText += `- Status: ${project.status}\n`; statusText += `- Tasks: ${projectTasks.length} total, ${completedTasks.length} completed (${completion}%)\n`; statusText += `- ID: ${project.id}\n\n`; }); statusText += `Use \`get_project_status\` with a specific project_id for detailed information.`; return { content: [{ type: "text", text: statusText }] }; } async update_project(args) { const { project_id, name, description, status, dry_run = false } = args; if (!project_id) { throw new Error('Project ID is required'); } // Handle dry run if (dry_run) { return { content: [{ type: "text", text: `[DRY RUN] update_project: Would update project '${project_id}' with provided changes. No changes made.` }], isError: false }; } const result = await this.inMemoryStore.runExclusive(async (db) => { const project = this.projectService.findProjectById(db.projects, project_id); if (!project) { throw new Error(`Project not found: ${project_id}`); } const updates = {}; if (name !== undefined) updates.name = name.trim(); if (description !== undefined) updates.description = description.trim(); if (status !== undefined) updates.status = status; const updatedProject = this.projectService.updateProject(db.projects, project_id, updates); return { result: updatedProject, commit: true, changedParts: new Set(['projects']) }; }); return { content: [{ type: "text", text: `[OK] **Project Updated Successfully!**\n\n**Name:** ${result.name}\n**Status:** ${result.status}\n**Description:** ${result.description || 'No description'}` }] }; } // Task Management Methods async create_task(args) { return this.safeExecute('create_task', async () => { this.trackToolUsage('create_task'); // Basic input validation (MCP schemas handle structure, but we need null checks) if (!args || typeof args !== 'object') { throw new Error('Invalid arguments - expected object'); } const { title, description = '', priority = 'medium', project_id, parent_id, line_range, dry_run = false } = args; // Check required fields and sanitize if (title === undefined || title === null) { throw new Error("Missing required field 'title'"); } const sanitizedTitle = ValidationUtils.sanitizeString(title); const sanitizedDescription = ValidationUtils.sanitizeString(description); if (!sanitizedTitle) { throw new Error("Field 'title' cannot be empty"); } if (sanitizedTitle.length > 200) { throw new Error('Task title cannot exceed 200 characters'); } if (sanitizedDescription.length > 2000) { throw new Error('Task description cannot exceed 2000 characters'); } if (priority && !['low', 'medium', 'high', 'critical'].includes(priority)) { throw new Error('Invalid task priority'); } // If no project_id provided, try to use the current project from meta let targetProjectId = project_id; if (!targetProjectId) { targetProjectId = this.validateCurrentProject(); } // Handle dry run if (dry_run) { return { content: [{ type: "text", text: `[DRY RUN] create_task: Would create task '${sanitizedTitle}' with priority '${priority}' in project '${targetProjectId}'. No changes made.` }], isError: false }; } const result = await this.inMemoryStore.runExclusive(async (db) => { // Verify project exists const project = this.projectService.findProjectById(db.projects, targetProjectId); if (!project) { throw new Error(`Project not found: ${targetProjectId}`); } // Verify parent task exists if provided if (parent_id) { const parentTask = this.taskService.findTaskById(db.tasks, parent_id); if (!parentTask) { throw new Error(`Parent task not found: ${parent_id}`); } if (parentTask.project_id !== targetProjectId) { throw new Error('Parent task must be in the same project'); } } const newTask = this.taskService.createTask({ title: sanitizedTitle, description: sanitizedDescription, priority, project_id: targetProjectId, parent_id, line_range }); db.tasks.push(newTask); return { result: newTask, commit: true, changedParts: new Set(['tasks']) }; }); this.buildTaskIndex(); // Recalculate project completion since we added a new task await this._recalculateProjectCompletion(targetProjectId); // Track session activity this.trackToolUsage('create_task', 'task_created', result.id); const database = this.inMemoryStore.getDatabase(); const project = this.projectService.findProjectById(database.projects, targetProjectId); return { content: [{ type: "text", text: `[OK] **Task Created Successfully!**\n\n**Title:** ${result.title}\n**ID:** ${result.id}\n**Project:** ${project?.name}\n**Priority:** ${result.priority}\n**Description:** ${result.description || 'No description provided'}\n\nTask is ready to be worked on!` }] }; }); } async update_task(args) { return this.safeExecute('update_task', async () => { this.trackToolUsage('update_task'); // Basic input validation (MCP schemas handle structure, but we need null checks) if (!args || typeof args !== 'object') { throw new Error('Invalid arguments - expected object'); } const { task_id, title, description, priority, completed, progress, notes, blockers, dry_run = false } = args; // Check required fields if (task_id === undefined || task_id === null) { throw new Error("Missing required field 'task_id'"); } const sanitizedTitle = title !== undefined ? ValidationUtils.sanitizeString(title) : undefined; const sanitizedDescription = description !== undefined ? ValidationUtils.sanitizeString(description) : undefined; if (sanitizedTitle !== undefined && sanitizedTitle.length > 200) { throw new Error('Task title cannot exceed 200 characters'); } if (sanitizedDescription !== undefined && sanitizedDescription.length > 2000) { throw new Error('Task description cannot exceed 2000 characters'); } if (priority !== undefined && !['low', 'medium', 'high', 'critical'].includes(priority)) { throw new Error('Invalid task priority'); } // Handle dry run if (dry_run) { return { content: [{ type: "text", text: `[DRY RUN] update_task: Would update task '${task_id}' with provided changes. No changes made.` }], isError: false }; } const result = await this.inMemoryStore.runExclusive(async (db) => { const task = this.taskService.findTaskById(db.tasks, task_id); if (!task) { throw new Error(`Task not found: ${task_id}`); } const updates = {}; if (sanitizedTitle !== undefined) updates.title = sanitizedTitle; if (sanitizedDescription !== undefined) updates.description = sanitizedDescription; if (priority !== undefined) updates.priority = priority; if (completed !== undefined) { updates.completed = completed; if (completed) { updates.completed_date = new Date().toISOString(); } } if (progress !== undefined) updates.progress = progress; const updatedTask = this.taskService.updateTask(db.tasks, task_id, updates); // Add notes and blockers if provided if (notes) { if (!updatedTask.notes) updatedTask.notes = []; if (Array.isArray(notes)) { // Handle array of notes for (const note of notes) { if (note?.trim()) { updatedTask.notes.push(`${new Date().toISOString()}: ${note.trim()}`); } } } else if (typeof notes === 'string' && notes.trim()) { // Handle single note string updatedTask.notes.push(`${new Date().toISOString()}: ${notes.trim()}`); } } if (blockers && Array.isArray(blockers)) { updatedTask.blockers = [...(updatedTask.blockers || []), ...blockers]; } // Add progress note as memory if provided const noteContent = Array.isArray(notes) ? notes.join('; ') : notes; if (noteContent?.trim()) { this.memoryService.addMemory(db.memories, { title: `Progress: ${task.title}`, content: noteContent.trim(), importance: 'medium', project_id: task.project_id, task_id: task.id }); } return { result: updatedTask, commit: true, changedParts: new Set(['tasks', 'memories']) }; }); this.buildTaskIndex(); // Recalculate project completion if task completion status changed if (completed !== undefined) { await this._recalculateProjectCompletion(result.project_id); } // Track session activity this.trackToolUsage('update_task', 'task_updated', result.id); if (completed === true) { this.trackToolUsage('update_task', 'task_completed', result.id); } let response = `[OK] **Task Updated Successfully!**\n\n**${result.title}**\n`; if (completed !== undefined) { response += `Status: ${result.completed ? 'Completed [DONE]' : 'Active [ACTIVE]'}\n`; } if (priority !== undefined) { response += `Priority: ${result.priority}\n`; } if (progress !== undefined) { response += `Progress: ${progress}%\n`; } const noteContent = Array.isArray(notes) ? notes.join('; ') : notes; if (noteContent?.trim()) { response += `Progress note saved.\n`; } if (blockers && blockers.length > 0) { response += `Blockers added: ${blockers.join(', ')}\n`; } return { content: [{ type: "text", text: response }] }; }); } // Memory Management Methods async remember_this(args) { return this.safeExecute('remember_this', async () => { this.trackToolUsage('remember_this'); // Basic input validation (MCP schemas handle structure, but we need null checks) if (!args || typeof args !== 'object') { throw new Error('Invalid arguments - expected object'); } const { content, title, importance = 'medium', project_id, task_id, line_range, dry_run = false } = args; // Check required fields and sanitize if (content === undefined || content === null) { throw new Error("Missing required field 'content'"); } const sanitizedContent = ValidationUtils.sanitizeString(content); const sanitizedTitle = title ? ValidationUtils.sanitizeString(title) : undefined; if (!sanitizedContent) { throw new Error("Field 'content' cannot be empty"); } // Use InMemoryStore validation for memory data const memoryData = { content: sanitizedContent, title: sanitizedTitle, importance, project_id, task_id, line_range }; const validatedMemory = this.inMemoryStore.validateAndSanitizeInput('memory', memoryData); // Handle dry run if (dry_run) { return { content: [{ type: "text", text: `[DRY RUN] remember_this: Would store memory '${sanitizedTitle || 'Untitled'}' with importance '${importance}'. No changes made.` }], isError: false }; } const result = await this.inMemoryStore.runExclusive(async (db) => { const newMemory = this.memoryService.addMemory(db.memories, { title: validatedMemory.title || `Memory from ${new Date().toLocaleDateString()}`, content: validatedMemory.content, importance: validatedMemory.importance, project_id: validatedMemory.project_id, task_id: validatedMemory.task_id, line_range: validatedMemory.line_range }); return { result: newMemory, commit: true, changedParts: new Set(['memories']) }; }); // Track session activity this.trackToolUsage('remember_this', 'memory_created', result.id); // Generate markdown suggestion for critical/high importance memories let markdownSuggestion = ''; if (result.importance === 'critical' || result.importance === 'high') { const filename = result.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') + '.md'; markdownSuggestion = `\n\n[INFO] **Suggestion:** Since this is ${result.importance} importance, consider creating a markdown file:\n\`\`\`markdown\n# ${result.title}\n\n${result.content}\n\n*Created: ${new Date().toLocaleDateString()}*\n*Importance: ${result.importance}*\n\`\`\`\n\nSave as: \`${filename}\``; } return { content: [{ type: "text", text: `[OK] **Memory Saved!**\n\n**Title:** ${result.title}\n**Importance:** ${result.importance}\n**Content:** ${result.content.substring(0, 100)}${result.content.length > 100 ? '...' : ''}${markdownSuggestion}` }] }; }); } /** * Universal state recall - replaces recall_context with comprehensive state overview */ async recall_state(args = {}) { this.trackToolUsage('recall_state'); const { limit = 20, project_id, include_completed = false, memory_importance, focus = 'all', format = 'text' } = args; const database = this.inMemoryStore.getDatabase(); // Generate comprehensive state data const stateData = RecallService.generateStateRecall(database, { limit, project_id, include_completed, memory_importance, focus }); if (format === 'json') { return { content: [{ type: "text", text: JSON.stringify(stateData, null, 2) }] }; } // Default: formatted text response const formattedText = RecallService.formatStateRecall(stateData); return { content: [{ type: "text", text: formattedText }] }; } async recall_context(args = {}) { const { query, project_id, importance, limit = 10 } = args; // Always get fresh database reference const database = this.inMemoryStore.getDatabase(); let memories = database.memories; // Apply filters if (project_id) { memories = memories.filter(memory => memory.project_id === project_id); } if (importance) { memories = memories.filter(memory => memory.importance === importance); } // Simple text search if query provided if (query?.trim()) { const searchTerm = query.trim().toLowerCase(); memories = memories.filter(memory => memory.title.toLowerCase().includes(searchTerm) || memory.content.toLowerCase().includes(searchTerm)); } // Sort by creation date (newest first) and limit memories = memories .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .slice(0, limit); if (memories.length === 0) { return { content: [{ type: "text", text: `[INFO] **No Memories Found**\n\nNo memories match your search criteria.` }] }; } let response = `# Recalled Memories\n\n`; memories.forEach((memory, index) => { response += `## ${index + 1}. ${memory.title}\n`; response += `**Importance:** ${memory.importance}\n`; response += `**Date:** ${new Date(memory.timestamp).toLocaleDateString()}\n`; if (memory.project_id) { const project = this.projectService.findProjectById(database.projects, memory.project_id); response += `**Project:** ${project?.name || 'Unknown'}\n`; } response += `**Content:** ${memory.content}\n\n`; }); return { content: [{ type: "text", text: response }] }; } async generate_handoff_summary(args = {}) { this.trackToolUsage('generate_handoff_summary'); const { project_id, format = 'detailed' } = args; // Always get fresh database reference const database = this.inMemoryStore.getDatabase(); let projects = database.projects; if (project_id) { const project = this.projectService.findProjectById(projects, project_id); if (!project) { throw new Error(`Project not found: ${project_id}`); } projects = [project]; } // Get session activity data const sessionActivity = this.getSessionActivity(); let summary = `# [HANDOFF] Enhanced Session Summary\n\n`; summary += `**Generated:** ${new Date().toLocaleString()}\n`; summary += `**Session Duration:** ${sessionActivity.sessionDuration} minutes\n`; summary += `**Session Activity:** ${Object.values(sessionActivity.toolUsageCount).reduce((a, b) => a + b, 0)} tool calls\n\n`; // Add session activity summary if (sessionActivity.tasksCreated.length > 0 || sessionActivity.tasksUpdated.length > 0 || sessionActivity.memoriesCreated.length > 0) { summary += `## [SESSION] What Happened This Session\n\n`; if (sessionActivity.tasksCreated.length > 0) { summary += `**Tasks Created:** ${sessionActivity.tasksCreated.length}\n`; sessionActivity.tasksCreated.slice(0, 3).forEach(taskId => { const task = database.tasks.find(t => t.id === taskId); if (task) summary += `- ${task.title}\n`; }); if (sessionActivity.tasksCreated.length > 3) { summary += `- ... and ${sessionActivity.tasksCreated.length - 3} more\n`; } summary += `\n`; } if (sessionActivity.tasksCompleted.length > 0) { summary += `**Tasks Completed:** ${sessionActivity.tasksCompleted.length}\n`; sessionActivity.tasksCompleted.forEach(taskId => { const task = database.tasks.find(t => t.id === taskId); if (task) summary += `- ✅ ${task.title}\n`; }); summary += `\n`; } if (sessionActivity.memoriesCreated.length > 0) { summary += `**Notes/Memories Created:** ${sessionActivity.memoriesCreated.length}\n`; sessionActivity.memoriesCreated.slice(0, 3).forEach(memoryId => { const memory = database.memories.find(m => m.id === memoryId); if (memory) summary += `- ${memory.title}\n`; }); if (sessionActivity.memoriesCreated.length > 3) { summary += `- ... and ${sessionActivity.memoriesCreated.length - 3} more\n`; } summary += `\n`; } if (Object.keys(sessionActivity.toolUsageCount).length > 0) { summary += `**Tool Usage:**\n`; Object.entries(sessionActivity.toolUsageCount).forEach(([tool, count]) => { summary += `- ${tool}: ${count}x\n`; }); summary += `\n`; } summary += `---\n\n`; } for (const project of projects) { const projectTasks = database.tasks.filter(task => task.project_id === project.id); const completedTasks = projectTasks.filter(task => task.completed); const activeTasks = projectTasks.filter(task => !task.completed); summary += `## [PROJECT] ${project.name}\n\n`; summary += `**Status:** ${project.status}\n`; summary += `**Progress:** ${project.completion_percentage}%\n`; summary += `**Tasks:** ${completedTasks.length}/${projectTasks.length} completed\n\n`; if (activeTasks.length > 0) { summary += `### [ACTIVE] Active Tasks\n`; activeTasks.forEach(task => { summary += `- **${task.title}** (${task.priority} priority)\n`; }); summary += `\n`; } if (completedTasks.length > 0) { summary += `### [DONE] Recently Completed\n`; completedTasks.slice(-3).forEach(task => { summary += `- **${task.title}**\n`; }); summary += `\n`; } // Recent memories for context const recentMemories = database.memories .filter(memory => memory.project_id === project.id) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .slice(0, 3); if (recentMemories.length > 0) { summary += `### [NOTES] Recent Notes\n`; recentMemories.forEach(memory => { summary += `- **${memory.title}:** ${memory.content.substring(0, 100)}${memory.content.length > 100 ? '...' : ''}\n`; }); summary += `\n`; } summary += `---\n\n`; } // Add markdown file suggestion const dateStr = new Date().toISOString().split('T')[0]; const markdownSuggestion = `\n\n[SUGGESTION] Save this summary as a markdown file for future reference:\n\`session-summary-${dateStr}.md\`\n\nThis will help you pick up where you left off in future sessions.`; return { content: [{ type: "text", text: summary + markdownSuggestion }] }; } async set_current_project(args) { this.trackToolUsage('set_current_project'); const { project_id, dry_run = false } = args; if (!project_id) { throw new Error('Project ID is required'); } // Handle dry run if (dry_run) { return { content: [{ type: "text", text: `[DRY RUN] set_current_project: Would set project '${project_id}' as current project. No changes made.` }], isError: false }; } const result = await this.inMemoryStore.runExclusive(async (db) => { const project = this.projectService.findProjectById(db.projects, project_id); if (!project) { throw new Error(`Project not found: ${project_id}`); } // Update meta information if (!db.meta) { db.meta = { last_updated: new Date().toISOString(), version: "2.0.0", session_count: 0, current_project_id: project_id }; } else { db.meta.current_project_id = project_id; } return { result: project, commit: true, changedParts: new Set(['meta']) }; }); // Track session activity this.trackToolUsage('set_current_project', 'project_switched', result.id); return { content: [{ type: "text", text: `[OK] Current project set to: **${result.name}**\n\nAll new tasks will be added to this project by default.` }] }; } // Getter methods for accessing internal state (useful for handlers) getDatabase() { return this.inMemoryStore.getDatabase(); } getTaskIndex() { return this.taskIndex; } /** * Cleanup method for graceful shutdown */ async shutdown() { this.isShuttingDown = true; // Clear task index to free memory this.taskIndex.clear(); // Cleanup in-memory store this.inMemoryStore.cleanup(); } /** * Track tool usage and session activity */ trackToolUsage(toolName, activityType, itemId) { // Track tool usage count const currentCount = this.sessionActivity.toolUsageCount.get(toolName) || 0; this.sessionActivity.toolUsageCount.set(toolName, currentCount + 1); // Track specific activities if (activityType && itemId) { switch (activityType) { case 'task_created': this.sessionActivity.tasksCreated.push(itemId); break; case 'task_updated': this.sessionActivity.tasksUpdated.push(itemId); break; case 'task_completed': this.sessionActivity.tasksCompleted.push(itemId); break; case 'memory_created': this.sessionActivity.memoriesCreated.push(itemId); break; case 'project_created': this.sessionActivity.projectsCreated.push(itemId); break; case 'project_switched': this.sessionActivity.projectSwitches.push(itemId); this.sessionActivity.lastActiveProject = itemId; break; } } } /** * Get session activity summary */ getSessionActivity() { return { ...this.sessionActivity, sessionDuration: Math.round((Date.now() - this.sessionStartTime.getTime()) / 1000 / 60), toolUsageCount: Object.fromEntries(this.sessionActivity.toolUsageCount) }; } /** * Reset session activity (useful for testing) */ resetSessionActivity() { this.sessionActivity = { tasksCreated: [], tasksUpdated: [], tasksCompleted: [], memoriesCreated: [], projectsCreated: [], projectSwitches: [], keyDecisions: [], toolUsageCount: new Map() }; this.sessionStartTime = new Date(); } // MCP Tool Wrappers for hidden methods /** * Internal system stats (no longer exposed as MCP tool) */ async get_system_stats(args = {}) { this.trackToolUsage('get_system_stats'); const { dry_run = false } = args; if (dry_run) { return { content: [{ type: "text", text: `[DRY RUN] get_system_stats: Would return current system statistics. No changes made.` }], isError: false }; } const stats = this.getSystemStats(); return { content: [{ type: "text", text: `[INFO] **System Statistics**\n\n**Database:**\n- Projects: ${stats.database.projects}\n- Tasks: ${stats.database.tasks}\n- Memories: ${stats.database.memories}\n- Queued Operations: ${stats.database.queuedOperations}\n- Last Updated: ${stats.database.lastUpdated}\n\n**Session:**\n- Task Index Size: ${stats.taskIndexSize}\n- Session Duration: ${stats.sessionDuration} minutes\n- Status: ${stats.isShuttingDown ? 'Shutting down' : 'Active'}` }] }; } /** * Internal data cleanup (no longer exposed as MCP tool) */ async cleanup_orphaned_data(args = {}) { return this.safeExecute('cleanup_orphaned_data', async () => { this.trackToolUsage('cleanup_orphaned_data'); const { dry_run = false } = args; if (dry_run) { return { content: [{ type: "text", text: `[DRY RUN] cleanup_orphaned_data: Would perform data integrity cleanup. No changes made.` }], isError: false }; } const stats = await this.cleanupOrphanedData(); let report = `[OK] **Data Cleanup Complete**\n\n`; report += `**Issues Fixed:**\n`; report += `- Orphaned Tasks: ${stats.orphanedTasks}\n`; report += `- Orphaned Memories: ${stats.orphanedMemories}\n`; report += `- Invalid Task References: ${stats.invalidTaskReferences}\n`; report += `- Invalid Current Project: ${stats.invalidCurrentProject ? 'Yes' : 'No'}\n`; report += `- Duplicates Removed: ${stats.duplicatesRemoved}\n`; report += `- Corrupted Data Fixed: ${stats.corruptedDataFixed}\n\n`; const totalIssues = stats.orphanedTasks + stats.orphanedMemories + stats.invalidTaskReferences + (stats.invalidCurrentProject ? 1 : 0) + stats.duplicatesRemoved + stats.corruptedDataFixed; if (totalIssues === 0) { report += `[INFO] Database is clean - no issues found.`; } else { report += `[INFO] Fixed ${totalIssues} total issues.`; } return { content: [{ type: "text", text: report }] }; }); } // New Read-Only Tools for better agent workflow /** * List tasks with filtering and pagination */ async list_tasks(args = {}) { this.trackToolUsage('list_tasks'); const { status, priority, project_id, completed, limit = 50, offset = 0 } = args; const database = this.inMemoryStore.getDatabase(); let tasks = database.tasks; // Apply filters if (project_id) { tasks = tasks.filter(task => task.project_id === project_id); } if (status !== undefined) { // Map status to completed boolean for consistency if (status === 'completed') { tasks = tasks.filter(task => task.completed); } else if (status === 'active') { tasks = tasks.filter(task => !task.completed); } } if (completed !== undefined) { tasks = tasks.filter(task => task.completed === completed); } if (priority) { tasks = tasks.filter(task => task.priority === priority); } // Sort by priority (critical > high > medium > low) and creation date const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; tasks = tasks.sort((a, b) => { const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority]; if (priorityDiff !== 0) return priorityDiff; return new Date(b.created_date).getTime() - new Date(a.created_date).getTime(); }); // Apply pagination const totalTasks = tasks.length; tasks = tasks.slice(offset, offset + limit); // Format response let response = `# Task List\n\n`; response += `**Total Tasks:** ${totalTasks} (showing ${tasks.length})\n`; if (project_id) { const project = this.projectService.findProjectById(database.projects, project_id); response += `**Project:** ${project?.name || 'Unknown'}\n`; } response += `\n`; if (tasks.length === 0) { response += `[INFO] No tasks found matching the criteria.`; } else { tasks.forEach((task, index) => { const status = task.completed ? '[DONE]' : '[ACTIVE]'; response += `${offset + index + 1}. **${task.title}** ${status}\n`; response += ` Priority: ${task.priority} | ID: ${task.id}\n`; if (task.progress && task.progress > 0) { response += ` Progress: ${task.progress}%\n`; } if (task.description) { response += ` ${task.description.substring(0, 100)}${task.description.length > 100 ? '...' : ''}\n`; } response += `\n`; }); } return { content: [{ type: "text", text: response }] }; } /** * List projects with basic info */ async list_projects(args = {}) { this.trackToolUsage('list_projects'); const { status, limit = 50, offset = 0 } = args; const database = this.inMemoryStore.getDatabase(); let projects = database.projects; // Apply filters if (status) { projects = projects.filter(project => project.status === status); } // Sort by status priority and creation date const statusOrder = { in_progress: 0, planning: 1, blocked: 2, completed: 3, archived: 4 }; projects = projects.sort((a, b) => { const statusDiff = (statusOrder[a.status] || 99) - (statusOrder[b.status] || 99); if (statusDiff !== 0) return statusDiff; return new Date(b.created_date).getTime() - new Date(a.created_date).getTime(); }); // Apply pagination const totalProjects = projects.length; projects = projects.slice(offset, offset + limit); // Calculate task counts for each project const projectsWithStats = projects.map(project => { const projectTasks = database.tasks.filter(task => task.project_id === project.id); const completedTasks = projectTasks.filter(task => task.completed); return { ...project, totalTasks: projectTasks.length, completedTasks: completedTasks.length, completion: projectTasks.length > 0 ? Math.round((completedTasks.length / projectTasks.length) * 100) : 0 }; }); // Format response let response = `# Project List\n\n`; response += `**Total Projects:** ${totalProjects} (showing ${projects.length})\n\n`; if (projectsWithStats.length === 0) { response += `[INFO] No projects found matching the criteria.`; } else { projectsWithStats.forEach((project, index) => { const isCurrentProject = database.meta?.current_project_id === project.id; const currentMarker = isCurrentProject ? ' [CURRENT]' : ''; response += `${offset + index + 1}. **${project.name}**${currentMarker}\n`; response += ` Status: ${project.status} | Completion: ${project.completion}%\n`; respo