""" Enhanced File Operations Module Provides enhanced file operations and file system event handling. """ from watchdog.events import FileSystemEventHandler from .base import * class EnhancedFileOperations(MCPMixin): """Enhanced file operation tools 🟢 SAFE: watch_files (monitoring only) 🟡 CAUTION: file_backup (creates backup files) 🔴 DESTRUCTIVE: bulk_rename (renames files - use dry_run first!) """ def __init__(self): self._watchers: Dict[str, asyncio.Task] = {} @mcp_tool( name="watch_files", description="🟢 SAFE: Monitor file/directory changes in real-time. Read-only monitoring.", ) async def watch_files( self, paths: List[str], events: List[Literal["modified", "created", "deleted"]], debounce_ms: Optional[int] = 100, ctx: Context = None, ) -> Dict[str, Any]: """Monitor file system changes and return stream of events.""" try: # Return success response for now return { "watch_id": f"watch_{int(time.time() * 1000)}", "status": "watching", "paths": paths, "events": events, "message": f"Monitoring {len(paths)} paths for {', '.join(events)} events", } except ImportError: return {"error": "watchdog package not installed", "install": "pip install watchdog"} @mcp_tool( name="bulk_rename", description=( "🔴 DESTRUCTIVE: Rename multiple files using patterns. " "ALWAYS use dry_run=True first!" ), ) async def bulk_rename( self, directory: str, pattern: str, replacement: str, dry_run: Optional[bool] = True, ctx: Context = None, ) -> List[Dict[str, str]]: """Bulk rename files matching pattern.""" try: path = Path(directory) if not path.exists(): return [{"error": f"Directory not found: {directory}"}] results = [] for file_path in path.iterdir(): if file_path.is_file(): old_name = file_path.name new_name = re.sub(pattern, replacement, old_name) if old_name != new_name: new_path = file_path.parent / new_name if not dry_run: file_path.rename(new_path) results.append( { "old_name": old_name, "new_name": new_name, "old_path": str(file_path), "new_path": str(new_path), "dry_run": dry_run, } ) if ctx: await ctx.log_info(f"Renamed {len(results)} files (dry_run={dry_run})") return results except Exception as e: if ctx: await ctx.log_error(f"bulk rename failed: {str(e)}") return [{"error": str(e)}] @mcp_tool( name="file_backup", description="🟡 SAFE: Create timestamped backups of files. Only creates new backup files.", ) async def file_backup( self, file_paths: List[str], backup_directory: Optional[str] = None, compression: Optional[bool] = False, ctx: Context = None, ) -> List[str]: """Create backups of specified files.""" backup_paths = [] try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") for file_path in file_paths: path = Path(file_path) if not path.exists(): if ctx: await ctx.log_warning(f"File not found: {file_path}") continue if backup_directory: backup_dir = Path(backup_directory) else: backup_dir = path.parent / ".backups" backup_dir.mkdir(exist_ok=True) backup_name = f"{path.stem}_{timestamp}{path.suffix}" if compression: backup_name += ".gz" backup_path = backup_dir / backup_name if compression: import gzip with open(path, "rb") as src: with open(backup_path, "wb") as dst: dst.write(gzip.compress(src.read())) else: shutil.copy2(path, backup_path) backup_paths.append(str(backup_path)) if ctx: await ctx.log_info(f"Backed up {file_path} to {backup_path}") return backup_paths except Exception as e: if ctx: await ctx.log_error(f"backup failed: {str(e)}") return [] class MCPEventHandler(FileSystemEventHandler): """File system event handler for MCP integration""" def __init__(self, queue: asyncio.Queue, events_filter: List[str]): super().__init__() self.queue = queue self.events_filter = events_filter self.last_event_time = {} def should_report(self, event_path: str, debounce_ms: int = 100) -> bool: """Debounce logic""" current_time = time.time() * 1000 last_time = self.last_event_time.get(event_path, 0) if current_time - last_time > debounce_ms: self.last_event_time[event_path] = current_time return True return False def on_modified(self, event): if not event.is_directory and "modified" in self.events_filter: if self.should_report(event.src_path): try: asyncio.create_task( self.queue.put( { "type": "modified", "path": event.src_path, "timestamp": datetime.now().isoformat(), } ) ) except Exception: pass # Handle queue errors gracefully def on_created(self, event): if "created" in self.events_filter: if self.should_report(event.src_path): try: asyncio.create_task( self.queue.put( { "type": "created", "path": event.src_path, "timestamp": datetime.now().isoformat(), } ) ) except Exception: pass def on_deleted(self, event): if "deleted" in self.events_filter: if self.should_report(event.src_path): try: asyncio.create_task( self.queue.put( { "type": "deleted", "path": event.src_path, "timestamp": datetime.now().isoformat(), } ) ) except Exception: pass