MCP Personal Assistant Agent
/**
* Local Task Provider
* Manages tasks using a local JSON file storage
*/
import fs from 'fs/promises';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import logger from '../../../utils/logger.js';
import config from '../../../config/index.js';
let localTaskProvider = null;
/**
* Local Task Provider Class
*/
class LocalTaskProvider {
constructor(filePath) {
this.filePath = filePath;
this.tasks = [];
this.initialized = false;
}
/**
* Initialize the provider and load data
*/
async initialize() {
if (this.initialized) {
return;
}
try {
// Ensure directory exists
const directory = path.dirname(this.filePath);
await fs.mkdir(directory, { recursive: true });
// Try to load tasks from file
try {
const fileContent = await fs.readFile(this.filePath, 'utf8');
this.tasks = JSON.parse(fileContent);
logger.info(`Loaded ${this.tasks.length} tasks from ${this.filePath}`);
} catch (error) {
// File might not exist yet, that's ok
if (error.code !== 'ENOENT') {
logger.warn(`Error reading task file: ${error.message}`);
}
this.tasks = [];
// Create the file with empty array
await this.saveToFile();
logger.info(`Created new task file at ${this.filePath}`);
}
this.initialized = true;
} catch (error) {
logger.error(`Failed to initialize local task provider: ${error.message}`);
throw new Error(`Failed to initialize local task provider: ${error.message}`);
}
}
/**
* Save tasks to file
*/
async saveToFile() {
try {
await fs.writeFile(this.filePath, JSON.stringify(this.tasks, null, 2));
logger.info(`Saved ${this.tasks.length} tasks to ${this.filePath}`);
} catch (error) {
logger.error(`Failed to save tasks to file: ${error.message}`);
throw new Error(`Failed to save tasks: ${error.message}`);
}
}
/**
* Get all tasks with optional filtering
*/
async getTasks(filters = {}) {
await this.initialize();
try {
logger.info(`Getting tasks with filters: ${JSON.stringify(filters)}`);
// Start with all tasks
let filteredTasks = [...this.tasks];
// Apply status filter
if (filters.status) {
filteredTasks = filteredTasks.filter(task => task.status === filters.status);
}
// Apply priority filter
if (filters.priority) {
filteredTasks = filteredTasks.filter(task => task.priority === filters.priority);
}
// Apply due date filter
if (filters.dueDate) {
// Handle relative dates
let targetDate;
if (filters.dueDate === 'today') {
targetDate = new Date().toISOString().split('T')[0];
} else if (filters.dueDate === 'tomorrow') {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
targetDate = tomorrow.toISOString().split('T')[0];
} else if (filters.dueDate === 'this_week') {
// Filter for the next 7 days
const today = new Date();
const nextWeek = new Date();
nextWeek.setDate(today.getDate() + 7);
filteredTasks = filteredTasks.filter(task => {
if (!task.dueDate) return false;
const taskDate = new Date(task.dueDate);
return taskDate >= today && taskDate <= nextWeek;
});
// Return early since we've already applied this filter
return {
success: true,
tasks: filteredTasks
};
} else {
// Assume it's a specific date
targetDate = filters.dueDate;
}
// Filter by exact date
filteredTasks = filteredTasks.filter(task => task.dueDate === targetDate);
}
return {
success: true,
tasks: filteredTasks
};
} catch (error) {
logger.error(`Failed to get tasks: ${error.message}`);
throw new Error(`Failed to get tasks: ${error.message}`);
}
}
/**
* Get a task by ID
*/
async getTaskById(taskId) {
await this.initialize();
try {
logger.info(`Getting task with ID: ${taskId}`);
const task = this.tasks.find(t => t.id === taskId);
if (!task) {
throw new Error(`Task with ID ${taskId} not found`);
}
return {
success: true,
task
};
} catch (error) {
logger.error(`Failed to get task: ${error.message}`);
throw new Error(`Failed to get task: ${error.message}`);
}
}
/**
* Create a new task
*/
async createTask(taskData) {
await this.initialize();
try {
logger.info(`Creating new task: ${taskData.title}`);
// Format due date if it's relative
let dueDate = taskData.dueDate;
if (dueDate) {
if (dueDate === 'today') {
dueDate = new Date().toISOString().split('T')[0];
} else if (dueDate === 'tomorrow') {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
dueDate = tomorrow.toISOString().split('T')[0];
}
}
// Create new task
const newTask = {
id: uuidv4(),
title: taskData.title,
description: taskData.description || '',
status: 'open',
priority: taskData.priority || 'medium',
dueDate: dueDate,
tags: taskData.tags || [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// Add to tasks
this.tasks.push(newTask);
// Save to file
await this.saveToFile();
return {
success: true,
task: newTask
};
} catch (error) {
logger.error(`Failed to create task: ${error.message}`);
throw new Error(`Failed to create task: ${error.message}`);
}
}
/**
* Update an existing task
*/
async updateTask(taskData) {
await this.initialize();
try {
logger.info(`Updating task with ID: ${taskData.id}`);
// Find task
const taskIndex = this.tasks.findIndex(t => t.id === taskData.id);
if (taskIndex === -1) {
throw new Error(`Task with ID ${taskData.id} not found`);
}
// Get the existing task
const existingTask = this.tasks[taskIndex];
// Format due date if it's relative
let dueDate = taskData.dueDate;
if (dueDate) {
if (dueDate === 'today') {
dueDate = new Date().toISOString().split('T')[0];
} else if (dueDate === 'tomorrow') {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
dueDate = tomorrow.toISOString().split('T')[0];
}
}
// Update task with new values, only if provided
const updatedTask = {
...existingTask,
title: taskData.title !== undefined ? taskData.title : existingTask.title,
description: taskData.description !== undefined ? taskData.description : existingTask.description,
status: taskData.status !== undefined ? taskData.status : existingTask.status,
priority: taskData.priority !== undefined ? taskData.priority : existingTask.priority,
dueDate: dueDate !== undefined ? dueDate : existingTask.dueDate,
tags: taskData.tags !== undefined ? taskData.tags : existingTask.tags,
updatedAt: new Date().toISOString()
};
// Update in array
this.tasks[taskIndex] = updatedTask;
// Save to file
await this.saveToFile();
return {
success: true,
task: updatedTask
};
} catch (error) {
logger.error(`Failed to update task: ${error.message}`);
throw new Error(`Failed to update task: ${error.message}`);
}
}
/**
* Delete a task
*/
async deleteTask(taskId) {
await this.initialize();
try {
logger.info(`Deleting task with ID: ${taskId}`);
// Find task
const taskIndex = this.tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
throw new Error(`Task with ID ${taskId} not found`);
}
// Remove from array
this.tasks.splice(taskIndex, 1);
// Save to file
await this.saveToFile();
return {
success: true,
message: `Task with ID ${taskId} deleted successfully`
};
} catch (error) {
logger.error(`Failed to delete task: ${error.message}`);
throw new Error(`Failed to delete task: ${error.message}`);
}
}
}
/**
* Get the local task provider instance
* @returns {Promise<LocalTaskProvider>} Local task provider instance
*/
export async function getLocalTaskProvider() {
if (localTaskProvider) {
return localTaskProvider;
}
try {
logger.info('Initializing local task provider');
// Check if local tasks are enabled in config
if (!config.modules.tasks.providers.local.enabled) {
throw new Error('Local task provider is disabled in configuration');
}
// Get file path from config
const filePath = path.resolve(process.cwd(), config.modules.tasks.providers.local.storageFile);
// Create provider
localTaskProvider = new LocalTaskProvider(filePath);
await localTaskProvider.initialize();
logger.info('Local task provider initialized successfully');
return localTaskProvider;
} catch (error) {
logger.error(`Failed to get local task provider: ${error.message}`);
throw new Error(`Failed to get local task provider: ${error.message}`);
}
}