Yazhiniyan's picture
Upload 190 files
5ef3875 verified
Raw History Blame Contribute Delete
5.42 kB
"""Search tools: search_files (content search) and find_files (glob)."""
from __future__ import annotations
import fnmatch
import re
from pathlib import Path
from typing import Any
from ycode.errors import ToolError
from ycode.tools.base import Tool, ToolContext, ToolResult
from ycode.tools.filesystem import iter_files
MAX_FILE_BYTES = 1_000_000
def _matches_glob(rel: str, pattern: str | None) -> bool:
if not pattern:
return True
name = rel.rsplit("/", 1)[-1]
patterns = [p.strip() for p in pattern.split(",") if p.strip()]
return any(
fnmatch.fnmatch(rel, p) or fnmatch.fnmatch(name, p) or fnmatch.fnmatch(rel, f"**/{p}") for p in patterns
)
class SearchFilesTool(Tool):
name = "search_files"
description = (
"Search file contents with a regular expression (Python syntax). Returns matching "
"lines as path:line: text. Use `glob` (e.g. '*.py' or 'src/**/*.ts') to narrow files."
)
parameters = {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Regular expression to search for."},
"path": {"type": "string", "description": "Directory to search (default project root)."},
"glob": {"type": "string", "description": "Filename filter, comma-separated globs."},
"case_sensitive": {"type": "boolean", "description": "Default false."},
"max_results": {"type": "integer", "description": "Default 100."},
},
"required": ["pattern"],
}
def describe(self, args: dict[str, Any]) -> str:
where = f" in {args['glob']}" if args.get("glob") else ""
return f"Searching for /{args.get('pattern', '')}/{where}"
def run(self, args: dict[str, Any], ctx: ToolContext) -> ToolResult:
root = ctx.permissions.paths.resolve(args.get("path") or ".")
if not root.exists():
raise ToolError(f"Path not found: {args.get('path')}")
flags = 0 if args.get("case_sensitive") else re.IGNORECASE
try:
regex = re.compile(args["pattern"], flags)
except re.error as exc:
raise ToolError(f"invalid regular expression: {exc}") from None
limit = min(max(int(args.get("max_results") or 100), 1), 1000)
files = [root] if root.is_file() else iter_files(root, ctx.ignore_dirs)
results: list[str] = []
files_matched = 0
for file in files:
rel = ctx.permissions.paths.relative(file)
if not _matches_glob(rel, args.get("glob")):
continue
try:
if file.stat().st_size > MAX_FILE_BYTES:
continue
data = file.read_bytes()
except OSError:
continue
if b"\x00" in data[:8192]:
continue
hit = False
for lineno, line in enumerate(data.decode("utf-8", errors="replace").splitlines(), 1):
if regex.search(line):
hit = True
results.append(f"{rel}:{lineno}: {line.strip()[:300]}")
if len(results) >= limit:
break
files_matched += hit
if len(results) >= limit:
break
if not results:
return ToolResult(content="No matches found.", summary="No matches")
suffix = f"\n[Stopped after {limit} results; refine the pattern.]" if len(results) >= limit else ""
return ToolResult(
content="\n".join(results) + suffix,
summary=f"{len(results)} matches in {files_matched} files",
)
class FindFilesTool(Tool):
name = "find_files"
description = "Find files by name/glob pattern (e.g. '*.test.ts', 'package.json', 'src/**/auth*')."
parameters = {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Glob pattern (comma-separated for several)."},
"path": {"type": "string", "description": "Directory to search (default project root)."},
"max_results": {"type": "integer", "description": "Default 200."},
},
"required": ["pattern"],
}
def describe(self, args: dict[str, Any]) -> str:
return f"Finding files matching {args.get('pattern', '')}"
def run(self, args: dict[str, Any], ctx: ToolContext) -> ToolResult:
root = ctx.permissions.paths.resolve(args.get("path") or ".")
if not root.is_dir():
raise ToolError(f"Directory not found: {args.get('path')}")
limit = min(max(int(args.get("max_results") or 200), 1), 2000)
found: list[str] = []
for file in iter_files(root, ctx.ignore_dirs):
rel = ctx.permissions.paths.relative(file)
if _matches_glob(rel, args["pattern"]):
found.append(rel)
if len(found) >= limit:
break
if not found:
return ToolResult(content="No files found.", summary="No files found")
return ToolResult(content="\n".join(found), summary=f"Found {len(found)} files")
def list_project_files(root: Path, ignore_dirs: tuple[str, ...], limit: int = 2000) -> list[str]:
out: list[str] = []
for file in iter_files(root, ignore_dirs):
out.append(file.relative_to(root).as_posix())
if len(out) >= limit:
break
return out