codetool-explore 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codetool_explore-0.6.0/.gitignore +11 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/PKG-INFO +22 -19
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/README.md +21 -18
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/pyproject.toml +1 -1
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/scripts/update_readme_benchmarks.py +17 -37
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/api.py +7 -6
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/cli.py +2 -2
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/explorer.py +235 -62
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_api.py +121 -1
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_cli.py +57 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/uv.lock +1 -1
- codetool_explore-0.5.0/.gitignore +0 -19
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/LICENSE +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/benchmarks/benchmark_output_lengths.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/benchmarks/benchmark_search.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/hatch_build.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/Cargo.lock +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/Cargo.toml +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/app.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/case.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/config.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/constants.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/file_search.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/ignore_rules.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/literal.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/main.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/matcher.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/models.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/output.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/path_utils.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/ranking.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/regex_search.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/search.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/text.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/rust/src/walker.rs +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/scripts/package_rust_binary.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/__init__.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/compression.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/cursor.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/errors.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/ignore.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/py.typed +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/__init__.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/case.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/config.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/constants.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/file_search.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/ignore_rules.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/literal.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/matcher.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/models.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/output.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/regex_search.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/search.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/text.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/walker.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/ranking.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/roots.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/rust_backend.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/text_output.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test___init__.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_cursor.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_hatch_build.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_ignore.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_packaged_binary.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_python_backend.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_ranking.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_rust_backend.py +0 -0
- {codetool_explore-0.5.0 → codetool_explore-0.6.0}/tests/test_rust_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codetool-explore
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Fast, dependency-free workspace search, read, and list exploration for coding-agent tools with Rust backend
|
|
5
5
|
Project-URL: Homepage, https://github.com/pbi-agent/codetool-explore
|
|
6
6
|
Project-URL: Repository, https://github.com/pbi-agent/codetool-explore
|
|
@@ -109,16 +109,19 @@ path-only rows under `target="content_or_path"` are returned without
|
|
|
109
109
|
line/snippet fields.
|
|
110
110
|
|
|
111
111
|
`target="read"` treats `pattern` as one known file path, resolves relative paths
|
|
112
|
-
under
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
under each root, and returns plain text with no line-number prefixes. When more
|
|
113
|
+
than one file is read, each file is prefixed by a compact `path:` header. Use
|
|
114
|
+
`start_line` and `limit` to cap the returned line range; if more lines remain,
|
|
115
|
+
text output starts with `-- more: cursor=N`. CSV files are read as ordinary
|
|
116
|
+
text. Binary-looking, missing, unreadable, or directory paths fail with
|
|
116
117
|
controlled `ExploreError` subclasses.
|
|
117
118
|
|
|
118
119
|
`target="list"` treats `pattern` as one file/directory path and returns one
|
|
119
|
-
directory level. Text output uses the same compact tree display
|
|
120
|
-
output when that saves tokens
|
|
121
|
-
|
|
120
|
+
directory level under each root. Text output uses the same compact tree display
|
|
121
|
+
as raw search output when that saves tokens, appending each root's listing in
|
|
122
|
+
root order. Directories end with `/`; file paths are returned as one entry. It
|
|
123
|
+
honors multi-root common-base paths, `glob`, `exclude`, ignore files, `limit`,
|
|
124
|
+
and `cursor`.
|
|
122
125
|
Read/list use the pure-Python stdlib implementation even when `backend="auto"`
|
|
123
126
|
or `"rust"` is requested.
|
|
124
127
|
|
|
@@ -139,9 +142,10 @@ treated as multiple roots when that exact path does not exist and every split
|
|
|
139
142
|
token is an existing file or directory. Existing paths with spaces still take
|
|
140
143
|
priority; quote individual spaced paths if combining them in one string.
|
|
141
144
|
|
|
142
|
-
File roots search only that file and report paths relative to the file's
|
|
143
|
-
directory
|
|
144
|
-
|
|
145
|
+
File roots search/read only that file and report paths relative to the file's
|
|
146
|
+
parent directory; listing a file path returns one file entry. Multi-root
|
|
147
|
+
searches/reads/listings report paths relative to the roots' common base, so
|
|
148
|
+
sibling roots keep prefixes such as `src/...` and `tests/...`; this also lets
|
|
145
149
|
`exclude=["src/generated/**"]` target one root.
|
|
146
150
|
|
|
147
151
|
Controlled failures raise `ExploreError` subclasses:
|
|
@@ -159,15 +163,16 @@ codetool-explore "service" . --target path --literal
|
|
|
159
163
|
codetool-explore "User(Service|Repository)" --root src --mode snippets --raw
|
|
160
164
|
codetool-explore "search_workspace" --root src --root webapp --root tests --literal
|
|
161
165
|
codetool-explore --read README.md --start-line 20 --limit 40
|
|
162
|
-
codetool-explore --
|
|
166
|
+
codetool-explore --read settings.py --root src --root tests
|
|
167
|
+
codetool-explore --list . --root src --root tests --glob "*.py"
|
|
163
168
|
```
|
|
164
169
|
|
|
165
170
|
The CLI defaults to compact JSON for search, plain text for `--read`, and
|
|
166
171
|
tree-compressed text for `--list`.
|
|
167
172
|
Use `--format text` or `--raw` for raw search text; no search matches print
|
|
168
|
-
`No Match`. Repeat `--root` for multiple search
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
`No Match`. Repeat `--root` for multiple search/read/list roots. A single
|
|
174
|
+
quoted space-delimited `--root` is accepted as a compatibility fallback when it
|
|
175
|
+
can be split into existing roots.
|
|
171
176
|
|
|
172
177
|
## Install
|
|
173
178
|
|
|
@@ -217,13 +222,11 @@ Token counts use `tiktoken` when available. The table compares output across 7 R
|
|
|
217
222
|
|
|
218
223
|
| Output | Tokens | Bytes | Chart |
|
|
219
224
|
| --- | ---: | ---: | --- |
|
|
220
|
-
| `explore
|
|
225
|
+
| `codetool-explore` | 11,008 | 34.3 KB | ██░░░░░░░░░░░░░░░░ |
|
|
221
226
|
| `rtk grep` stdout | 19,646 | 60.1 KB | ███░░░░░░░░░░░░░░░ |
|
|
222
|
-
| default `explore(...)` | 38,393 | 125.3 KB | █████░░░░░░░░░░░░░ |
|
|
223
|
-
| `explore(..., result_format="full")` | 39,027 | 134.7 KB | █████░░░░░░░░░░░░░ |
|
|
224
227
|
| `rg` stdout | 129,775 | 402.4 KB | ██████████████████ |
|
|
225
228
|
|
|
226
|
-
|
|
229
|
+
`codetool-explore` is raw text from `explore(..., result_format="text")`; it omits backend/totals metadata, includes only a cursor hint when truncated, and prints `No Match` for empty pages. It is 0.56× the `rtk grep` token count in this run.
|
|
227
230
|
|
|
228
231
|
Source: `reports/rtk_vs_codetool_output_lengths.json`.
|
|
229
232
|
|
|
@@ -79,16 +79,19 @@ path-only rows under `target="content_or_path"` are returned without
|
|
|
79
79
|
line/snippet fields.
|
|
80
80
|
|
|
81
81
|
`target="read"` treats `pattern` as one known file path, resolves relative paths
|
|
82
|
-
under
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
under each root, and returns plain text with no line-number prefixes. When more
|
|
83
|
+
than one file is read, each file is prefixed by a compact `path:` header. Use
|
|
84
|
+
`start_line` and `limit` to cap the returned line range; if more lines remain,
|
|
85
|
+
text output starts with `-- more: cursor=N`. CSV files are read as ordinary
|
|
86
|
+
text. Binary-looking, missing, unreadable, or directory paths fail with
|
|
86
87
|
controlled `ExploreError` subclasses.
|
|
87
88
|
|
|
88
89
|
`target="list"` treats `pattern` as one file/directory path and returns one
|
|
89
|
-
directory level. Text output uses the same compact tree display
|
|
90
|
-
output when that saves tokens
|
|
91
|
-
|
|
90
|
+
directory level under each root. Text output uses the same compact tree display
|
|
91
|
+
as raw search output when that saves tokens, appending each root's listing in
|
|
92
|
+
root order. Directories end with `/`; file paths are returned as one entry. It
|
|
93
|
+
honors multi-root common-base paths, `glob`, `exclude`, ignore files, `limit`,
|
|
94
|
+
and `cursor`.
|
|
92
95
|
Read/list use the pure-Python stdlib implementation even when `backend="auto"`
|
|
93
96
|
or `"rust"` is requested.
|
|
94
97
|
|
|
@@ -109,9 +112,10 @@ treated as multiple roots when that exact path does not exist and every split
|
|
|
109
112
|
token is an existing file or directory. Existing paths with spaces still take
|
|
110
113
|
priority; quote individual spaced paths if combining them in one string.
|
|
111
114
|
|
|
112
|
-
File roots search only that file and report paths relative to the file's
|
|
113
|
-
directory
|
|
114
|
-
|
|
115
|
+
File roots search/read only that file and report paths relative to the file's
|
|
116
|
+
parent directory; listing a file path returns one file entry. Multi-root
|
|
117
|
+
searches/reads/listings report paths relative to the roots' common base, so
|
|
118
|
+
sibling roots keep prefixes such as `src/...` and `tests/...`; this also lets
|
|
115
119
|
`exclude=["src/generated/**"]` target one root.
|
|
116
120
|
|
|
117
121
|
Controlled failures raise `ExploreError` subclasses:
|
|
@@ -129,15 +133,16 @@ codetool-explore "service" . --target path --literal
|
|
|
129
133
|
codetool-explore "User(Service|Repository)" --root src --mode snippets --raw
|
|
130
134
|
codetool-explore "search_workspace" --root src --root webapp --root tests --literal
|
|
131
135
|
codetool-explore --read README.md --start-line 20 --limit 40
|
|
132
|
-
codetool-explore --
|
|
136
|
+
codetool-explore --read settings.py --root src --root tests
|
|
137
|
+
codetool-explore --list . --root src --root tests --glob "*.py"
|
|
133
138
|
```
|
|
134
139
|
|
|
135
140
|
The CLI defaults to compact JSON for search, plain text for `--read`, and
|
|
136
141
|
tree-compressed text for `--list`.
|
|
137
142
|
Use `--format text` or `--raw` for raw search text; no search matches print
|
|
138
|
-
`No Match`. Repeat `--root` for multiple search
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
`No Match`. Repeat `--root` for multiple search/read/list roots. A single
|
|
144
|
+
quoted space-delimited `--root` is accepted as a compatibility fallback when it
|
|
145
|
+
can be split into existing roots.
|
|
141
146
|
|
|
142
147
|
## Install
|
|
143
148
|
|
|
@@ -187,13 +192,11 @@ Token counts use `tiktoken` when available. The table compares output across 7 R
|
|
|
187
192
|
|
|
188
193
|
| Output | Tokens | Bytes | Chart |
|
|
189
194
|
| --- | ---: | ---: | --- |
|
|
190
|
-
| `explore
|
|
195
|
+
| `codetool-explore` | 11,008 | 34.3 KB | ██░░░░░░░░░░░░░░░░ |
|
|
191
196
|
| `rtk grep` stdout | 19,646 | 60.1 KB | ███░░░░░░░░░░░░░░░ |
|
|
192
|
-
| default `explore(...)` | 38,393 | 125.3 KB | █████░░░░░░░░░░░░░ |
|
|
193
|
-
| `explore(..., result_format="full")` | 39,027 | 134.7 KB | █████░░░░░░░░░░░░░ |
|
|
194
197
|
| `rg` stdout | 129,775 | 402.4 KB | ██████████████████ |
|
|
195
198
|
|
|
196
|
-
|
|
199
|
+
`codetool-explore` is raw text from `explore(..., result_format="text")`; it omits backend/totals metadata, includes only a cursor hint when truncated, and prints `No Match` for empty pages. It is 0.56× the `rtk grep` token count in this run.
|
|
197
200
|
|
|
198
201
|
Source: `reports/rtk_vs_codetool_output_lengths.json`.
|
|
199
202
|
|
|
@@ -175,37 +175,30 @@ def render_tokens(data: dict[str, Any], source: str) -> list[str]:
|
|
|
175
175
|
if "summary" not in data:
|
|
176
176
|
aggregate = compression_totals(data)
|
|
177
177
|
totals = aggregate["totals"]
|
|
178
|
-
full = int(totals["full_bytes"])
|
|
179
|
-
compressed = int(totals["compressed_bytes"])
|
|
180
178
|
raw = int(totals["raw_bytes"])
|
|
181
179
|
rtk = int(totals["rtk_bytes"])
|
|
182
180
|
rg = int(totals["rg_bytes"])
|
|
183
|
-
grep = int(totals["grep_bytes"])
|
|
184
|
-
full_tokens = int(totals["full_tokens"])
|
|
185
|
-
compressed_tokens = int(totals["compressed_tokens"])
|
|
186
181
|
raw_tokens = int(totals["raw_tokens"])
|
|
187
182
|
rtk_tokens = int(totals["rtk_tokens"])
|
|
188
183
|
rg_tokens = int(totals["rg_tokens"])
|
|
189
|
-
grep_tokens = int(totals["grep_tokens"])
|
|
190
184
|
savings = aggregate["savings"]
|
|
191
|
-
if not
|
|
185
|
+
if not raw:
|
|
192
186
|
raise SystemExit(f"{source} does not contain token-compression data")
|
|
193
|
-
rows = [
|
|
194
|
-
('`explore(..., result_format="full")`', full_tokens, full),
|
|
195
|
-
("default `explore(...)`", compressed_tokens, compressed),
|
|
196
|
-
]
|
|
187
|
+
rows = []
|
|
197
188
|
if raw:
|
|
198
|
-
rows.append((
|
|
189
|
+
rows.append(("`codetool-explore`", raw_tokens, raw))
|
|
199
190
|
if rtk:
|
|
200
191
|
rows.append(("`rtk grep` stdout", rtk_tokens, rtk))
|
|
201
192
|
if rg:
|
|
202
193
|
rows.append(("`rg` stdout", rg_tokens, rg))
|
|
203
|
-
if grep:
|
|
204
|
-
rows.append(("GNU `grep` stdout", grep_tokens, grep))
|
|
205
194
|
max_tokens = max(tokens for _, tokens, _ in rows)
|
|
206
|
-
percent = round(((full - compressed) / full) * 100, 2)
|
|
207
|
-
average_percent = round(mean(savings), 2) if savings else percent
|
|
208
195
|
samples = aggregate["samples"] or len(savings)
|
|
196
|
+
raw_ratio = round(raw_tokens / rtk_tokens, 2) if rtk_tokens else None
|
|
197
|
+
raw_note = (
|
|
198
|
+
f" It is {raw_ratio}× the `rtk grep` token count in this run."
|
|
199
|
+
if raw_ratio is not None
|
|
200
|
+
else ""
|
|
201
|
+
)
|
|
209
202
|
lines = [
|
|
210
203
|
"### Token compression",
|
|
211
204
|
"",
|
|
@@ -224,9 +217,9 @@ def render_tokens(data: dict[str, Any], source: str) -> list[str]:
|
|
|
224
217
|
lines += [
|
|
225
218
|
"",
|
|
226
219
|
(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
"
|
|
220
|
+
"`codetool-explore` is raw text from "
|
|
221
|
+
'`explore(..., result_format="text")`; it omits metadata and '
|
|
222
|
+
"is optimized for minimum tokens." + raw_note
|
|
230
223
|
),
|
|
231
224
|
"",
|
|
232
225
|
f"Source: `{source}`.",
|
|
@@ -235,33 +228,20 @@ def render_tokens(data: dict[str, Any], source: str) -> list[str]:
|
|
|
235
228
|
|
|
236
229
|
summary = data["summary"]
|
|
237
230
|
totals = summary["total_bytes"]
|
|
238
|
-
compressed = int(totals["codetool_compressed_json"])
|
|
239
|
-
full = int(totals["codetool_full_json"])
|
|
240
231
|
raw = int(totals.get("codetool_raw_text", 0))
|
|
241
232
|
rtk = int(totals.get("rtk_grep_stdout", 0))
|
|
242
233
|
rg = int(totals.get("rg_stdout", 0))
|
|
243
|
-
grep = int(totals.get("grep_stdout", 0))
|
|
244
234
|
token_totals = summary.get("total_tokens", {})
|
|
245
|
-
full_tokens = int(token_totals.get("codetool_full_json", round(full / 4)))
|
|
246
|
-
compressed_tokens = int(
|
|
247
|
-
token_totals.get("codetool_compressed_json", round(compressed / 4))
|
|
248
|
-
)
|
|
249
235
|
raw_tokens = int(token_totals.get("codetool_raw_text", round(raw / 4)))
|
|
250
236
|
rtk_tokens = int(token_totals.get("rtk_grep_stdout", round(rtk / 4)))
|
|
251
237
|
rg_tokens = int(token_totals.get("rg_stdout", round(rg / 4)))
|
|
252
|
-
|
|
253
|
-
rows = [
|
|
254
|
-
('`explore(..., result_format="full")`', full_tokens, full),
|
|
255
|
-
("default `explore(...)`", compressed_tokens, compressed),
|
|
256
|
-
]
|
|
238
|
+
rows = []
|
|
257
239
|
if raw:
|
|
258
|
-
rows.append((
|
|
240
|
+
rows.append(("`codetool-explore`", raw_tokens, raw))
|
|
259
241
|
if rtk:
|
|
260
242
|
rows.append(("`rtk grep` stdout", rtk_tokens, rtk))
|
|
261
243
|
if rg:
|
|
262
244
|
rows.append(("`rg` stdout", rg_tokens, rg))
|
|
263
|
-
if grep:
|
|
264
|
-
rows.append(("GNU `grep` stdout", grep_tokens, grep))
|
|
265
245
|
max_tokens = max(tokens for _, tokens, _ in rows)
|
|
266
246
|
comparison = summary["total_comparison"]
|
|
267
247
|
lines = [
|
|
@@ -281,15 +261,15 @@ def render_tokens(data: dict[str, Any], source: str) -> list[str]:
|
|
|
281
261
|
)
|
|
282
262
|
raw_ratio = comparison.get("codetool_raw_over_rtk_tokens_ratio")
|
|
283
263
|
raw_note = (
|
|
284
|
-
f"
|
|
264
|
+
f" It is {raw_ratio}× the `rtk grep` token count in this run."
|
|
285
265
|
if raw_ratio is not None
|
|
286
266
|
else ""
|
|
287
267
|
)
|
|
288
268
|
lines += [
|
|
289
269
|
"",
|
|
290
270
|
(
|
|
291
|
-
|
|
292
|
-
"
|
|
271
|
+
"`codetool-explore` is raw text from "
|
|
272
|
+
'`explore(..., result_format="text")`; it omits backend/totals '
|
|
293
273
|
"metadata, includes only a cursor hint when truncated, and prints "
|
|
294
274
|
"`No Match` for empty pages." + raw_note
|
|
295
275
|
),
|
|
@@ -107,12 +107,13 @@ def explore(
|
|
|
107
107
|
paths without opening files, and ``"content_or_path"`` returns files
|
|
108
108
|
matching either.
|
|
109
109
|
``"read"`` treats ``pattern`` as one file path and returns a controlled
|
|
110
|
-
line range. ``"list"`` treats ``pattern`` as one
|
|
111
|
-
returns a one-level listing.
|
|
110
|
+
line range for each root. ``"list"`` treats ``pattern`` as one
|
|
111
|
+
file/directory path and returns a one-level listing for each root.
|
|
112
112
|
Patterns are interpreted as regex by default; pass ``regex=False`` for exact
|
|
113
113
|
literal search. ``root`` may be one directory/file path or a non-empty list
|
|
114
|
-
of directory/file paths. Multi-root searches report paths
|
|
115
|
-
roots' common base, so sibling roots keep prefixes such as
|
|
114
|
+
of directory/file paths. Multi-root searches/reads/listings report paths
|
|
115
|
+
relative to the roots' common base, so sibling roots keep prefixes such as
|
|
116
|
+
``src/...``.
|
|
116
117
|
To tolerate common JSON/tool-call mistakes, a whitespace-separated root
|
|
117
118
|
string is treated as multiple roots only when that exact path does not
|
|
118
119
|
exist and every split token is an existing file or directory.
|
|
@@ -122,8 +123,8 @@ def explore(
|
|
|
122
123
|
compatibility fallback.
|
|
123
124
|
|
|
124
125
|
Search results are returned in a compact structured shape by default; read
|
|
125
|
-
results default to plain text
|
|
126
|
-
text. Pass
|
|
126
|
+
results default to plain text (with path headers only when multiple files
|
|
127
|
+
are read) and list results default to tree-compressed text. Pass
|
|
127
128
|
``result_format="full"`` to receive the pre-compression backend result
|
|
128
129
|
dictionary unchanged. Pass ``result_format="text"`` (or ``"raw"``) for an
|
|
129
130
|
RTK-inspired plain-text rendering optimized for token compression.
|
|
@@ -26,12 +26,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
26
26
|
action_group.add_argument(
|
|
27
27
|
"--read",
|
|
28
28
|
dest="read_path",
|
|
29
|
-
help="read one known file path with controlled line output",
|
|
29
|
+
help="read one known file path under each root with controlled line output",
|
|
30
30
|
)
|
|
31
31
|
action_group.add_argument(
|
|
32
32
|
"--list",
|
|
33
33
|
dest="list_path",
|
|
34
|
-
help="list one directory level or one file path",
|
|
34
|
+
help="list one directory level or one file path under each root",
|
|
35
35
|
)
|
|
36
36
|
parser.add_argument(
|
|
37
37
|
"--root",
|
|
@@ -19,7 +19,8 @@ from .ignore import (
|
|
|
19
19
|
)
|
|
20
20
|
from .python_backend.constants import binary_check_bytes
|
|
21
21
|
from .python_backend.ignore_rules import ignore_patterns_for_root
|
|
22
|
-
from .
|
|
22
|
+
from .python_backend.models import IgnorePatterns
|
|
23
|
+
from .roots import NormalizedRoots, RootInput, SearchRoot, normalize_search_roots
|
|
23
24
|
|
|
24
25
|
MAX_READ_CHARS = 100_000
|
|
25
26
|
READ_CHUNK_BYTES = 8192
|
|
@@ -35,6 +36,9 @@ class ResolvedExplorePath:
|
|
|
35
36
|
root_display: str | list[str]
|
|
36
37
|
display_base: str | None
|
|
37
38
|
root_base: str
|
|
39
|
+
search_root_abs: str
|
|
40
|
+
root_is_file: bool
|
|
41
|
+
rel_base_abs: str | None
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
def read_file_target(
|
|
@@ -45,38 +49,58 @@ def read_file_target(
|
|
|
45
49
|
limit: int = 50,
|
|
46
50
|
cursor: str | int | None = None,
|
|
47
51
|
) -> dict[str, object]:
|
|
48
|
-
"""Return a controlled line range from one text file."""
|
|
52
|
+
"""Return a controlled line range from one text file under each root."""
|
|
49
53
|
|
|
50
54
|
safe_limit = normalize_limit(limit)
|
|
51
55
|
safe_start_line = _normalize_start_line(start_line, cursor=cursor)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
raise ExploreRootError(f"read target is a directory: {resolved.query!r}")
|
|
58
|
-
if not os.path.isfile(resolved.abs_path):
|
|
59
|
-
raise ExploreRootError(f"read target is not a file: {resolved.query!r}")
|
|
60
|
-
|
|
61
|
-
_raise_if_binary(resolved.abs_path, resolved.display_path)
|
|
62
|
-
text, returned_lines, has_more_lines, content_truncated = _read_text_range(
|
|
63
|
-
resolved.abs_path,
|
|
64
|
-
display_path=resolved.display_path,
|
|
65
|
-
start_line=safe_start_line,
|
|
66
|
-
limit=safe_limit,
|
|
56
|
+
resolved_paths = _resolve_explore_paths(
|
|
57
|
+
path,
|
|
58
|
+
root=root,
|
|
59
|
+
target="read",
|
|
60
|
+
allow_multiple=True,
|
|
67
61
|
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
files: list[dict[str, object]] = []
|
|
63
|
+
seen_files: set[str] = set()
|
|
64
|
+
for resolved in resolved_paths:
|
|
65
|
+
identity = os.path.normcase(os.path.abspath(resolved.abs_path))
|
|
66
|
+
if identity in seen_files:
|
|
67
|
+
continue
|
|
68
|
+
seen_files.add(identity)
|
|
69
|
+
error_path = (
|
|
70
|
+
resolved.query if len(resolved_paths) == 1 else resolved.display_path
|
|
71
|
+
)
|
|
72
|
+
files.append(
|
|
73
|
+
_read_resolved_file(
|
|
74
|
+
resolved,
|
|
75
|
+
error_path=error_path,
|
|
76
|
+
start_line=safe_start_line,
|
|
77
|
+
limit=safe_limit,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
text = _format_read_files_text(files)
|
|
82
|
+
returned_lines = sum(int(item.get("returned", 0) or 0) for item in files)
|
|
83
|
+
content_truncated = any(bool(item.get("content_truncated")) for item in files)
|
|
84
|
+
next_cursor = next(
|
|
85
|
+
(
|
|
86
|
+
str(item_next)
|
|
87
|
+
for item in files
|
|
88
|
+
if (item_next := item.get("next_cursor")) not in (None, "")
|
|
89
|
+
),
|
|
90
|
+
None,
|
|
72
91
|
)
|
|
92
|
+
path_display: object
|
|
93
|
+
if len(files) == 1:
|
|
94
|
+
path_display = files[0]["path"]
|
|
95
|
+
else:
|
|
96
|
+
path_display = [item["path"] for item in files]
|
|
73
97
|
|
|
74
|
-
|
|
75
|
-
"pattern":
|
|
76
|
-
"root":
|
|
98
|
+
result: dict[str, object] = {
|
|
99
|
+
"pattern": resolved_paths[0].query,
|
|
100
|
+
"root": resolved_paths[0].root_display,
|
|
77
101
|
"target": "read",
|
|
78
102
|
"mode": "read",
|
|
79
|
-
"path":
|
|
103
|
+
"path": path_display,
|
|
80
104
|
"start_line": safe_start_line,
|
|
81
105
|
"limit": safe_limit,
|
|
82
106
|
"cursor": None if cursor in (None, "") else str(cursor),
|
|
@@ -84,12 +108,16 @@ def read_file_target(
|
|
|
84
108
|
"returned": returned_lines,
|
|
85
109
|
"line_count": returned_lines,
|
|
86
110
|
"count": returned_lines,
|
|
87
|
-
"
|
|
111
|
+
"file_count": len(files),
|
|
112
|
+
"truncated": bool(next_cursor or content_truncated),
|
|
88
113
|
"content_truncated": content_truncated,
|
|
89
114
|
"next_cursor": next_cursor,
|
|
90
115
|
"offset": safe_start_line - 1,
|
|
91
116
|
"backend": "python",
|
|
92
117
|
}
|
|
118
|
+
if len(files) > 1:
|
|
119
|
+
result["files"] = files
|
|
120
|
+
return result
|
|
93
121
|
|
|
94
122
|
|
|
95
123
|
def list_path_target(
|
|
@@ -104,41 +132,64 @@ def list_path_target(
|
|
|
104
132
|
"""Return one ls-like page for a file or one directory level."""
|
|
105
133
|
|
|
106
134
|
safe_limit = normalize_limit(limit)
|
|
107
|
-
|
|
135
|
+
resolved_paths = _resolve_explore_paths(
|
|
136
|
+
path,
|
|
137
|
+
root=root,
|
|
138
|
+
target="list",
|
|
139
|
+
allow_empty=True,
|
|
140
|
+
allow_multiple=True,
|
|
141
|
+
)
|
|
108
142
|
glob_patterns = normalize_patterns(glob)
|
|
109
143
|
exclude_patterns = normalize_patterns(exclude)
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
resolved
|
|
116
|
-
glob_patterns=glob_patterns,
|
|
117
|
-
exclude_patterns=exclude_patterns,
|
|
118
|
-
)
|
|
119
|
-
elif os.path.isdir(resolved.abs_path):
|
|
120
|
-
entries = _list_directory(
|
|
121
|
-
resolved,
|
|
122
|
-
glob_patterns=glob_patterns,
|
|
123
|
-
exclude_patterns=exclude_patterns,
|
|
124
|
-
)
|
|
125
|
-
else:
|
|
126
|
-
raise ExploreRootError(
|
|
127
|
-
f"list target is neither a directory nor file: {resolved.query!r}"
|
|
145
|
+
entries: list[dict[str, object]] = []
|
|
146
|
+
seen_entries: set[tuple[str, str]] = set()
|
|
147
|
+
for resolved in resolved_paths:
|
|
148
|
+
error_path = (
|
|
149
|
+
resolved.query if len(resolved_paths) == 1 else resolved.display_path
|
|
128
150
|
)
|
|
151
|
+
if not os.path.exists(resolved.abs_path):
|
|
152
|
+
raise ExploreRootError(f"list target does not exist: {error_path!r}")
|
|
153
|
+
if os.path.isfile(resolved.abs_path):
|
|
154
|
+
root_entries = _list_single_file(
|
|
155
|
+
resolved,
|
|
156
|
+
glob_patterns=glob_patterns,
|
|
157
|
+
exclude_patterns=exclude_patterns,
|
|
158
|
+
)
|
|
159
|
+
elif os.path.isdir(resolved.abs_path):
|
|
160
|
+
root_entries = _list_directory(
|
|
161
|
+
resolved,
|
|
162
|
+
glob_patterns=glob_patterns,
|
|
163
|
+
exclude_patterns=exclude_patterns,
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
raise ExploreRootError(
|
|
167
|
+
f"list target is neither a directory nor file: {error_path!r}"
|
|
168
|
+
)
|
|
169
|
+
for entry in root_entries:
|
|
170
|
+
identity = _entry_identity(entry)
|
|
171
|
+
if identity in seen_entries:
|
|
172
|
+
continue
|
|
173
|
+
seen_entries.add(identity)
|
|
174
|
+
entries.append(entry)
|
|
129
175
|
|
|
130
176
|
page, truncated, next_cursor, offset = page_items(
|
|
131
177
|
entries, limit=safe_limit, cursor=cursor
|
|
132
178
|
)
|
|
133
179
|
total_files = sum(1 for entry in entries if entry.get("kind") == "file")
|
|
134
180
|
total_dirs = sum(1 for entry in entries if entry.get("kind") == "dir")
|
|
181
|
+
path_display: object
|
|
182
|
+
if len(resolved_paths) == 1:
|
|
183
|
+
path_display = resolved_paths[0].display_path
|
|
184
|
+
else:
|
|
185
|
+
path_display = [resolved.display_path for resolved in resolved_paths]
|
|
135
186
|
|
|
136
187
|
result: dict[str, object] = {
|
|
137
|
-
"pattern":
|
|
138
|
-
"root":
|
|
188
|
+
"pattern": resolved_paths[0].query,
|
|
189
|
+
"root": resolved_paths[0].root_display,
|
|
139
190
|
"target": "list",
|
|
140
191
|
"mode": "list",
|
|
141
|
-
"path":
|
|
192
|
+
"path": path_display,
|
|
142
193
|
"entries": page,
|
|
143
194
|
"returned": len(page),
|
|
144
195
|
"total_entries": len(entries),
|
|
@@ -167,8 +218,25 @@ def _resolve_explore_path(
|
|
|
167
218
|
target: str,
|
|
168
219
|
allow_empty: bool = False,
|
|
169
220
|
) -> ResolvedExplorePath:
|
|
221
|
+
return _resolve_explore_paths(
|
|
222
|
+
path,
|
|
223
|
+
root=root,
|
|
224
|
+
target=target,
|
|
225
|
+
allow_empty=allow_empty,
|
|
226
|
+
allow_multiple=False,
|
|
227
|
+
)[0]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _resolve_explore_paths(
|
|
231
|
+
path: object,
|
|
232
|
+
*,
|
|
233
|
+
root: RootInput,
|
|
234
|
+
target: str,
|
|
235
|
+
allow_empty: bool = False,
|
|
236
|
+
allow_multiple: bool = False,
|
|
237
|
+
) -> tuple[ResolvedExplorePath, ...]:
|
|
170
238
|
root_set = normalize_search_roots(root)
|
|
171
|
-
if root_set.has_multiple:
|
|
239
|
+
if root_set.has_multiple and not allow_multiple:
|
|
172
240
|
raise ExploreArgumentError(f"target={target!r} supports one root at a time")
|
|
173
241
|
|
|
174
242
|
try:
|
|
@@ -183,15 +251,32 @@ def _resolve_explore_path(
|
|
|
183
251
|
else:
|
|
184
252
|
raise ExploreArgumentError(f"{target} path must not be empty")
|
|
185
253
|
|
|
186
|
-
|
|
254
|
+
return tuple(
|
|
255
|
+
_resolve_path_for_search_root(query, search_root=search_root, root_set=root_set)
|
|
256
|
+
for search_root in root_set.roots
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _resolve_path_for_search_root(
|
|
261
|
+
query: str,
|
|
262
|
+
*,
|
|
263
|
+
search_root: SearchRoot,
|
|
264
|
+
root_set: NormalizedRoots,
|
|
265
|
+
) -> ResolvedExplorePath:
|
|
187
266
|
root_abs = search_root.abs_path
|
|
267
|
+
root_is_file = os.path.isfile(root_abs)
|
|
188
268
|
root_base = root_abs if os.path.isdir(root_abs) else os.path.dirname(root_abs)
|
|
189
269
|
if os.path.isabs(query):
|
|
190
270
|
abs_path = os.path.abspath(query)
|
|
191
|
-
|
|
271
|
+
base = root_set.rel_base if root_set.has_multiple else root_base
|
|
272
|
+
display_base = base if base and _is_under(abs_path, base) else None
|
|
192
273
|
else:
|
|
193
|
-
abs_path =
|
|
194
|
-
|
|
274
|
+
abs_path = (
|
|
275
|
+
os.path.abspath(root_abs)
|
|
276
|
+
if root_is_file and query == "."
|
|
277
|
+
else os.path.abspath(os.path.join(root_base, query))
|
|
278
|
+
)
|
|
279
|
+
display_base = root_set.rel_base if root_set.has_multiple else root_base
|
|
195
280
|
display_path = _display_path(abs_path, display_base)
|
|
196
281
|
return ResolvedExplorePath(
|
|
197
282
|
query=query,
|
|
@@ -200,6 +285,9 @@ def _resolve_explore_path(
|
|
|
200
285
|
root_display=root_set.display,
|
|
201
286
|
display_base=display_base,
|
|
202
287
|
root_base=root_base,
|
|
288
|
+
search_root_abs=root_abs,
|
|
289
|
+
root_is_file=root_is_file,
|
|
290
|
+
rel_base_abs=root_set.rel_base,
|
|
203
291
|
)
|
|
204
292
|
|
|
205
293
|
|
|
@@ -219,6 +307,56 @@ def _normalize_start_line(start_line: int, *, cursor: str | int | None) -> int:
|
|
|
219
307
|
return value
|
|
220
308
|
|
|
221
309
|
|
|
310
|
+
def _read_resolved_file(
|
|
311
|
+
resolved: ResolvedExplorePath,
|
|
312
|
+
*,
|
|
313
|
+
error_path: str,
|
|
314
|
+
start_line: int,
|
|
315
|
+
limit: int,
|
|
316
|
+
) -> dict[str, object]:
|
|
317
|
+
if not os.path.exists(resolved.abs_path):
|
|
318
|
+
raise ExploreRootError(f"file does not exist: {error_path!r}")
|
|
319
|
+
if os.path.isdir(resolved.abs_path):
|
|
320
|
+
raise ExploreRootError(f"read target is a directory: {error_path!r}")
|
|
321
|
+
if not os.path.isfile(resolved.abs_path):
|
|
322
|
+
raise ExploreRootError(f"read target is not a file: {error_path!r}")
|
|
323
|
+
|
|
324
|
+
_raise_if_binary(resolved.abs_path, resolved.display_path)
|
|
325
|
+
text, returned_lines, has_more_lines, content_truncated = _read_text_range(
|
|
326
|
+
resolved.abs_path,
|
|
327
|
+
display_path=resolved.display_path,
|
|
328
|
+
start_line=start_line,
|
|
329
|
+
limit=limit,
|
|
330
|
+
)
|
|
331
|
+
next_cursor = (
|
|
332
|
+
str(start_line + returned_lines)
|
|
333
|
+
if has_more_lines and returned_lines > 0
|
|
334
|
+
else None
|
|
335
|
+
)
|
|
336
|
+
return {
|
|
337
|
+
"path": resolved.display_path,
|
|
338
|
+
"text": text,
|
|
339
|
+
"returned": returned_lines,
|
|
340
|
+
"line_count": returned_lines,
|
|
341
|
+
"content_truncated": content_truncated,
|
|
342
|
+
"next_cursor": next_cursor,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _format_read_files_text(files: list[dict[str, object]]) -> str:
|
|
347
|
+
if len(files) == 1:
|
|
348
|
+
return str(files[0].get("text", ""))
|
|
349
|
+
sections: list[str] = []
|
|
350
|
+
for item in files:
|
|
351
|
+
header = f"{item.get('path', '')}:"
|
|
352
|
+
text = str(item.get("text", ""))
|
|
353
|
+
if text or int(item.get("returned", 0) or 0) > 0:
|
|
354
|
+
sections.append(f"{header}\n{text}")
|
|
355
|
+
else:
|
|
356
|
+
sections.append(header)
|
|
357
|
+
return "\n".join(sections)
|
|
358
|
+
|
|
359
|
+
|
|
222
360
|
def _is_under(path: str, base: str) -> bool:
|
|
223
361
|
try:
|
|
224
362
|
common_path = os.path.commonpath(
|
|
@@ -378,11 +516,9 @@ def _list_single_file(
|
|
|
378
516
|
) -> list[dict[str, object]]:
|
|
379
517
|
parent = os.path.dirname(resolved.abs_path) or os.curdir
|
|
380
518
|
rel_path = _display_path(resolved.abs_path, resolved.display_base)
|
|
381
|
-
api_ignore_base =
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
rel_base_abs=None,
|
|
385
|
-
is_file=False,
|
|
519
|
+
api_ignore_patterns, api_ignore_base = _listing_ignore_patterns(
|
|
520
|
+
resolved,
|
|
521
|
+
is_file=True,
|
|
386
522
|
)
|
|
387
523
|
local_ignore_patterns = ignore_patterns_for_root(
|
|
388
524
|
resolved.abs_path,
|
|
@@ -393,6 +529,7 @@ def _list_single_file(
|
|
|
393
529
|
rel_path,
|
|
394
530
|
is_dir=False,
|
|
395
531
|
exclude_patterns=exclude_patterns,
|
|
532
|
+
ignore_patterns=api_ignore_patterns.common,
|
|
396
533
|
root_ignore_patterns=api_ignore_patterns.root,
|
|
397
534
|
common_rel_path=relative_path(resolved.abs_path, api_ignore_base),
|
|
398
535
|
) or _is_ignored_by_local_listing_patterns(
|
|
@@ -412,10 +549,8 @@ def _list_directory(
|
|
|
412
549
|
glob_patterns: tuple[str, ...],
|
|
413
550
|
exclude_patterns: tuple[str, ...],
|
|
414
551
|
) -> list[dict[str, object]]:
|
|
415
|
-
api_ignore_base =
|
|
416
|
-
|
|
417
|
-
api_ignore_base,
|
|
418
|
-
rel_base_abs=None,
|
|
552
|
+
api_ignore_patterns, api_ignore_base = _listing_ignore_patterns(
|
|
553
|
+
resolved,
|
|
419
554
|
is_file=False,
|
|
420
555
|
)
|
|
421
556
|
local_ignore_patterns = ignore_patterns_for_root(
|
|
@@ -439,6 +574,7 @@ def _list_directory(
|
|
|
439
574
|
rel_path,
|
|
440
575
|
is_dir=is_dir,
|
|
441
576
|
exclude_patterns=exclude_patterns,
|
|
577
|
+
ignore_patterns=api_ignore_patterns.common,
|
|
442
578
|
root_ignore_patterns=api_ignore_patterns.root,
|
|
443
579
|
common_rel_path=relative_path(entry.path, api_ignore_base),
|
|
444
580
|
) or _is_ignored_by_local_listing_patterns(
|
|
@@ -466,6 +602,37 @@ def _list_directory(
|
|
|
466
602
|
return entries
|
|
467
603
|
|
|
468
604
|
|
|
605
|
+
def _listing_ignore_patterns(
|
|
606
|
+
resolved: ResolvedExplorePath,
|
|
607
|
+
*,
|
|
608
|
+
is_file: bool,
|
|
609
|
+
) -> tuple[IgnorePatterns, str]:
|
|
610
|
+
root_filter_base = (
|
|
611
|
+
os.path.dirname(resolved.search_root_abs) or os.curdir
|
|
612
|
+
if resolved.root_is_file
|
|
613
|
+
else resolved.search_root_abs
|
|
614
|
+
)
|
|
615
|
+
if _is_under(resolved.abs_path, root_filter_base):
|
|
616
|
+
return (
|
|
617
|
+
ignore_patterns_for_root(
|
|
618
|
+
resolved.search_root_abs,
|
|
619
|
+
rel_base_abs=resolved.rel_base_abs,
|
|
620
|
+
is_file=resolved.root_is_file,
|
|
621
|
+
),
|
|
622
|
+
root_filter_base,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
api_ignore_base = _listing_api_ignore_base(resolved, is_file=is_file)
|
|
626
|
+
return (
|
|
627
|
+
ignore_patterns_for_root(
|
|
628
|
+
api_ignore_base,
|
|
629
|
+
rel_base_abs=None,
|
|
630
|
+
is_file=False,
|
|
631
|
+
),
|
|
632
|
+
api_ignore_base,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
|
|
469
636
|
def _listing_api_ignore_base(resolved: ResolvedExplorePath, *, is_file: bool) -> str:
|
|
470
637
|
if _is_under(resolved.abs_path, resolved.root_base):
|
|
471
638
|
return resolved.root_base
|
|
@@ -494,4 +661,10 @@ def _is_ignored_by_local_listing_patterns(
|
|
|
494
661
|
|
|
495
662
|
def _entry_sort_key(entry: dict[str, Any]) -> tuple[str, str]:
|
|
496
663
|
path = str(entry.get("path", ""))
|
|
497
|
-
return (path.rstrip("/").casefold(), path)
|
|
664
|
+
return (path.rstrip("/").casefold(), path)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _entry_identity(entry: dict[str, object]) -> tuple[str, str]:
|
|
668
|
+
kind = str(entry.get("kind", ""))
|
|
669
|
+
path = normalize_relpath(str(entry.get("path", ""))).rstrip("/")
|
|
670
|
+
return (os.path.normcase(path), kind)
|
|
@@ -297,6 +297,67 @@ def test_read_target_reports_more_and_supports_csv_as_text(tmp_path):
|
|
|
297
297
|
assert compressed["text"] == "name,value"
|
|
298
298
|
|
|
299
299
|
|
|
300
|
+
def test_read_target_accepts_root_list_with_headers(tmp_path):
|
|
301
|
+
write(tmp_path / "src" / "settings.txt", "src one\nsrc two\n")
|
|
302
|
+
write(tmp_path / "tests" / "settings.txt", "test one\ntest two\n")
|
|
303
|
+
|
|
304
|
+
text = api.explore(
|
|
305
|
+
"settings.txt",
|
|
306
|
+
root=[str(tmp_path / "src"), str(tmp_path / "tests")],
|
|
307
|
+
target="read",
|
|
308
|
+
limit=1,
|
|
309
|
+
)
|
|
310
|
+
full = api.explore(
|
|
311
|
+
"settings.txt",
|
|
312
|
+
root=[str(tmp_path / "src"), str(tmp_path / "tests")],
|
|
313
|
+
target="read",
|
|
314
|
+
limit=1,
|
|
315
|
+
result_format="full",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
assert text == "\n".join(
|
|
319
|
+
[
|
|
320
|
+
"-- more: cursor=2",
|
|
321
|
+
"src/settings.txt:",
|
|
322
|
+
"src one",
|
|
323
|
+
"tests/settings.txt:",
|
|
324
|
+
"test one",
|
|
325
|
+
]
|
|
326
|
+
)
|
|
327
|
+
assert full["root"] == [str(tmp_path / "src"), str(tmp_path / "tests")]
|
|
328
|
+
assert full["path"] == ["src/settings.txt", "tests/settings.txt"]
|
|
329
|
+
assert full["text"] == "\n".join(
|
|
330
|
+
[
|
|
331
|
+
"src/settings.txt:",
|
|
332
|
+
"src one",
|
|
333
|
+
"tests/settings.txt:",
|
|
334
|
+
"test one",
|
|
335
|
+
]
|
|
336
|
+
)
|
|
337
|
+
assert full["returned"] == 2
|
|
338
|
+
assert full["line_count"] == 2
|
|
339
|
+
assert full["file_count"] == 2
|
|
340
|
+
assert full["next_cursor"] == "2"
|
|
341
|
+
assert full["files"] == [
|
|
342
|
+
{
|
|
343
|
+
"path": "src/settings.txt",
|
|
344
|
+
"text": "src one",
|
|
345
|
+
"returned": 1,
|
|
346
|
+
"line_count": 1,
|
|
347
|
+
"content_truncated": False,
|
|
348
|
+
"next_cursor": "2",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"path": "tests/settings.txt",
|
|
352
|
+
"text": "test one",
|
|
353
|
+
"returned": 1,
|
|
354
|
+
"line_count": 1,
|
|
355
|
+
"content_truncated": False,
|
|
356
|
+
"next_cursor": "2",
|
|
357
|
+
},
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
|
|
300
361
|
def test_read_target_preserves_blank_line_when_more_marker_is_present(tmp_path):
|
|
301
362
|
write(tmp_path / "blank.txt", "\nsecond\n")
|
|
302
363
|
|
|
@@ -331,7 +392,8 @@ def test_read_target_clean_errors(tmp_path):
|
|
|
331
392
|
api.explore("src", root=str(tmp_path), target="read")
|
|
332
393
|
with pytest.raises(ExploreRootError, match="binary"):
|
|
333
394
|
api.explore("binary.dat", root=str(tmp_path), target="read")
|
|
334
|
-
|
|
395
|
+
write(tmp_path / "src" / "a.py", "")
|
|
396
|
+
with pytest.raises(ExploreRootError, match="tests/a.py"):
|
|
335
397
|
api.explore(
|
|
336
398
|
"a.py",
|
|
337
399
|
root=[str(tmp_path / "src"), str(tmp_path / "tests")],
|
|
@@ -442,6 +504,64 @@ def test_list_text_output_uses_tree_for_repeated_prefixes(tmp_path):
|
|
|
442
504
|
assert "\n\n" not in result
|
|
443
505
|
|
|
444
506
|
|
|
507
|
+
def test_list_target_accepts_root_list_and_appends_each_tree(tmp_path):
|
|
508
|
+
write(tmp_path / "src" / "codetool_explore" / "__init__.py", "")
|
|
509
|
+
for name in ("test_api.py", "test_cli.py", "test_cursor.py", "test_ignore.py"):
|
|
510
|
+
write(tmp_path / "tests" / name, "")
|
|
511
|
+
|
|
512
|
+
result = api.explore(
|
|
513
|
+
".",
|
|
514
|
+
root=[str(tmp_path / "src"), str(tmp_path / "tests")],
|
|
515
|
+
target="list",
|
|
516
|
+
)
|
|
517
|
+
full = api.explore(
|
|
518
|
+
".",
|
|
519
|
+
root=[str(tmp_path / "src"), str(tmp_path / "tests")],
|
|
520
|
+
target="list",
|
|
521
|
+
result_format="full",
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
assert result == "\n".join(
|
|
525
|
+
[
|
|
526
|
+
"src/",
|
|
527
|
+
" codetool_explore/",
|
|
528
|
+
"tests/",
|
|
529
|
+
" test_api.py",
|
|
530
|
+
" test_cli.py",
|
|
531
|
+
" test_cursor.py",
|
|
532
|
+
" test_ignore.py",
|
|
533
|
+
]
|
|
534
|
+
)
|
|
535
|
+
assert full["root"] == [str(tmp_path / "src"), str(tmp_path / "tests")]
|
|
536
|
+
assert full["path"] == ["src", "tests"]
|
|
537
|
+
assert full["entries"] == [
|
|
538
|
+
{"path": "src/codetool_explore/", "kind": "dir"},
|
|
539
|
+
{"path": "tests/test_api.py", "kind": "file"},
|
|
540
|
+
{"path": "tests/test_cli.py", "kind": "file"},
|
|
541
|
+
{"path": "tests/test_cursor.py", "kind": "file"},
|
|
542
|
+
{"path": "tests/test_ignore.py", "kind": "file"},
|
|
543
|
+
]
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def test_list_target_root_list_uses_search_ignore_logic(tmp_path):
|
|
547
|
+
write(tmp_path / ".gitignore", "build/\n*.tmp\n")
|
|
548
|
+
write(tmp_path / "build" / "keep.py", "")
|
|
549
|
+
write(tmp_path / "build" / "skip.tmp", "")
|
|
550
|
+
write(tmp_path / "src" / "app.py", "")
|
|
551
|
+
|
|
552
|
+
result = api.explore(
|
|
553
|
+
".",
|
|
554
|
+
root=[str(tmp_path / "build"), str(tmp_path / "src")],
|
|
555
|
+
target="list",
|
|
556
|
+
result_format="full",
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
assert result["entries"] == [
|
|
560
|
+
{"path": "build/keep.py", "kind": "file"},
|
|
561
|
+
{"path": "src/app.py", "kind": "file"},
|
|
562
|
+
]
|
|
563
|
+
|
|
564
|
+
|
|
445
565
|
def test_compressed_snippets_preserve_line_snippet_context_and_page(tmp_path):
|
|
446
566
|
write(tmp_path / "a.py", "before\nneedle one\nneedle two\n")
|
|
447
567
|
|
|
@@ -212,6 +212,33 @@ def test_cli_read_defaults_to_text_and_honors_line_range(tmp_path, capsys):
|
|
|
212
212
|
assert capsys.readouterr().out == "-- more: cursor=3\ntwo\n"
|
|
213
213
|
|
|
214
214
|
|
|
215
|
+
def test_cli_read_accepts_repeated_roots_with_headers(tmp_path, capsys):
|
|
216
|
+
write(tmp_path / "src" / "settings.txt", "src one\n")
|
|
217
|
+
write(tmp_path / "tests" / "settings.txt", "test one\n")
|
|
218
|
+
|
|
219
|
+
code = cli.main(
|
|
220
|
+
[
|
|
221
|
+
"--read",
|
|
222
|
+
"settings.txt",
|
|
223
|
+
"--root",
|
|
224
|
+
str(tmp_path / "src"),
|
|
225
|
+
"--root",
|
|
226
|
+
str(tmp_path / "tests"),
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
assert code == 0
|
|
231
|
+
assert capsys.readouterr().out == "\n".join(
|
|
232
|
+
[
|
|
233
|
+
"src/settings.txt:",
|
|
234
|
+
"src one",
|
|
235
|
+
"tests/settings.txt:",
|
|
236
|
+
"test one",
|
|
237
|
+
"",
|
|
238
|
+
]
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
215
242
|
def test_cli_list_defaults_to_text(tmp_path, capsys):
|
|
216
243
|
write(tmp_path / "src" / "app.py", "")
|
|
217
244
|
write(tmp_path / "src" / "pkg" / "mod.py", "")
|
|
@@ -222,6 +249,36 @@ def test_cli_list_defaults_to_text(tmp_path, capsys):
|
|
|
222
249
|
assert capsys.readouterr().out == "src/\n app.py\n pkg/\n"
|
|
223
250
|
|
|
224
251
|
|
|
252
|
+
def test_cli_list_accepts_repeated_roots(tmp_path, capsys):
|
|
253
|
+
write(tmp_path / "src" / "codetool_explore" / "__init__.py", "")
|
|
254
|
+
for name in ("test_api.py", "test_cli.py", "test_cursor.py"):
|
|
255
|
+
write(tmp_path / "tests" / name, "")
|
|
256
|
+
|
|
257
|
+
code = cli.main(
|
|
258
|
+
[
|
|
259
|
+
"--list",
|
|
260
|
+
".",
|
|
261
|
+
"--root",
|
|
262
|
+
str(tmp_path / "src"),
|
|
263
|
+
"--root",
|
|
264
|
+
str(tmp_path / "tests"),
|
|
265
|
+
]
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
assert code == 0
|
|
269
|
+
assert capsys.readouterr().out == "\n".join(
|
|
270
|
+
[
|
|
271
|
+
"src/",
|
|
272
|
+
" codetool_explore/",
|
|
273
|
+
"tests/",
|
|
274
|
+
" test_api.py",
|
|
275
|
+
" test_cli.py",
|
|
276
|
+
" test_cursor.py",
|
|
277
|
+
"",
|
|
278
|
+
]
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
225
282
|
def test_cli_read_full_format_returns_json(tmp_path, capsys):
|
|
226
283
|
write(tmp_path / "docs" / "guide.txt", "one\ntwo\n")
|
|
227
284
|
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
.venv/
|
|
2
|
-
__pycache__/
|
|
3
|
-
.pytest_cache/
|
|
4
|
-
*.py[cod]
|
|
5
|
-
rust/target/
|
|
6
|
-
benchmark-corpus/
|
|
7
|
-
benchmarks/corpus/
|
|
8
|
-
reports/*
|
|
9
|
-
!reports/research_lab_benchmark.json
|
|
10
|
-
!reports/research_lab_result.html
|
|
11
|
-
!reports/rtk_vs_codetool_output_length_analysis.html
|
|
12
|
-
!reports/rtk_vs_codetool_output_lengths.json
|
|
13
|
-
!reports/search_benchmark.json
|
|
14
|
-
!reports/search_benchmark_analysis.html
|
|
15
|
-
!reports/search_benchmark_rtk_compression.json
|
|
16
|
-
!reports/search_compression_analysis.html
|
|
17
|
-
research/
|
|
18
|
-
.tmp*/
|
|
19
|
-
.ruff_cache/
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/__init__.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/case.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/config.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/constants.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/file_search.py
RENAMED
|
File without changes
|
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/literal.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/matcher.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/models.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/output.py
RENAMED
|
File without changes
|
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/search.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/text.py
RENAMED
|
File without changes
|
{codetool_explore-0.5.0 → codetool_explore-0.6.0}/src/codetool_explore/python_backend/walker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|