UNPKG

@cabbages/memory-pickle-mcp

Version:

MCP server for AI agent project management - 13 tools for session memory and task tracking

182 lines (180 loc) 7.57 kB
import { generateId } from '../utils/idGenerator.js'; /** * Service responsible for memory management and handoff operations */ export class MemoryService { /** * Creates a new memory entry */ createMemory(args) { const { title, content, category = 'general', importance = 'medium', tags = [], task_id, project_id, line_range } = args; if (!title || !content) { throw new Error('Title and content are required'); } // Validate line_range if provided if (line_range) { if (typeof line_range.start_line !== 'number' || typeof line_range.end_line !== 'number') { throw new Error('Line range start_line and end_line must be numbers'); } if (line_range.start_line < 1 || line_range.end_line < 1) { throw new Error('Line numbers must be positive (1-based)'); } if (line_range.start_line > line_range.end_line) { throw new Error('start_line must be less than or equal to end_line'); } } return { id: generateId('mem'), timestamp: new Date().toISOString(), category, importance, tags: Array.isArray(tags) ? tags : [tags], title, content, task_id, project_id, related_memories: [], line_range }; } /** * Creates and adds a memory to the memories array */ addMemory(memories, args) { const memory = this.createMemory(args); memories.push(memory); return memory; } /** * Searches memories based on query and filters */ searchMemories(memories, args) { const { query, category, tags, limit = 10 } = args; // Allow searching by tags alone, or require query if no other filters if (!query && !category && !tags) { return []; } const lowerQuery = query?.toLowerCase(); const results = memories.filter(memory => { const lowerCaseTags = memory.tags.map(t => t.toLowerCase()); const matchesQuery = !lowerQuery || (memory.title.toLowerCase().includes(lowerQuery) || memory.content.toLowerCase().includes(lowerQuery) || lowerCaseTags.some(tag => tag.includes(lowerQuery))); const matchesCategory = !category || memory.category.toLowerCase() === category.toLowerCase(); // Use AND logic for tags: all provided tags must be present. const matchesTags = !tags || tags.every((tag) => lowerCaseTags.includes(tag.toLowerCase())); return matchesQuery && matchesCategory && matchesTags; }).slice(0, limit); return results; } /** * Formats memory search results for display */ formatMemoryResults(memories) { const formattedResults = memories.map(memory => { return `## ${memory.title} **Category:** ${memory.category} | **Importance:** ${memory.importance} **Tags:** ${memory.tags.join(', ')} **Date:** ${new Date(memory.timestamp).toLocaleDateString()} ${memory.content}`; }).join('\n\n---\n\n'); return `[FOUND] Found ${memories.length} relevant memories:\n\n${formattedResults}`; } /** * Generates a handoff summary for session transitions */ generateHandoffSummary(project, tasks, sessionCount, sessionStartTime) { // Get tasks completed in current session (default to last 2 hours if no session start time) const sessionCutoff = sessionStartTime || new Date(Date.now() - 2 * 60 * 60 * 1000); const recentlyCompleted = tasks .filter(t => t.project_id === project.id && t.completed && t.completed_date && new Date(t.completed_date) > sessionCutoff) .map(t => t.title); const inProgress = tasks .filter(t => t.project_id === project.id && !t.completed && (t.progress && t.progress > 0)) .map(t => `${t.title} (${t.progress}%)`); const blocked = tasks .filter(t => t.project_id === project.id && t.blockers && t.blockers.length > 0) .map(t => `${t.title}: ${t.blockers.join(', ')}`); const upcoming = tasks .filter(t => t.project_id === project.id && !t.completed && (t.priority === 'critical' || t.priority === 'high')) .sort((a, b) => { const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }) .slice(0, 5) .map(t => `${t.title} (${t.priority})`); return { project_name: project.name, completion_percentage: project.completion_percentage, last_session_date: new Date().toISOString(), completed_in_last_session: recentlyCompleted, in_progress: inProgress, blocked_items: blocked, next_priorities: upcoming, session_notes: `Session #${sessionCount + 1}` }; } /** * Formats handoff summary for display */ formatHandoffSummary(handoff, format = 'detailed') { let result = `# [HANDOFF] Project Handoff Summary\n\n`; result += `**Project:** ${handoff.project_name}\n`; result += `**Completion:** ${handoff.completion_percentage}%\n`; result += `**Session Date:** ${new Date(handoff.last_session_date).toLocaleString()}\n\n`; if (format === 'detailed') { result += `## [DONE] Completed This Session\n`; if (handoff.completed_in_last_session.length > 0) { handoff.completed_in_last_session.forEach(item => { result += `- ${item}\n`; }); } else { result += `- No tasks completed this session\n`; } result += `\n## [ACTIVE] In Progress\n`; if (handoff.in_progress.length > 0) { handoff.in_progress.forEach(item => { result += `- ${item}\n`; }); } else { result += `- No tasks currently in progress\n`; } if (handoff.blocked_items.length > 0) { result += `\n## [BLOCKED] Blocked Items\n`; handoff.blocked_items.forEach(item => { result += `- ${item}\n`; }); } result += `\n## [NEXT] Next Priorities\n`; if (handoff.next_priorities.length > 0) { handoff.next_priorities.forEach(item => { result += `- ${item}\n`; }); } else { result += `- No high priority items pending\n`; } } else { // Compact format for easy copy/paste result += `**Quick Summary:**\n`; result += `Completed: ${handoff.completed_in_last_session.length} tasks | `; result += `In Progress: ${handoff.in_progress.length} | `; result += `Blocked: ${handoff.blocked_items.length}\n\n`; result += `**Continue with:** ${handoff.next_priorities[0] || 'Review project status for next tasks'}`; } result += `\n\n---\n*Copy this summary to your next chat session to continue where you left off.*`; return result; } }