UNPKG

mcp-think-tank

Version:

Structured thinking and knowledge management tool for Model Context Protocol

197 lines (196 loc) 6.44 kB
import fs from 'fs'; import path from 'path'; import os from 'os'; import { createDirectory } from '../utils/fs.js'; // Get tasks path from environment or use default const tasksPath = process.env.TASKS_PATH || path.join(os.homedir(), '.mcp-think-tank/tasks.jsonl'); // Ensure directory exists createDirectory(path.dirname(tasksPath)); // Safely log errors to stderr without interfering with stdout JSON const safeErrorLog = (message) => { // Only log in debug mode or redirect to stderr if (process.env.MCP_DEBUG === 'true') { process.stderr.write(`${message}\n`); } }; export class TaskStorage { constructor() { this.tasks = new Map(); this.saveTimeout = null; this.saveDebounceMs = 5000; // 5 seconds debounce this.isShuttingDown = false; this.load(); } // Load tasks from file load() { try { if (!fs.existsSync(tasksPath)) { // Create empty file if it doesn't exist fs.writeFileSync(tasksPath, ''); return; } const content = fs.readFileSync(tasksPath, 'utf-8'); if (!content.trim()) { return; } // Process file line by line const lines = content.split('\n').filter(line => line.trim()); for (const line of lines) { try { const task = JSON.parse(line); this.tasks.set(task.id, task); } catch (err) { safeErrorLog(`Error parsing task line: ${err}`); } } } catch (err) { safeErrorLog(`Error loading tasks: ${err}`); } } // Save a single task (append to file) saveTask(task, operation) { try { const entry = JSON.stringify({ ...task, _operation: operation, _timestamp: new Date().toISOString() }); fs.appendFileSync(tasksPath, `${entry}\n`); this.logOperation(operation, task); } catch (err) { safeErrorLog(`Error saving task: ${err}`); } } // Clear all timeouts - important for graceful shutdown clearAllTimeouts() { if (this.saveTimeout) { clearTimeout(this.saveTimeout); this.saveTimeout = null; } this.isShuttingDown = true; } // Save immediately - used during shutdown saveImmediately() { if (this.saveTimeout) { clearTimeout(this.saveTimeout); this.saveTimeout = null; } try { // Use sync operations for clean shutdown const tmpTasks = []; for (const task of this.tasks.values()) { const entry = JSON.stringify({ ...task, _operation: 'save', _timestamp: new Date().toISOString() }); tmpTasks.push(entry); } if (tmpTasks.length > 0) { fs.writeFileSync(tasksPath, tmpTasks.join('\n') + '\n', 'utf8'); safeErrorLog(`Saved ${tmpTasks.length} tasks during shutdown`); } } catch (err) { safeErrorLog(`Error during immediate task save: ${err}`); } } // Save all tasks (used for batch operations) save() { // Don't schedule new saves during shutdown if (this.isShuttingDown) { return; } if (this.saveTimeout) { clearTimeout(this.saveTimeout); this.saveTimeout = null; } this.saveTimeout = setTimeout(() => { this.saveTimeout = null; try { // We'll use a temporary file approach to avoid race conditions const tempPath = `${tasksPath}.tmp`; // Use a more direct approach with less event listeners const taskEntries = []; for (const task of this.tasks.values()) { const entry = JSON.stringify({ ...task, _operation: 'save', _timestamp: new Date().toISOString() }); taskEntries.push(entry); } // Write to temp file fs.writeFileSync(tempPath, taskEntries.join('\n') + '\n', 'utf8'); // Rename temp file to real file (atomic operation) fs.renameSync(tempPath, tasksPath); } catch (err) { safeErrorLog(`Error batch saving tasks: ${err}`); } }, this.saveDebounceMs); } // Get all tasks getAllTasks() { return Array.from(this.tasks.values()); } // Get filtered tasks getTasksBy(filter) { return this.getAllTasks().filter(task => { for (const [key, value] of Object.entries(filter)) { if (task[key] !== value) { return false; } } return true; }); } // Add a new task addTask(task) { this.tasks.set(task.id, task); this.saveTask(task, 'add'); return task; } // Update a task updateTask(id, updates) { const task = this.tasks.get(id); if (!task) { return null; } const updatedTask = { ...task, ...updates }; this.tasks.set(id, updatedTask); this.saveTask(updatedTask, 'update'); return updatedTask; } // Delete a task deleteTask(id) { const task = this.tasks.get(id); if (!task) { return false; } this.tasks.delete(id); this.saveTask(task, 'delete'); return true; } // Log operation logOperation(_op, _task) { // No logging } } // Export singleton instance export const taskStorage = new TaskStorage(); // Ensure all tasks are saved on process exit to prevent data loss process.once('beforeExit', () => { safeErrorLog('Process beforeExit - saving tasks'); taskStorage.saveImmediately(); }); // Also handle exit to ensure proper cleanup process.once('exit', () => { safeErrorLog('Process exit - final cleanup'); if (taskStorage) { taskStorage.clearAllTimeouts(); } });