UNPKG

@openguardrails/moltguard

Version:

AI agent security plugin for OpenClaw: prompt injection detection, PII sanitization, and monitoring dashboard

137 lines 4.39 kB
/** * File System Watcher for Auto-Scanning * * Monitors workspace .md files for changes and triggers automatic security scans. * Debounces rapid changes to avoid excessive scanning. */ import fs from "node:fs"; import path from "node:path"; import os from "node:os"; export class FileWatcher { watchers = []; pendingFiles = new Set(); debounceTimer = null; config; isRunning = false; constructor(config) { this.config = { workspaceDir: config.workspaceDir || path.join(os.homedir(), ".openclaw"), debounceMs: config.debounceMs ?? 3000, onFilesChanged: config.onFilesChanged, logger: config.logger, }; } /** * Start watching workspace directories */ start() { if (this.isRunning) return; this.isRunning = true; const watchPaths = [ this.config.workspaceDir, // Root (soul.md, agent.md, heartbeat.md) path.join(this.config.workspaceDir, "memories"), path.join(this.config.workspaceDir, "skills"), path.join(this.config.workspaceDir, "plugins"), ]; for (const watchPath of watchPaths) { try { if (!fs.existsSync(watchPath)) continue; const watcher = fs.watch(watchPath, { recursive: true }, (eventType, filename) => { if (!filename) return; // Only watch .md files if (!filename.endsWith(".md")) return; // Ignore node_modules and hidden directories if (filename.includes("node_modules") || filename.includes("/.")) return; const fullPath = path.join(watchPath, filename); this.scheduleScann(fullPath); }); watcher.unref(); this.watchers.push(watcher); this.config.logger?.debug?.(`Watching: ${watchPath}`); } catch (err) { this.config.logger?.debug?.(`Failed to watch ${watchPath}: ${err}`); } } if (this.watchers.length > 0) { this.config.logger?.info(`File watcher started (${this.watchers.length} directories)`); } } /** * Stop watching */ stop() { if (!this.isRunning) return; for (const watcher of this.watchers) { watcher.close(); } this.watchers = []; if (this.debounceTimer) { clearTimeout(this.debounceTimer); this.debounceTimer = null; } this.pendingFiles.clear(); this.isRunning = false; this.config.logger?.info("File watcher stopped"); } /** * Schedule a file for scanning (debounced) */ scheduleScann(filePath) { this.pendingFiles.add(filePath); if (this.debounceTimer) { clearTimeout(this.debounceTimer); } this.debounceTimer = setTimeout(() => { this.processPendingScans(); }, this.config.debounceMs); this.debounceTimer.unref(); } /** * Process all pending scans */ async processPendingScans() { if (this.pendingFiles.size === 0) return; const files = Array.from(this.pendingFiles); this.pendingFiles.clear(); this.debounceTimer = null; // Filter to only existing files const existingFiles = files.filter(f => { try { return fs.existsSync(f) && fs.statSync(f).isFile(); } catch { return false; } }); if (existingFiles.length === 0) return; this.config.logger?.debug?.(`Auto-scanning ${existingFiles.length} changed file(s)...`); try { await this.config.onFilesChanged(existingFiles); } catch (err) { this.config.logger?.debug?.(`Auto-scan failed: ${err}`); } } /** * Check if watcher is running */ get running() { return this.isRunning; } /** * Get number of watched directories */ get watchCount() { return this.watchers.length; } } //# sourceMappingURL=file-watcher.js.map