"""File-finding tool — structured glob, a clean alternative to `find` via bash. Read-only and workspace-jailed. Results sorted by modification time (most recent first), matching how coding agents typically surface "relevant" files. """ from pathlib import Path from cli_textual.tools.base import ToolResult from cli_textual.tools.write_file import _resolve_in_workspace MAX_CHARS = 8192 _SKIP_DIRS = {".git", "__pycache__", "node_modules", ".venv", ".mypy_cache", ".pytest_cache"} async def glob( pattern: str, path: str = ".", max_results: int = 100, workspace_root: Path | None = None, ) -> ToolResult: """Find files matching a glob pattern (supports ``**`` for recursion). Returns workspace-relative paths, newest first. Path is jailed to the workspace. Args: pattern: Glob pattern, e.g. ``**/*.py`` or ``src/**/test_*.py``. path: Directory to search under (relative to workspace or absolute). max_results: Cap on number of paths returned (default 100). """ try: workspace, base = _resolve_in_workspace(path, workspace_root) except PermissionError as exc: return ToolResult(output=f"Error: {exc}", is_error=True) if not base.exists(): return ToolResult(output=f"Error: path not found: {path}", is_error=True) hits = [] for p in base.glob(pattern): if not p.is_file(): continue rp = p.resolve() # Glob patterns may contain `..`, which pathlib treats as a literal # segment — re-jail every match so a pattern can't escape the workspace. if rp != workspace and workspace not in rp.parents: continue if any(part in _SKIP_DIRS for part in rp.relative_to(workspace).parts): continue hits.append(rp) try: hits.sort(key=lambda p: p.stat().st_mtime, reverse=True) except OSError: hits.sort() if not hits: return ToolResult(output=f"No files matching {pattern!r}") rels = [str(p.relative_to(workspace)) for p in hits[:max_results]] out = "\n".join(rels) truncated = "" if len(out) > MAX_CHARS: out = out[:MAX_CHARS] truncated = "\n[truncated]" note = f"\n[showing {max_results} of {len(hits)}]" if len(hits) > max_results else "" return ToolResult(output=out + truncated + note)