@cabbages/memory-pickle-mcp
Version:
MCP server for AI agent project management - 13 tools for session memory and task tracking
235 lines (234 loc) • 11.2 kB
JavaScript
/**
* Service for universal state recall - combines projects, tasks, and memories
* into optimized, ranked JSON responses for agent planning
*/
export class RecallService {
/**
* Universal state recall - returns ranked, filtered context in single call
*/
static generateStateRecall(database, args = {}) {
const { limit = 20, project_id, include_completed = false, memory_importance, focus = 'all' } = args;
// Get current project
const currentProjectId = project_id || database.meta?.current_project_id;
const currentProject = currentProjectId
? database.projects.find(p => p.id === currentProjectId) || null
: null;
// Filter tasks by project if specified
let tasks = database.tasks;
if (currentProjectId) {
tasks = tasks.filter(t => t.project_id === currentProjectId);
}
// Get active tasks (prioritized and ranked)
const activeTasks = tasks
.filter(t => !t.completed)
.sort((a, b) => {
// Sort by priority first, then creation date
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
if (priorityDiff !== 0)
return priorityDiff;
return new Date(a.created_date).getTime() - new Date(b.created_date).getTime();
})
.slice(0, Math.floor(limit * 0.6)); // 60% of limit for active tasks
// Get overdue tasks (tasks with blockers or high priority older than 7 days)
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const overdueTasks = tasks
.filter(t => !t.completed && ((t.blockers && t.blockers.length > 0) ||
(t.priority === 'critical' && new Date(t.created_date) < sevenDaysAgo)))
.slice(0, 5);
// Get recent completions
const recentCompletions = include_completed
? tasks
.filter(t => t.completed && t.completed_date)
.sort((a, b) => new Date(b.completed_date).getTime() - new Date(a.completed_date).getTime())
.slice(0, Math.floor(limit * 0.2)) // 20% of limit
: [];
// Filter memories
let memories = database.memories;
if (currentProjectId) {
memories = memories.filter(m => m.project_id === currentProjectId ||
(!m.project_id && m.task_id && tasks.some(t => t.id === m.task_id)));
}
if (memory_importance) {
memories = memories.filter(m => m.importance === memory_importance);
}
// Get recent memories (ranked by importance and recency)
const recentMemories = memories
.sort((a, b) => {
// Sort by importance first, then recency
const importanceOrder = { critical: 0, high: 1, medium: 2, low: 3 };
const importanceDiff = importanceOrder[a.importance] - importanceOrder[b.importance];
if (importanceDiff !== 0)
return importanceDiff;
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
})
.slice(0, Math.floor(limit * 0.2)); // 20% of limit
// Calculate project stats
const projectStats = {
total: database.projects.length,
in_progress: database.projects.filter(p => p.status === 'in_progress').length,
completed: database.projects.filter(p => p.status === 'completed').length,
blocked: database.projects.filter(p => p.status === 'blocked').length
};
// Calculate task stats
const allTasks = database.tasks;
const taskStats = {
total: allTasks.length,
active: allTasks.filter(t => !t.completed).length,
completed: allTasks.filter(t => t.completed).length,
critical: allTasks.filter(t => t.priority === 'critical' && !t.completed).length,
high_priority: allTasks.filter(t => t.priority === 'high' && !t.completed).length
};
// Session context
const sessionContext = {
current_project_id: currentProjectId,
total_projects: database.projects.length,
total_tasks: database.tasks.length,
total_memories: database.memories.length
};
return {
current_project: currentProject,
active_tasks: activeTasks,
overdue_tasks: overdueTasks,
recent_completions: recentCompletions,
recent_memories: recentMemories,
project_stats: projectStats,
task_stats: taskStats,
session_context: sessionContext
};
}
/**
* Format state recall as human-readable text
*/
static formatStateRecall(stateData) {
let output = `# 🎯 Universal State Recall\n\n`;
// Current project context
if (stateData.current_project) {
output += `## 📋 Current Project: **${stateData.current_project.name}**\n`;
output += `Status: ${stateData.current_project.status} | Completion: ${stateData.current_project.completion_percentage}%\n`;
if (stateData.current_project.description) {
output += `${stateData.current_project.description.substring(0, 150)}${stateData.current_project.description.length > 150 ? '...' : ''}\n`;
}
output += `\n`;
}
else {
output += `## 📋 No Current Project Set\n`;
output += `Use \`set_current_project\` to focus on a specific project.\n\n`;
}
// Active tasks (prioritized)
if (stateData.active_tasks.length > 0) {
output += `## 🔥 Active Tasks (${stateData.active_tasks.length})\n`;
stateData.active_tasks.forEach((task, index) => {
const priorityEmoji = { critical: '🚨', high: '⚡', medium: '📌', low: '📝' };
output += `${index + 1}. ${priorityEmoji[task.priority]} **${task.title}** (${task.priority})\n`;
if (task.progress && task.progress > 0) {
output += ` Progress: ${task.progress}%\n`;
}
});
output += `\n`;
}
// Overdue/blocked tasks
if (stateData.overdue_tasks.length > 0) {
output += `## ⚠️ Overdue/Blocked Tasks (${stateData.overdue_tasks.length})\n`;
stateData.overdue_tasks.forEach((task, index) => {
output += `${index + 1}. **${task.title}** (${task.priority})\n`;
if (task.blockers && task.blockers.length > 0) {
output += ` 🚫 Blocked: ${task.blockers[0]}\n`;
}
});
output += `\n`;
}
// Recent completions
if (stateData.recent_completions.length > 0) {
output += `## ✅ Recent Completions (${stateData.recent_completions.length})\n`;
stateData.recent_completions.forEach((task, index) => {
const completedDate = task.completed_date
? new Date(task.completed_date).toLocaleDateString()
: 'Recently';
output += `${index + 1}. **${task.title}** (${completedDate})\n`;
});
output += `\n`;
}
// Recent memories
if (stateData.recent_memories.length > 0) {
output += `## 🧠 Recent Context (${stateData.recent_memories.length})\n`;
stateData.recent_memories.forEach((memory, index) => {
const importanceEmoji = { critical: '🚨', high: '⚡', medium: '📝', low: '💡' };
output += `${index + 1}. ${importanceEmoji[memory.importance]} **${memory.title}**\n`;
output += ` ${memory.content.substring(0, 100)}${memory.content.length > 100 ? '...' : ''}\n`;
});
output += `\n`;
}
// Summary stats
output += `## 📊 Summary Statistics\n`;
output += `**Projects:** ${stateData.project_stats.total} total`;
if (stateData.project_stats.in_progress > 0) {
output += ` (${stateData.project_stats.in_progress} in progress)`;
}
if (stateData.project_stats.blocked > 0) {
output += ` (${stateData.project_stats.blocked} blocked)`;
}
output += `\n`;
output += `**Tasks:** ${stateData.task_stats.active}/${stateData.task_stats.total} active`;
if (stateData.task_stats.critical > 0) {
output += ` (${stateData.task_stats.critical} critical)`;
}
output += `\n`;
output += `**Memories:** ${stateData.session_context.total_memories} stored\n`;
return output;
}
/**
* Search and rank memories with advanced filtering
*/
static searchMemories(memories, query, options = {}) {
const { importance, project_id, task_id, limit = 10 } = options;
let filtered = memories;
// Apply filters
if (importance) {
filtered = filtered.filter(m => m.importance === importance);
}
if (project_id) {
filtered = filtered.filter(m => m.project_id === project_id);
}
if (task_id) {
filtered = filtered.filter(m => m.task_id === task_id);
}
// Text search with ranking
if (query?.trim()) {
const searchTerm = query.trim().toLowerCase();
filtered = filtered
.map(memory => {
let score = 0;
const titleMatch = memory.title.toLowerCase().includes(searchTerm);
const contentMatch = memory.content.toLowerCase().includes(searchTerm);
if (titleMatch)
score += 10;
if (contentMatch)
score += 5;
// Boost score based on importance
const importanceBoost = { critical: 4, high: 3, medium: 2, low: 1 };
score += importanceBoost[memory.importance];
// Boost recent memories
const age = Date.now() - new Date(memory.timestamp).getTime();
const daysSinceCreated = age / (1000 * 60 * 60 * 24);
if (daysSinceCreated < 7)
score += 2;
return { memory, score };
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.map(item => item.memory);
}
else {
// Sort by importance and recency if no search query
const importanceOrder = { critical: 0, high: 1, medium: 2, low: 3 };
filtered = filtered.sort((a, b) => {
const importanceDiff = importanceOrder[a.importance] - importanceOrder[b.importance];
if (importanceDiff !== 0)
return importanceDiff;
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
});
}
return filtered.slice(0, limit);
}
}