UNPKG

@endlessblink/like-i-said-v2

Version:

Task Management & Memory for Claude - Track tasks, remember context, and maintain continuity across sessions with 27 powerful tools. Works with Claude Desktop and Claude Code.

521 lines (438 loc) 16.2 kB
import chokidar from 'chokidar'; import fs from 'fs'; import path from 'path'; import { TaskAutomation } from './task-automation.js'; import { AutomationConfig } from './automation-config.js'; /** * File System Monitor for Task Management * Monitors file system changes and updates dashboard automatically * Enhanced with proactive task automation triggers */ export class FileSystemMonitor { constructor(taskStorage, dashboardBridge, automationConfig = null) { this.taskStorage = taskStorage; this.dashboardBridge = dashboardBridge; this.tasksDir = process.env.TASK_DIR || 'tasks'; this.watcher = null; this.isWatching = false; this.changeQueue = new Map(); // Debounce rapid changes this.changeTimeout = 500; // 500ms debounce // Automation components this.automationConfig = automationConfig || new AutomationConfig(); this.taskAutomation = new TaskAutomation(taskStorage); this.automationDebounce = new Map(); // Prevent automation loops } /** * Start monitoring the file system */ startMonitoring() { if (this.isWatching) { console.error('📡 File system monitor already running'); return; } if (!fs.existsSync(this.tasksDir)) { fs.mkdirSync(this.tasksDir, { recursive: true }); } this.watcher = chokidar.watch(`${this.tasksDir}/**/*.md`, { persistent: true, ignoreInitial: true, followSymlinks: false, awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 } }); // File added this.watcher.on('add', (filePath) => { this.debounceChange(filePath, 'add'); }); // File changed this.watcher.on('change', (filePath) => { this.debounceChange(filePath, 'change'); }); // File removed this.watcher.on('unlink', (filePath) => { this.debounceChange(filePath, 'unlink'); }); // Directory added this.watcher.on('addDir', (dirPath) => { console.error(`📁 Directory added: ${path.basename(dirPath)}`); this.broadcastDirectoryChange('add', dirPath); }); // Directory removed this.watcher.on('unlinkDir', (dirPath) => { console.error(`📁 Directory removed: ${path.basename(dirPath)}`); this.broadcastDirectoryChange('remove', dirPath); }); // Error handling this.watcher.on('error', (error) => { console.error('📡 File system monitor error:', error); }); this.isWatching = true; console.error('📡 File system monitor started for tasks directory'); } /** * Stop monitoring */ stopMonitoring() { if (this.watcher) { this.watcher.close(); this.watcher = null; } this.isWatching = false; console.error('📡 File system monitor stopped'); } /** * Debounce rapid file changes */ debounceChange(filePath, changeType) { const key = `${filePath}:${changeType}`; // Clear existing timeout if (this.changeQueue.has(key)) { clearTimeout(this.changeQueue.get(key)); } // Set new timeout const timeout = setTimeout(() => { this.handleFileChange(filePath, changeType); this.changeQueue.delete(key); }, this.changeTimeout); this.changeQueue.set(key, timeout); } /** * Handle file system changes */ async handleFileChange(filePath, changeType) { const relativePath = path.relative(this.tasksDir, filePath); const fileName = path.basename(filePath); console.error(`📄 File ${changeType}: ${fileName}`); try { switch (changeType) { case 'add': await this.handleFileAdded(filePath); break; case 'change': await this.handleFileChanged(filePath); break; case 'unlink': await this.handleFileRemoved(filePath); break; } } catch (error) { console.error(`❌ Error handling ${changeType} for ${fileName}:`, error); } } /** * Handle file added */ async handleFileAdded(filePath) { try { // Reload task index to include new file this.taskStorage.reload(); // Get the newly added task const tasks = this.taskStorage.getAllTasks(); const newTask = tasks.find(task => task.filepath === filePath); if (newTask) { console.error(`✅ Task added: ${newTask.title}`); // Broadcast to dashboard clients this.broadcastTaskChange('taskAdded', newTask); } } catch (error) { console.error('Error handling file added:', error); } } /** * Handle file changed */ async handleFileChanged(filePath) { try { // Reload task index to get updated data this.taskStorage.reload(); // Get the updated task const tasks = this.taskStorage.getAllTasks(); const updatedTask = tasks.find(task => task.filepath === filePath); if (updatedTask) { console.error(`✅ Task updated: ${updatedTask.title}`); // Broadcast to dashboard clients this.broadcastTaskChange('taskUpdated', updatedTask); // Trigger automation check if enabled if (this.automationConfig.get('enableFileChangeAutomation')) { await this.triggerAutomationCheck(updatedTask, 'fileChange'); } } } catch (error) { console.error('Error handling file changed:', error); } } /** * Handle file removed */ async handleFileRemoved(filePath) { try { // Extract task ID from file path const fileName = path.basename(filePath, '.md'); const taskId = fileName; // Assuming filename is the task ID console.error(`✅ Task removed: ${taskId}`); // Reload task index to remove deleted task this.taskStorage.reload(); // Broadcast to dashboard clients this.broadcastTaskChange('taskDeleted', { id: taskId, filepath: filePath }); } catch (error) { console.error('Error handling file removed:', error); } } /** * Broadcast task changes to dashboard clients */ broadcastTaskChange(eventType, task) { if (this.dashboardBridge && this.dashboardBridge.broadcastToClients) { this.dashboardBridge.broadcastToClients({ type: eventType, task: task, timestamp: new Date().toISOString() }); } } /** * Broadcast directory changes to dashboard clients */ broadcastDirectoryChange(changeType, dirPath) { const project = path.basename(dirPath); if (this.dashboardBridge && this.dashboardBridge.broadcastToClients) { this.dashboardBridge.broadcastToClients({ type: 'projectChange', changeType: changeType, project: project, timestamp: new Date().toISOString() }); } } /** * Get monitoring status */ getStatus() { return { isWatching: this.isWatching, watchedPath: this.tasksDir, queuedChanges: this.changeQueue.size, uptime: this.isWatching ? Date.now() - this.startTime : 0 }; } /** * Force reload of all tasks */ async forceReload() { console.error('🔄 Force reloading all tasks...'); try { this.taskStorage.reload(); // Broadcast full reload to clients if (this.dashboardBridge && this.dashboardBridge.broadcastToClients) { this.dashboardBridge.broadcastToClients({ type: 'tasksReloaded', timestamp: new Date().toISOString() }); } console.error('✅ Tasks reloaded successfully'); } catch (error) { console.error('❌ Error during force reload:', error); } } /** * Get file system statistics */ async getFileSystemStats() { const stats = { totalFiles: 0, projects: [], lastModified: null }; if (!fs.existsSync(this.tasksDir)) { return stats; } const projects = fs.readdirSync(this.tasksDir, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => entry.name); for (const project of projects) { const projectPath = path.join(this.tasksDir, project); const statuses = fs.readdirSync(projectPath, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => entry.name); let projectFileCount = 0; let projectLastModified = null; for (const status of statuses) { const statusPath = path.join(projectPath, status); const files = fs.readdirSync(statusPath) .filter(file => file.endsWith('.md')); projectFileCount += files.length; // Get last modified time for (const file of files) { const filePath = path.join(statusPath, file); const stat = fs.statSync(filePath); if (!projectLastModified || stat.mtime > projectLastModified) { projectLastModified = stat.mtime; } } } stats.projects.push({ name: project, fileCount: projectFileCount, statuses: statuses, lastModified: projectLastModified }); stats.totalFiles += projectFileCount; if (!stats.lastModified || (projectLastModified && projectLastModified > stats.lastModified)) { stats.lastModified = projectLastModified; } } return stats; } /** * Trigger automation check for a task * Safely checks and applies automation suggestions */ async triggerAutomationCheck(task, triggerType = 'manual') { // Check if automation is enabled for this project if (!this.automationConfig.isProjectEnabled(task.project)) { return; } // Check rate limiting if (!this.automationConfig.checkRateLimit()) { console.error('⚠️ Automation rate limit reached, skipping automation check'); return; } // Debounce to prevent automation loops const debounceKey = `${task.id}-${triggerType}`; if (this.automationDebounce.has(debounceKey)) { const lastCheck = this.automationDebounce.get(debounceKey); const timeSinceLastCheck = Date.now() - lastCheck; const debounceTime = this.automationConfig.get('fileChangeDebounce'); if (timeSinceLastCheck < debounceTime) { return; // Skip this check } } this.automationDebounce.set(debounceKey, Date.now()); try { console.error(`🤖 Checking automation opportunities for task: ${task.title}`); // Get automation suggestions const suggestions = await this.taskAutomation.getAutomationSuggestions(task.id); if (!suggestions || !suggestions.hasOpportunities) { return; } // Apply automations based on confidence and settings await this.applyAutomationSuggestions(task, suggestions, triggerType); } catch (error) { console.error('❌ Error in automation check:', error); } } /** * Apply automation suggestions based on configuration */ async applyAutomationSuggestions(task, suggestions, triggerType) { const confidenceThreshold = this.automationConfig.get('confidenceThreshold'); const requireHighConfidence = this.automationConfig.get('requireHighConfidence'); const dryRunMode = this.automationConfig.get('dryRunMode'); // Process status change suggestions if (suggestions.statusChange && suggestions.statusChange.confidence >= confidenceThreshold) { const { suggestedStatus, confidence, reason } = suggestions.statusChange; // Special handling for file change triggers if (triggerType === 'fileChange' && this.automationConfig.get('autoProgressOnFileChange') && task.status === 'todo' && suggestedStatus === 'in_progress') { console.error(`🤖 Auto-marking task as in_progress due to file change`); if (!dryRunMode) { await this.applyStatusChange(task, suggestedStatus, reason); } else { console.error(`🏃 DRY RUN: Would change status from ${task.status} to ${suggestedStatus}`); } } else if (!requireHighConfidence || confidence >= 0.85) { console.error(`🤖 Suggesting status change: ${task.status} → ${suggestedStatus} (${Math.round(confidence * 100)}% confidence)`); if (!dryRunMode) { await this.applyStatusChange(task, suggestedStatus, reason); } else { console.error(`🏃 DRY RUN: Would change status from ${task.status} to ${suggestedStatus}`); } } } // Process other automation opportunities if (suggestions.opportunities) { for (const [type, opportunity] of Object.entries(suggestions.opportunities)) { if (opportunity.shouldApply && opportunity.confidence >= confidenceThreshold) { console.error(`🤖 Automation opportunity: ${type} (${Math.round(opportunity.confidence * 100)}% confidence)`); if (!dryRunMode && this.shouldApplyAutomationType(type)) { // Apply specific automation based on type await this.applyAutomationType(task, type, opportunity); } } } } } /** * Apply a status change to a task */ async applyStatusChange(task, newStatus, reason) { try { // Update task status await this.taskStorage.updateTask(task.id, { status: newStatus }); // Record automation this.automationConfig.recordAutomation('statusChange', task.id, { oldStatus: task.status, newStatus: newStatus, reason: reason }); // Broadcast automation event this.broadcastAutomationEvent('statusChanged', { taskId: task.id, taskTitle: task.title, oldStatus: task.status, newStatus: newStatus, reason: reason }); console.error(`✅ Task status updated: ${task.status} → ${newStatus}`); } catch (error) { console.error('❌ Error applying status change:', error); } } /** * Check if a specific automation type should be applied */ shouldApplyAutomationType(type) { const typeSettings = { 'subtask_completion': this.automationConfig.get('autoCompleteSubtasks'), 'memory_evidence': true, // Always allow memory-based automation 'dependency_resolution': true, 'stale_detection': this.automationConfig.get('autoDetectStale'), 'blocked_detection': this.automationConfig.get('autoDetectBlocked') }; return typeSettings[type] !== false; } /** * Apply a specific type of automation */ async applyAutomationType(task, type, opportunity) { // Implementation depends on automation type console.error(`🤖 Applying ${type} automation for task ${task.id}`); this.automationConfig.recordAutomation(type, task.id, opportunity); this.broadcastAutomationEvent('automationApplied', { taskId: task.id, taskTitle: task.title, type: type, details: opportunity }); } /** * Broadcast automation events to dashboard */ broadcastAutomationEvent(eventType, data) { if (this.dashboardBridge && this.dashboardBridge.broadcastToClients && this.automationConfig.get('notifyOnAutomation')) { this.dashboardBridge.broadcastToClients({ type: 'automation', event: eventType, data: data, timestamp: new Date().toISOString() }); } } } export default FileSystemMonitor;