Spaces:
Sleeping
Sleeping
yc1838 Claude Opus 4.7 commited on
Commit ·
0dc5c75
1
Parent(s): 96fa5bb
fix: add default excludes and truncation sentinel to find_files
Browse filesAddresses code-review findings I2 and I3:
- I2: skip node_modules/.git/.venv/__pycache__/etc. so find_files
returns useful results on real codebases
- I3: signal truncation when result count exceeds max_results
- M3: drop unused os import in tests
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- src/lilith_agent/tools/files.py +26 -2
- tests/test_files.py +28 -1
src/lilith_agent/tools/files.py
CHANGED
|
@@ -15,6 +15,21 @@ from pypdf import PdfReader
|
|
| 15 |
_WRITE_ROOT: Path = Path(".lilith/scratch").resolve()
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def set_write_root(root: str | Path) -> None:
|
| 19 |
"""Set the sandboxed root directory for write_file. Call once at startup."""
|
| 20 |
global _WRITE_ROOT
|
|
@@ -166,9 +181,12 @@ def find_files(name: str, root: str = ".", max_results: int = 200) -> str:
|
|
| 166 |
root = os.path.expanduser(root)
|
| 167 |
if not Path(root).exists():
|
| 168 |
return f"ERROR: Path {root} does not exist."
|
|
|
|
|
|
|
|
|
|
| 169 |
try:
|
| 170 |
result = subprocess.run(
|
| 171 |
-
|
| 172 |
capture_output=True,
|
| 173 |
text=True,
|
| 174 |
timeout=30,
|
|
@@ -176,7 +194,13 @@ def find_files(name: str, root: str = ".", max_results: int = 200) -> str:
|
|
| 176 |
lines = [ln for ln in result.stdout.splitlines() if ln]
|
| 177 |
if not lines:
|
| 178 |
return "No files found."
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
except subprocess.TimeoutExpired:
|
| 181 |
return "ERROR: find timed out (>30s)."
|
| 182 |
except Exception as e:
|
|
|
|
| 15 |
_WRITE_ROOT: Path = Path(".lilith/scratch").resolve()
|
| 16 |
|
| 17 |
|
| 18 |
+
# Default directories pruned by find_files. Keeps results useful on real
|
| 19 |
+
# codebases by skipping vendored/build/cache trees.
|
| 20 |
+
_DEFAULT_EXCLUDES: tuple[str, ...] = (
|
| 21 |
+
"node_modules",
|
| 22 |
+
".git",
|
| 23 |
+
".venv",
|
| 24 |
+
"__pycache__",
|
| 25 |
+
".pytest_cache",
|
| 26 |
+
".mypy_cache",
|
| 27 |
+
"dist",
|
| 28 |
+
"build",
|
| 29 |
+
".tox",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
def set_write_root(root: str | Path) -> None:
|
| 34 |
"""Set the sandboxed root directory for write_file. Call once at startup."""
|
| 35 |
global _WRITE_ROOT
|
|
|
|
| 181 |
root = os.path.expanduser(root)
|
| 182 |
if not Path(root).exists():
|
| 183 |
return f"ERROR: Path {root} does not exist."
|
| 184 |
+
argv = ["find", str(root), "-name", name]
|
| 185 |
+
for excl in _DEFAULT_EXCLUDES:
|
| 186 |
+
argv.extend(["-not", "-path", f"*/{excl}/*"])
|
| 187 |
try:
|
| 188 |
result = subprocess.run(
|
| 189 |
+
argv,
|
| 190 |
capture_output=True,
|
| 191 |
text=True,
|
| 192 |
timeout=30,
|
|
|
|
| 194 |
lines = [ln for ln in result.stdout.splitlines() if ln]
|
| 195 |
if not lines:
|
| 196 |
return "No files found."
|
| 197 |
+
if len(lines) > max_results:
|
| 198 |
+
shown = lines[:max_results]
|
| 199 |
+
shown.append(
|
| 200 |
+
f"[truncated: showing first {max_results} of {len(lines)} matches]"
|
| 201 |
+
)
|
| 202 |
+
return "\n".join(shown)
|
| 203 |
+
return "\n".join(lines)
|
| 204 |
except subprocess.TimeoutExpired:
|
| 205 |
return "ERROR: find timed out (>30s)."
|
| 206 |
except Exception as e:
|
tests/test_files.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
import os
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
import pytest
|
|
@@ -122,3 +121,31 @@ def test_find_files_expands_tilde(monkeypatch, tmp_path: Path):
|
|
| 122 |
def test_find_files_missing_root_errors(tmp_path: Path):
|
| 123 |
out = find_files("anything", str(tmp_path / "does_not_exist"))
|
| 124 |
assert out.startswith("ERROR")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
import pytest
|
|
|
|
| 121 |
def test_find_files_missing_root_errors(tmp_path: Path):
|
| 122 |
out = find_files("anything", str(tmp_path / "does_not_exist"))
|
| 123 |
assert out.startswith("ERROR")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def test_find_files_excludes_node_modules(tmp_path: Path):
|
| 127 |
+
(tmp_path / "node_modules").mkdir()
|
| 128 |
+
(tmp_path / "node_modules" / "skip.txt").write_text("")
|
| 129 |
+
(tmp_path / "src").mkdir()
|
| 130 |
+
(tmp_path / "src" / "keep.txt").write_text("")
|
| 131 |
+
out = find_files("*.txt", str(tmp_path))
|
| 132 |
+
assert "keep.txt" in out
|
| 133 |
+
assert "skip.txt" not in out
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def test_find_files_excludes_dot_git(tmp_path: Path):
|
| 137 |
+
(tmp_path / ".git").mkdir()
|
| 138 |
+
(tmp_path / ".git" / "config").write_text("")
|
| 139 |
+
(tmp_path / "src").mkdir()
|
| 140 |
+
(tmp_path / "src" / "config").write_text("")
|
| 141 |
+
out = find_files("config", str(tmp_path))
|
| 142 |
+
assert str(tmp_path / "src" / "config") in out
|
| 143 |
+
assert str(tmp_path / ".git" / "config") not in out
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def test_find_files_truncation_sentinel(tmp_path: Path):
|
| 147 |
+
for i in range(4):
|
| 148 |
+
(tmp_path / f"file{i}.txt").write_text("")
|
| 149 |
+
out = find_files("*.txt", str(tmp_path), max_results=3)
|
| 150 |
+
assert "[truncated:" in out
|
| 151 |
+
assert "of 4" in out
|