96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""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}'"
|