mcp-think-tank
Version:
Structured thinking and knowledge management tool for Model Context Protocol
197 lines (196 loc) • 6.44 kB
JavaScript
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();
}
});