"""File operation tools: read, write, edit, search.""" from __future__ import annotations from pathlib import Path from . import tool @tool("read_file", "Read the contents of a file", category="files") def read_file(path: str) -> str: p = Path(path).resolve() if not p.exists(): return f"File not found: {path}" if not p.is_file(): return f"Not a file: {path}" try: content = p.read_text(encoding="utf-8", errors="replace") if len(content) > 50000: return content[:50000] + f"\n\n... (truncated, {len(content)} total chars)" return content except Exception as e: return f"Error reading file: {e}" @tool("write_file", "Write content to a file (creates or overwrites)", category="files") def write_file(path: str, content: str) -> str: p = Path(path).resolve() p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content, encoding="utf-8") return f"Written {len(content)} chars to {p}" @tool("edit_file", "Replace text in a file (first occurrence)", category="files") def edit_file(path: str, old_text: str, new_text: str) -> str: p = Path(path).resolve() if not p.exists(): return f"File not found: {path}" content = p.read_text(encoding="utf-8") if old_text not in content: return f"Text not found in {path}" content = content.replace(old_text, new_text, 1) p.write_text(content, encoding="utf-8") return f"Replaced text in {p}" @tool("list_directory", "List files and folders in a directory", category="files") def list_directory(path: str = ".") -> str: p = Path(path).resolve() if not p.is_dir(): return f"Not a directory: {path}" entries = sorted(p.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())) lines = [] for e in entries[:200]: prefix = "📁 " if e.is_dir() else "📄 " size = "" if e.is_file(): s = e.stat().st_size if s > 1_000_000: size = f" ({s / 1_000_000:.1f} MB)" elif s > 1000: size = f" ({s / 1000:.1f} KB)" else: size = f" ({s} B)" lines.append(f"{prefix}{e.name}{size}") return "\n".join(lines) if lines else "(empty directory)" @tool("search_files", "Search for files matching a glob pattern", category="files") def search_files(pattern: str, directory: str = ".") -> str: p = Path(directory).resolve() matches = list(p.glob(pattern))[:100] if not matches: return f"No files matching '{pattern}' in {directory}" return "\n".join(str(m) for m in matches) @tool("search_in_files", "Search for text content across files", category="files") def search_in_files(query: str, directory: str = ".", extension: str = "") -> str: p = Path(directory).resolve() pattern = f"**/*{extension}" if extension else "**/*" results = [] for f in p.glob(pattern): if not f.is_file() or f.stat().st_size > 1_000_000: continue try: content = f.read_text(encoding="utf-8", errors="ignore") for i, line in enumerate(content.split("\n"), 1): if query.lower() in line.lower(): results.append(f"{f}:{i}: {line.strip()[:200]}") if len(results) >= 50: return "\n".join(results) + "\n... (truncated)" except Exception: continue return "\n".join(results) if results else f"No matches for '{query}'"