Skip to main content
Glama
file_watcher.js14.3 kB
const fs = require('fs'); const path = require('path'); const logger = require('./logger'); const tableRenderer = require('./tableRenderer'); const core = require('./core'); /** * File watcher for tasks.json to automatically update task table and files */ class TaskFileWatcher { constructor(workspaceRoot, options = {}) { this.workspaceRoot = workspaceRoot; this.tasksFilePath = path.resolve(workspaceRoot, '.acf', 'tasks.json'); this.watcher = null; this.debounceTimer = null; this.debounceDelay = options.debounceDelay || 500; // 500ms debounce this.isWatching = false; this.lastModified = null; // Enhanced features this.changeQueue = []; this.isProcessing = false; this.maxQueueSize = options.maxQueueSize || 10; this.enableTaskFiles = options.enableTaskFiles !== false; // Default true this.enableTableSync = options.enableTableSync !== false; // Default true this.enablePriorityRecalc = options.enablePriorityRecalc || false; // Default false this.changeHistory = []; this.maxHistorySize = options.maxHistorySize || 50; this.retryAttempts = options.retryAttempts || 3; this.retryDelay = options.retryDelay || 1000; // 1 second // Statistics this.stats = { changesDetected: 0, changesProcessed: 0, errors: 0, lastProcessed: null, averageProcessingTime: 0 }; } /** * Start watching the tasks.json file for changes */ start() { if (this.isWatching) { logger.debug('File watcher is already running'); return; } if (!fs.existsSync(this.tasksFilePath)) { logger.debug(`Tasks file not found: ${this.tasksFilePath}. Watcher will start when file is created.`); return; } try { // Get initial modification time const stats = fs.statSync(this.tasksFilePath); this.lastModified = stats.mtime.getTime(); this.watcher = fs.watch(this.tasksFilePath, (eventType, filename) => { if (eventType === 'change') { this.handleFileChange(); } }); this.isWatching = true; logger.debug(`Started watching tasks.json at: ${this.tasksFilePath}`); } catch (error) { logger.error(`Failed to start file watcher: ${error.message}`); } } /** * Stop watching the tasks.json file */ stop() { if (this.watcher) { this.watcher.close(); this.watcher = null; } if (this.debounceTimer) { clearTimeout(this.debounceTimer); this.debounceTimer = null; } this.isWatching = false; logger.debug('Stopped watching tasks.json'); } /** * Handle file change events with advanced debouncing and queuing */ handleFileChange() { this.stats.changesDetected++; // Add change to queue const changeEvent = { timestamp: Date.now(), id: `change_${this.stats.changesDetected}` }; this.addToQueue(changeEvent); // Clear existing timer if (this.debounceTimer) { clearTimeout(this.debounceTimer); } // Set new timer with adaptive delay const adaptiveDelay = this.calculateAdaptiveDelay(); this.debounceTimer = setTimeout(() => { this.processQueue(); }, adaptiveDelay); } /** * Add change event to processing queue */ addToQueue(changeEvent) { // Prevent queue overflow if (this.changeQueue.length >= this.maxQueueSize) { logger.warn(`Change queue full (${this.maxQueueSize}), dropping oldest event`); this.changeQueue.shift(); } this.changeQueue.push(changeEvent); logger.debug(`Added change event to queue: ${changeEvent.id} (queue size: ${this.changeQueue.length})`); } /** * Calculate adaptive debounce delay based on recent activity */ calculateAdaptiveDelay() { const recentChanges = this.changeHistory.filter( change => Date.now() - change.timestamp < 10000 // Last 10 seconds ).length; // Increase delay if there's high activity if (recentChanges > 5) { return this.debounceDelay * 2; // Double delay for high activity } else if (recentChanges > 2) { return this.debounceDelay * 1.5; // 1.5x delay for moderate activity } return this.debounceDelay; // Normal delay } /** * Process the entire change queue */ async processQueue() { if (this.isProcessing || this.changeQueue.length === 0) { return; } this.isProcessing = true; const startTime = Date.now(); try { logger.debug(`Processing ${this.changeQueue.length} queued changes`); // Process all queued changes as a batch const changes = [...this.changeQueue]; this.changeQueue = []; await this.processFileChangeWithRetry(changes); // Update statistics this.stats.changesProcessed += changes.length; this.stats.lastProcessed = Date.now(); const processingTime = Date.now() - startTime; this.updateAverageProcessingTime(processingTime); logger.debug(`Processed ${changes.length} changes in ${processingTime}ms`); } catch (error) { this.stats.errors++; logger.error(`Error processing change queue: ${error.message}`); } finally { this.isProcessing = false; } } /** * Process file changes with retry logic and comprehensive updates */ async processFileChangeWithRetry(changes, attempt = 1) { try { // Check if file still exists if (!fs.existsSync(this.tasksFilePath)) { logger.debug('Tasks file was deleted, stopping watcher'); this.stop(); return; } // Check if file was actually modified (avoid duplicate events) const stats = fs.statSync(this.tasksFilePath); const currentModified = stats.mtime.getTime(); if (this.lastModified && currentModified <= this.lastModified) { logger.debug('No actual file modification detected, skipping processing'); return; } this.lastModified = currentModified; // Read and validate the tasks file const tasksData = JSON.parse(fs.readFileSync(this.tasksFilePath, 'utf8')); // Record change in history const changeRecord = { timestamp: Date.now(), changes: changes.length, fileSize: stats.size, taskCount: tasksData.tasks ? tasksData.tasks.length : 0 }; this.addToHistory(changeRecord); // Perform comprehensive synchronization await this.performComprehensiveSync(tasksData); logger.info(`Successfully processed ${changes.length} file changes`); } catch (error) { logger.error(`Error processing tasks.json change (attempt ${attempt}): ${error.message}`); // Retry logic if (attempt < this.retryAttempts) { logger.info(`Retrying in ${this.retryDelay}ms... (attempt ${attempt + 1}/${this.retryAttempts})`); setTimeout(() => { this.processFileChangeWithRetry(changes, attempt + 1); }, this.retryDelay); } else { logger.error(`Failed to process file changes after ${this.retryAttempts} attempts`); this.stats.errors++; } } } /** * Perform comprehensive synchronization of all task-related files */ async performComprehensiveSync(tasksData) { const syncResults = { taskTable: false, taskFiles: false, priorityRecalc: false }; try { // 1. Update task table (if enabled) if (this.enableTableSync) { syncResults.taskTable = tableRenderer.writeTaskTable(tasksData, this.workspaceRoot); if (syncResults.taskTable) { logger.debug('✅ Task table synchronized'); } else { logger.warn('❌ Failed to synchronize task table'); } } // 2. Generate individual task files (if enabled) if (this.enableTaskFiles) { try { const result = core.generateTaskFiles(this.workspaceRoot); syncResults.taskFiles = result.success; if (syncResults.taskFiles) { logger.debug('✅ Individual task files synchronized'); } else { logger.warn('❌ Failed to synchronize task files'); } } catch (error) { logger.warn(`Failed to generate task files: ${error.message}`); } } // 3. Recalculate priorities (if enabled) if (this.enablePriorityRecalc) { try { const result = core.recalculatePriorities(this.workspaceRoot, { applyDependencyBoosts: true, optimizeDistribution: true }); syncResults.priorityRecalc = result.success; if (syncResults.priorityRecalc) { logger.debug('✅ Priorities recalculated'); } else { logger.warn('❌ Failed to recalculate priorities'); } } catch (error) { logger.warn(`Failed to recalculate priorities: ${error.message}`); } } // Log sync summary const successCount = Object.values(syncResults).filter(Boolean).length; const totalCount = Object.values(syncResults).filter(v => v !== false).length; logger.debug(`Sync completed: ${successCount}/${totalCount} operations successful`); } catch (error) { logger.error(`Error during comprehensive sync: ${error.message}`); throw error; } return syncResults; } /** * Add change record to history */ addToHistory(changeRecord) { // Prevent history overflow if (this.changeHistory.length >= this.maxHistorySize) { this.changeHistory.shift(); } this.changeHistory.push(changeRecord); } /** * Update average processing time */ updateAverageProcessingTime(newTime) { if (this.stats.averageProcessingTime === 0) { this.stats.averageProcessingTime = newTime; } else { // Exponential moving average this.stats.averageProcessingTime = (this.stats.averageProcessingTime * 0.8) + (newTime * 0.2); } } /** * Force sync all task-related files with current tasks.json */ async forceSync() { if (!fs.existsSync(this.tasksFilePath)) { throw new Error('Tasks file does not exist'); } try { const tasksData = JSON.parse(fs.readFileSync(this.tasksFilePath, 'utf8')); await this.performComprehensiveSync(tasksData); logger.info('Force sync completed successfully'); } catch (error) { logger.error(`Force sync failed: ${error.message}`); throw error; } } /** * Check if the watcher is currently active */ isActive() { return this.isWatching; } /** * Get watcher statistics */ getStats() { return { ...this.stats, queueSize: this.changeQueue.length, historySize: this.changeHistory.length, isProcessing: this.isProcessing, isWatching: this.isWatching, uptime: this.isWatching ? Date.now() - (this.stats.lastProcessed || Date.now()) : 0 }; } /** * Get recent change history */ getChangeHistory(limit = 10) { return this.changeHistory.slice(-limit); } /** * Configure watcher options */ configure(options) { if (options.debounceDelay !== undefined) { this.debounceDelay = options.debounceDelay; } if (options.maxQueueSize !== undefined) { this.maxQueueSize = options.maxQueueSize; } if (options.enableTaskFiles !== undefined) { this.enableTaskFiles = options.enableTaskFiles; } if (options.enableTableSync !== undefined) { this.enableTableSync = options.enableTableSync; } if (options.enablePriorityRecalc !== undefined) { this.enablePriorityRecalc = options.enablePriorityRecalc; } if (options.retryAttempts !== undefined) { this.retryAttempts = options.retryAttempts; } if (options.retryDelay !== undefined) { this.retryDelay = options.retryDelay; } logger.info('File watcher configuration updated'); } /** * Reset statistics */ resetStats() { this.stats = { changesDetected: 0, changesProcessed: 0, errors: 0, lastProcessed: null, averageProcessingTime: 0 }; this.changeHistory = []; logger.info('File watcher statistics reset'); } /** * Get current configuration */ getConfig() { return { debounceDelay: this.debounceDelay, maxQueueSize: this.maxQueueSize, enableTaskFiles: this.enableTaskFiles, enableTableSync: this.enableTableSync, enablePriorityRecalc: this.enablePriorityRecalc, retryAttempts: this.retryAttempts, retryDelay: this.retryDelay, maxHistorySize: this.maxHistorySize }; } } /** * Global watcher instance */ let globalWatcher = null; /** * Initialize the file watcher for a workspace * @param {string} workspaceRoot - The workspace root path * @param {Object} options - Watcher configuration options * @returns {TaskFileWatcher} The watcher instance */ function initializeWatcher(workspaceRoot, options = {}) { if (globalWatcher) { globalWatcher.stop(); } globalWatcher = new TaskFileWatcher(workspaceRoot, options); globalWatcher.start(); return globalWatcher; } /** * Configure the global watcher * @param {Object} options - Configuration options */ function configureWatcher(options) { if (globalWatcher) { globalWatcher.configure(options); } else { logger.warn('No active watcher to configure'); } } /** * Get watcher statistics * @returns {Object|null} Watcher statistics or null if no watcher */ function getWatcherStats() { return globalWatcher ? globalWatcher.getStats() : null; } /** * Force sync all task files * @returns {Promise} Promise that resolves when sync is complete */ async function forceSyncAll() { if (globalWatcher) { return await globalWatcher.forceSync(); } else { throw new Error('No active watcher for force sync'); } } /** * Get the current global watcher instance * @returns {TaskFileWatcher|null} The current watcher or null */ function getWatcher() { return globalWatcher; } /** * Stop the global watcher */ function stopWatcher() { if (globalWatcher) { globalWatcher.stop(); globalWatcher = null; } } module.exports = { TaskFileWatcher, initializeWatcher, getWatcher, stopWatcher, configureWatcher, getWatcherStats, forceSyncAll };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/FutureAtoms/agentic-control-framework'

If you have feedback or need assistance with the MCP directory API, please join our Discord server