contextzip 0.3.7__tar.gz → 0.3.9__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.
- {contextzip-0.3.7 → contextzip-0.3.9}/PKG-INFO +46 -93
- {contextzip-0.3.7 → contextzip-0.3.9}/README.md +45 -92
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/__init__.py +7 -1
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/api.py +82 -1
- contextzip-0.3.9/contextzip/applier.py +635 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/cli.py +148 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/cli_display.py +105 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/config.py +24 -16
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/packager.py +47 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/base.py +31 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/server.py +32 -10
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/PKG-INFO +46 -93
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/SOURCES.txt +1 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/pyproject.toml +1 -1
- {contextzip-0.3.7 → contextzip-0.3.9}/LICENSE +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/ai/__init__.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/ai/gemini.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/ai/heuristic.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/ai/selector.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/cli_ai.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/cli_onboard.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/clipboard.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/detector.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/error_parser.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/filters.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/git.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/project_config.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/errors/__init__.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/errors/node.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/errors/python.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/go.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/node.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/python.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/ruby.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/rust.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/watcher.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/__init__.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/assets.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/persist.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/suggestions.py +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.3.7 → contextzip-0.3.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextzip
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Intelligently package your codebase for AI tools
|
|
5
5
|
Author-email: Deepesh <akadeepesh@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,6 +43,8 @@ Every AI session starts the same way: hunt down the relevant files, manually ski
|
|
|
43
43
|
|
|
44
44
|
contextzip eliminates that entirely. Run it from your project root — it detects your stack, applies smart exclusions, produces a lean ZIP, and opens your file manager with the archive already selected. One `Ctrl+C` and you're done.
|
|
45
45
|
|
|
46
|
+
And when the AI tool hands you a ZIP back with the changes, `contextzip apply-zip` closes the loop — no manual unzip-and-hope, no losing track of what actually changed.
|
|
47
|
+
|
|
46
48
|
---
|
|
47
49
|
|
|
48
50
|
## Features
|
|
@@ -51,11 +53,12 @@ contextzip eliminates that entirely. Run it from your project root — it detect
|
|
|
51
53
|
- **Respects `.gitignore`** — your existing ignore patterns are honoured automatically
|
|
52
54
|
- **Git-aware packaging** — use `--git-changes` to package only modified, staged, and untracked files; perfect for incremental debugging and PR review sessions
|
|
53
55
|
- **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
|
|
56
|
+
- **Apply AI-returned changes back** — `contextzip apply-zip` takes the ZIP an AI tool hands you back and writes it into your project safely: diffed against what was actually sent, backed up before anything is overwritten, and never silently deleting anything
|
|
54
57
|
- **Terminal error watcher** — wrap any dev server with `contextzip watch` to auto-detect errors and package a ready-to-upload debug context in one keypress
|
|
55
|
-
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip
|
|
56
|
-
- **
|
|
57
|
-
- **Persistent workspace** — all generated ZIPs land in `.contextzip/output/`, discoverable, reusable, and git-ignored automatically
|
|
58
|
+
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip.json`
|
|
59
|
+
- **Persistent workspace** — all generated ZIPs land in `.contextzip/`, discoverable, reusable, and git-ignored automatically
|
|
58
60
|
- **Warns before it's a problem** — flags large (≥ 1 MB) and binary files that AI tools can't read, before you waste an upload
|
|
61
|
+
- **Never packages secrets** — SSH keys, cloud credentials, keystores, Terraform state, and other credential files are always excluded, on top of your `.gitignore` (see [What gets excluded](#what-gets-excluded))
|
|
59
62
|
- **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
|
|
60
63
|
- **Full CLI control** — `--include`, `--exclude`, `--dry-run`, `--output`, all composable
|
|
61
64
|
|
|
@@ -96,7 +99,7 @@ contextzip will:
|
|
|
96
99
|
|
|
97
100
|
1. Detect your framework (e.g. `Next.js + Node.js`)
|
|
98
101
|
2. Apply the appropriate exclusion rules
|
|
99
|
-
3. Create a compressed ZIP in `.contextzip
|
|
102
|
+
3. Create a compressed ZIP in `.contextzip/` at your project root
|
|
100
103
|
4. Open your file manager with the ZIP selected and ready to copy
|
|
101
104
|
|
|
102
105
|
---
|
|
@@ -107,8 +110,6 @@ contextzip will:
|
|
|
107
110
|
contextzip [OPTIONS]
|
|
108
111
|
```
|
|
109
112
|
|
|
110
|
-
> `cz` is a shorthand alias for `contextzip` — both commands are identical and support every option and subcommand shown below (e.g. `cz --git-changes`, `cz exclude node_modules`).
|
|
111
|
-
|
|
112
113
|
| Option | Description |
|
|
113
114
|
|---|---|
|
|
114
115
|
| `-p`, `--prompt TEXT` | Describe your task in plain English — Gemini selects only the relevant files |
|
|
@@ -148,6 +149,9 @@ contextzip --prompt "Refactor auth middleware" --dry-run
|
|
|
148
149
|
|
|
149
150
|
# Save to a custom path
|
|
150
151
|
contextzip --output ~/Desktop/project-context.zip
|
|
152
|
+
|
|
153
|
+
# Apply the ZIP an AI tool handed back
|
|
154
|
+
contextzip apply-zip
|
|
151
155
|
```
|
|
152
156
|
|
|
153
157
|
---
|
|
@@ -173,6 +177,10 @@ with open(pkg.zip_path, "rb") as f:
|
|
|
173
177
|
collection = get_files(include=["src/"], exclude=["tests/"])
|
|
174
178
|
pkg = create_zip(collection, output="/tmp/upload.zip")
|
|
175
179
|
print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
|
|
180
|
+
|
|
181
|
+
# Apply a ZIP an AI tool returned (auto-detects from .contextzip/inbox/)
|
|
182
|
+
result = apply_zip()
|
|
183
|
+
print(f"Wrote {len(result.written)} files, backup at {result.backup_dir}")
|
|
176
184
|
```
|
|
177
185
|
|
|
178
186
|
| Function | Description |
|
|
@@ -180,6 +188,7 @@ print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
|
|
|
180
188
|
| `get_git_changes(path?)` | Modified, added, and untracked files from git |
|
|
181
189
|
| `get_files(path?, include?, exclude?)` | All project files after exclusion rules |
|
|
182
190
|
| `create_zip(collection, output?)` | Write a `FileCollection` to a ZIP archive |
|
|
191
|
+
| `apply_zip(zip_path?, project_dir?, manifest?)` | Apply an AI-returned ZIP back into the project |
|
|
183
192
|
| `detect_ecosystem(path?)` | Detect framework and confidence level |
|
|
184
193
|
|
|
185
194
|
All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, etc.) rather than exiting.
|
|
@@ -210,6 +219,8 @@ contextzip config # show current key status
|
|
|
210
219
|
contextzip config --reset-key # clear and re-run setup
|
|
211
220
|
```
|
|
212
221
|
|
|
222
|
+
Saved keys live in your OS user-config directory (e.g. `~/.config/contextzip/config.json` on Linux/macOS), and that file's permissions are locked down to your user only (`0600`) on every save.
|
|
223
|
+
|
|
213
224
|
---
|
|
214
225
|
|
|
215
226
|
## Terminal error watcher
|
|
@@ -229,7 +240,7 @@ contextzip starts your process normally. You see output exactly as you would wit
|
|
|
229
240
|
╰───────────────────────────────────────────────────╯
|
|
230
241
|
```
|
|
231
242
|
|
|
232
|
-
Press **D** and contextzip immediately writes `.contextzip/
|
|
243
|
+
Press **D** and contextzip immediately writes `.contextzip/debug-context.zip`. Your server keeps running — no restart, no interruption.
|
|
233
244
|
|
|
234
245
|
**What's in the ZIP:**
|
|
235
246
|
|
|
@@ -251,7 +262,19 @@ Press **D** and contextzip immediately writes `.contextzip/output/debug-context.
|
|
|
251
262
|
|
|
252
263
|
contextzip stacks exclusion rules based on your detected stack, on top of your `.gitignore`.
|
|
253
264
|
|
|
254
|
-
**Always excluded:** `.git/`, `.env` files, logs, caches, editor config (`.vscode/`, `.idea/`), OS files (`.DS_Store`, `Thumbs.db`), common binary formats, and contextzip's own `.contextzip/` working folder.
|
|
265
|
+
**Always excluded:** `.git/`, `.env` files, logs, caches, editor config (`.vscode/`, `.idea/`), OS files (`.DS_Store`, `Thumbs.db`), common binary formats, secrets and credential files (see below), and contextzip's own `.contextzip/` working folder.
|
|
266
|
+
|
|
267
|
+
**Secrets & credentials — always excluded, regardless of framework:**
|
|
268
|
+
|
|
269
|
+
| Category | Examples |
|
|
270
|
+
|---|---|
|
|
271
|
+
| SSH private keys | `id_rsa`, `id_dsa`, `id_ecdsa`, `id_ed25519` (public counterparts like `id_rsa.pub` are kept) |
|
|
272
|
+
| Keystores & certs | `*.p12`, `*.pfx`, `*.pkcs12`, `*.jks`, `*.keystore`, `*.ppk`, `*.key` |
|
|
273
|
+
| CLI / package manager credentials | `.npmrc`, `.netrc`, `.pypirc`, `.pgpass`, `.dockercfg`, Docker `config.json` |
|
|
274
|
+
| Cloud provider credentials | `.aws/credentials`, `.aws/config`, `*serviceaccount*.json`, `*credentials*.json`, `kubeconfig` |
|
|
275
|
+
| Infra-as-code state | `*.tfstate`, `*.tfstate.*`, `.terraform/` (Terraform state routinely contains plaintext secrets, even for "just infra" resources) |
|
|
276
|
+
|
|
277
|
+
These patterns are applied on top of your `.gitignore` and can't be re-included, so a stray credential file lying around your project never accidentally ends up in a ZIP you paste into an AI tool.
|
|
255
278
|
|
|
256
279
|
**By framework:**
|
|
257
280
|
|
|
@@ -279,7 +302,7 @@ detects both Node.js and Python from `root/`, without either marker existing at
|
|
|
279
302
|
By default, `.contextzip/` is created at your git root — that's `_find_git_root()` walking up from the current directory until it finds a `.git` folder; outside a repo, it falls back to the current directory. This can be overridden two ways, in order of priority:
|
|
280
303
|
|
|
281
304
|
1. **`CONTEXTZIP_WORKSPACE_LOCATION` environment variable** — for a one-off override on a single run
|
|
282
|
-
2. **A committed `.contextzip
|
|
305
|
+
2. **A committed `.contextzip.json` at the project root** — team-shared, applies to everyone who clones the repo:
|
|
283
306
|
```json
|
|
284
307
|
{ "workspace_location": "git-root" }
|
|
285
308
|
```
|
|
@@ -293,95 +316,25 @@ By default, `.contextzip/` is created at your git root — that's `_find_git_roo
|
|
|
293
316
|
|
|
294
317
|
Accepted values are `"git-root"`, `"cwd"`, or any path (absolute, or relative to the git root). Run `contextzip config` with no flags to see which value is currently active and where it came from.
|
|
295
318
|
|
|
296
|
-
This matters most for monorepos where you sometimes run contextzip from a subdirectory (`cd frontend && contextzip`) — with the default `git-root` setting, the workspace still lands at the repo root no matter where you ran it from, so you don't end up with scattered `.contextzip/` folders across `frontend/`, `backend/`, etc. Set `workspace_location: "cwd"` in a project's `.contextzip
|
|
319
|
+
This matters most for monorepos where you sometimes run contextzip from a subdirectory (`cd frontend && contextzip`) — with the default `git-root` setting, the workspace still lands at the repo root no matter where you ran it from, so you don't end up with scattered `.contextzip/` folders across `frontend/`, `backend/`, etc. Set `workspace_location: "cwd"` in a project's `.contextzip.json` instead if you'd rather each subproject keep its own.
|
|
297
320
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
## Project configuration
|
|
301
|
-
|
|
302
|
-
Every project gets a `.contextzip/` workspace at the project root:
|
|
321
|
+
**Workspace layout:**
|
|
303
322
|
|
|
304
323
|
```
|
|
305
324
|
.contextzip/
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
```json
|
|
319
|
-
{
|
|
320
|
-
"workspace_location": "git-root",
|
|
321
|
-
"scan_depth": null,
|
|
322
|
-
"always_include": [],
|
|
323
|
-
"always_exclude": [],
|
|
324
|
-
"ai": {
|
|
325
|
-
"enabled": true,
|
|
326
|
-
"provider": "gemini",
|
|
327
|
-
"max_files": 10
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
All keys are optional — start with just the ones you need.
|
|
333
|
-
|
|
334
|
-
| Key | What it does |
|
|
335
|
-
| --- | --- |
|
|
336
|
-
| `workspace_location` | Same as `contextzip config --set-workspace`, but team-shared. See [Workspace location](#workspace-location). |
|
|
337
|
-
| `scan_depth` | Reserved for a future bounded-depth scan mode. |
|
|
338
|
-
| `always_include` | A standing "force include" list (gitwildmatch patterns). Files matching it are packaged even if an auto-rule or `.gitignore` would otherwise exclude them — e.g. `["docs/architecture.md"]` to always pull in a doc that lives in an otherwise-excluded folder. |
|
|
339
|
-
| `always_exclude` | A standing exclusion list, applied on every run without retyping `-e`/`--exclude`. |
|
|
340
|
-
| `ai.enabled` | Set to `false` to disable `--prompt` entirely for this project; contextzip will refuse with a clear message instead of silently ignoring it. |
|
|
341
|
-
| `ai.provider` | Reserved for future AI providers. Only `"gemini"` is currently supported. |
|
|
342
|
-
| `ai.max_files` | Caps how many files `--prompt` mode may return, for both the Gemini and keyword-heuristic paths. |
|
|
343
|
-
|
|
344
|
-
`always_include`/`always_exclude` are additive on top of `--include`/`--exclude` for that run — an explicit `contextzip include PATH` still has the final say over what's actually packaged. Persistent preferences belong in `config.json` rather than an ever-growing list of CLI flags; CLI flags stay for one-off, explicit actions (`--dry-run`, `--prompt "…"`, `--output FILE`, etc.).
|
|
345
|
-
|
|
346
|
-
You don't have to hand-write `config.json` — see [Visual config UI](#visual-config-ui) below for a point-and-click way to produce it.
|
|
347
|
-
|
|
348
|
-
### Deprecation: `.contextzip.json`
|
|
349
|
-
|
|
350
|
-
Earlier versions of contextzip read a `.contextzip.json` file at the project root for `workspace_location`/`scan_depth`. That file is now **deprecated** in favor of `.contextzip/config.json` — contextzip still reads it automatically if `.contextzip/config.json` doesn't exist yet (so nothing breaks), and prints a one-time reminder to migrate. To migrate, just move its contents into the `"workspace_location"`/`"scan_depth"` keys of a new `.contextzip/config.json` and delete the old file.
|
|
351
|
-
|
|
352
|
-
`~/.config/contextzip/config.json` (your personal, per-machine config — API key, personal workspace override) is unrelated and unaffected by any of this.
|
|
353
|
-
|
|
354
|
-
---
|
|
355
|
-
|
|
356
|
-
## Visual config UI
|
|
357
|
-
|
|
358
|
-
```
|
|
359
|
-
contextzip config --ui
|
|
325
|
+
config.json # team-shared preferences (committed)
|
|
326
|
+
.gitignore # ignores everything below except itself + config.json
|
|
327
|
+
output/
|
|
328
|
+
codebase.zip # what you generate and send out
|
|
329
|
+
codebase.manifest.json # local-only — never uploaded, used by apply-zip
|
|
330
|
+
inbox/
|
|
331
|
+
<ai-returned>.zip # drop AI-returned zips here for apply-zip to pick up
|
|
332
|
+
applied/
|
|
333
|
+
<timestamp>-name.zip # archived after a successful apply-zip
|
|
334
|
+
backups/
|
|
335
|
+
<timestamp>/ # pre-overwrite copies, one folder per apply-zip run
|
|
360
336
|
```
|
|
361
337
|
|
|
362
|
-
Opens a local browser tab for setting `always_include`/`always_exclude` by clicking through your actual file tree instead of hand-writing patterns — live file counts and packed size update as you go, and one-click chips suggest excluding things like PDFs, office docs, fonts, media, or anything over 1MB that isn't already covered by contextzip's default rules.
|
|
363
|
-
|
|
364
|
-
It's also offered automatically the **first time** you run `contextzip` in a project that has no config at all:
|
|
365
|
-
|
|
366
|
-
```
|
|
367
|
-
$ contextzip
|
|
368
|
-
╭─────────────────────────╮
|
|
369
|
-
│ contextzip v0.3.5 │
|
|
370
|
-
╰─────────────────────────╯
|
|
371
|
-
...
|
|
372
|
-
╭─ First run ─────────────────────────────────────╮
|
|
373
|
-
│ No project config found yet. │
|
|
374
|
-
│ contextzip can open a local browser tab... │
|
|
375
|
-
╰───────────────────────────────────────────────────╯
|
|
376
|
-
Set up include/exclude visually now? [Y/n]:
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
Decline once and it won't ask again (run `contextzip config --ui` any time you want it). It's also skipped automatically for non-interactive runs, when `CI` is set, or when `--prompt`/`--output` are passed — it only ever offers on a plain, interactive, first-ever run.
|
|
380
|
-
|
|
381
|
-
**Nothing about your project leaves your machine.** The server binds to `127.0.0.1` only (never `0.0.0.0`), every request needs a random per-session token embedded in the URL (the same approach Jupyter Notebook uses), and the page itself makes no calls anywhere except back to that local server — no CDN scripts, no web fonts, no analytics. Saving writes straight to `.contextzip/config.json` on disk and the server shuts itself down; if you close the tab without saving, it also shuts down after a short idle period so a forgotten session doesn't linger as an open port.
|
|
382
|
-
|
|
383
|
-
If you accept the first-run offer, the same `contextzip` invocation picks up whatever you saved and finishes packaging immediately — no need to run it again.
|
|
384
|
-
|
|
385
338
|
---
|
|
386
339
|
|
|
387
340
|
## Contributing
|
|
@@ -14,6 +14,8 @@ Every AI session starts the same way: hunt down the relevant files, manually ski
|
|
|
14
14
|
|
|
15
15
|
contextzip eliminates that entirely. Run it from your project root — it detects your stack, applies smart exclusions, produces a lean ZIP, and opens your file manager with the archive already selected. One `Ctrl+C` and you're done.
|
|
16
16
|
|
|
17
|
+
And when the AI tool hands you a ZIP back with the changes, `contextzip apply-zip` closes the loop — no manual unzip-and-hope, no losing track of what actually changed.
|
|
18
|
+
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Features
|
|
@@ -22,11 +24,12 @@ contextzip eliminates that entirely. Run it from your project root — it detect
|
|
|
22
24
|
- **Respects `.gitignore`** — your existing ignore patterns are honoured automatically
|
|
23
25
|
- **Git-aware packaging** — use `--git-changes` to package only modified, staged, and untracked files; perfect for incremental debugging and PR review sessions
|
|
24
26
|
- **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
|
|
27
|
+
- **Apply AI-returned changes back** — `contextzip apply-zip` takes the ZIP an AI tool hands you back and writes it into your project safely: diffed against what was actually sent, backed up before anything is overwritten, and never silently deleting anything
|
|
25
28
|
- **Terminal error watcher** — wrap any dev server with `contextzip watch` to auto-detect errors and package a ready-to-upload debug context in one keypress
|
|
26
|
-
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip
|
|
27
|
-
- **
|
|
28
|
-
- **Persistent workspace** — all generated ZIPs land in `.contextzip/output/`, discoverable, reusable, and git-ignored automatically
|
|
29
|
+
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip.json`
|
|
30
|
+
- **Persistent workspace** — all generated ZIPs land in `.contextzip/`, discoverable, reusable, and git-ignored automatically
|
|
29
31
|
- **Warns before it's a problem** — flags large (≥ 1 MB) and binary files that AI tools can't read, before you waste an upload
|
|
32
|
+
- **Never packages secrets** — SSH keys, cloud credentials, keystores, Terraform state, and other credential files are always excluded, on top of your `.gitignore` (see [What gets excluded](#what-gets-excluded))
|
|
30
33
|
- **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
|
|
31
34
|
- **Full CLI control** — `--include`, `--exclude`, `--dry-run`, `--output`, all composable
|
|
32
35
|
|
|
@@ -67,7 +70,7 @@ contextzip will:
|
|
|
67
70
|
|
|
68
71
|
1. Detect your framework (e.g. `Next.js + Node.js`)
|
|
69
72
|
2. Apply the appropriate exclusion rules
|
|
70
|
-
3. Create a compressed ZIP in `.contextzip
|
|
73
|
+
3. Create a compressed ZIP in `.contextzip/` at your project root
|
|
71
74
|
4. Open your file manager with the ZIP selected and ready to copy
|
|
72
75
|
|
|
73
76
|
---
|
|
@@ -78,8 +81,6 @@ contextzip will:
|
|
|
78
81
|
contextzip [OPTIONS]
|
|
79
82
|
```
|
|
80
83
|
|
|
81
|
-
> `cz` is a shorthand alias for `contextzip` — both commands are identical and support every option and subcommand shown below (e.g. `cz --git-changes`, `cz exclude node_modules`).
|
|
82
|
-
|
|
83
84
|
| Option | Description |
|
|
84
85
|
|---|---|
|
|
85
86
|
| `-p`, `--prompt TEXT` | Describe your task in plain English — Gemini selects only the relevant files |
|
|
@@ -119,6 +120,9 @@ contextzip --prompt "Refactor auth middleware" --dry-run
|
|
|
119
120
|
|
|
120
121
|
# Save to a custom path
|
|
121
122
|
contextzip --output ~/Desktop/project-context.zip
|
|
123
|
+
|
|
124
|
+
# Apply the ZIP an AI tool handed back
|
|
125
|
+
contextzip apply-zip
|
|
122
126
|
```
|
|
123
127
|
|
|
124
128
|
---
|
|
@@ -144,6 +148,10 @@ with open(pkg.zip_path, "rb") as f:
|
|
|
144
148
|
collection = get_files(include=["src/"], exclude=["tests/"])
|
|
145
149
|
pkg = create_zip(collection, output="/tmp/upload.zip")
|
|
146
150
|
print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
|
|
151
|
+
|
|
152
|
+
# Apply a ZIP an AI tool returned (auto-detects from .contextzip/inbox/)
|
|
153
|
+
result = apply_zip()
|
|
154
|
+
print(f"Wrote {len(result.written)} files, backup at {result.backup_dir}")
|
|
147
155
|
```
|
|
148
156
|
|
|
149
157
|
| Function | Description |
|
|
@@ -151,6 +159,7 @@ print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
|
|
|
151
159
|
| `get_git_changes(path?)` | Modified, added, and untracked files from git |
|
|
152
160
|
| `get_files(path?, include?, exclude?)` | All project files after exclusion rules |
|
|
153
161
|
| `create_zip(collection, output?)` | Write a `FileCollection` to a ZIP archive |
|
|
162
|
+
| `apply_zip(zip_path?, project_dir?, manifest?)` | Apply an AI-returned ZIP back into the project |
|
|
154
163
|
| `detect_ecosystem(path?)` | Detect framework and confidence level |
|
|
155
164
|
|
|
156
165
|
All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, etc.) rather than exiting.
|
|
@@ -181,6 +190,8 @@ contextzip config # show current key status
|
|
|
181
190
|
contextzip config --reset-key # clear and re-run setup
|
|
182
191
|
```
|
|
183
192
|
|
|
193
|
+
Saved keys live in your OS user-config directory (e.g. `~/.config/contextzip/config.json` on Linux/macOS), and that file's permissions are locked down to your user only (`0600`) on every save.
|
|
194
|
+
|
|
184
195
|
---
|
|
185
196
|
|
|
186
197
|
## Terminal error watcher
|
|
@@ -200,7 +211,7 @@ contextzip starts your process normally. You see output exactly as you would wit
|
|
|
200
211
|
╰───────────────────────────────────────────────────╯
|
|
201
212
|
```
|
|
202
213
|
|
|
203
|
-
Press **D** and contextzip immediately writes `.contextzip/
|
|
214
|
+
Press **D** and contextzip immediately writes `.contextzip/debug-context.zip`. Your server keeps running — no restart, no interruption.
|
|
204
215
|
|
|
205
216
|
**What's in the ZIP:**
|
|
206
217
|
|
|
@@ -222,7 +233,19 @@ Press **D** and contextzip immediately writes `.contextzip/output/debug-context.
|
|
|
222
233
|
|
|
223
234
|
contextzip stacks exclusion rules based on your detected stack, on top of your `.gitignore`.
|
|
224
235
|
|
|
225
|
-
**Always excluded:** `.git/`, `.env` files, logs, caches, editor config (`.vscode/`, `.idea/`), OS files (`.DS_Store`, `Thumbs.db`), common binary formats, and contextzip's own `.contextzip/` working folder.
|
|
236
|
+
**Always excluded:** `.git/`, `.env` files, logs, caches, editor config (`.vscode/`, `.idea/`), OS files (`.DS_Store`, `Thumbs.db`), common binary formats, secrets and credential files (see below), and contextzip's own `.contextzip/` working folder.
|
|
237
|
+
|
|
238
|
+
**Secrets & credentials — always excluded, regardless of framework:**
|
|
239
|
+
|
|
240
|
+
| Category | Examples |
|
|
241
|
+
|---|---|
|
|
242
|
+
| SSH private keys | `id_rsa`, `id_dsa`, `id_ecdsa`, `id_ed25519` (public counterparts like `id_rsa.pub` are kept) |
|
|
243
|
+
| Keystores & certs | `*.p12`, `*.pfx`, `*.pkcs12`, `*.jks`, `*.keystore`, `*.ppk`, `*.key` |
|
|
244
|
+
| CLI / package manager credentials | `.npmrc`, `.netrc`, `.pypirc`, `.pgpass`, `.dockercfg`, Docker `config.json` |
|
|
245
|
+
| Cloud provider credentials | `.aws/credentials`, `.aws/config`, `*serviceaccount*.json`, `*credentials*.json`, `kubeconfig` |
|
|
246
|
+
| Infra-as-code state | `*.tfstate`, `*.tfstate.*`, `.terraform/` (Terraform state routinely contains plaintext secrets, even for "just infra" resources) |
|
|
247
|
+
|
|
248
|
+
These patterns are applied on top of your `.gitignore` and can't be re-included, so a stray credential file lying around your project never accidentally ends up in a ZIP you paste into an AI tool.
|
|
226
249
|
|
|
227
250
|
**By framework:**
|
|
228
251
|
|
|
@@ -250,7 +273,7 @@ detects both Node.js and Python from `root/`, without either marker existing at
|
|
|
250
273
|
By default, `.contextzip/` is created at your git root — that's `_find_git_root()` walking up from the current directory until it finds a `.git` folder; outside a repo, it falls back to the current directory. This can be overridden two ways, in order of priority:
|
|
251
274
|
|
|
252
275
|
1. **`CONTEXTZIP_WORKSPACE_LOCATION` environment variable** — for a one-off override on a single run
|
|
253
|
-
2. **A committed `.contextzip
|
|
276
|
+
2. **A committed `.contextzip.json` at the project root** — team-shared, applies to everyone who clones the repo:
|
|
254
277
|
```json
|
|
255
278
|
{ "workspace_location": "git-root" }
|
|
256
279
|
```
|
|
@@ -264,95 +287,25 @@ By default, `.contextzip/` is created at your git root — that's `_find_git_roo
|
|
|
264
287
|
|
|
265
288
|
Accepted values are `"git-root"`, `"cwd"`, or any path (absolute, or relative to the git root). Run `contextzip config` with no flags to see which value is currently active and where it came from.
|
|
266
289
|
|
|
267
|
-
This matters most for monorepos where you sometimes run contextzip from a subdirectory (`cd frontend && contextzip`) — with the default `git-root` setting, the workspace still lands at the repo root no matter where you ran it from, so you don't end up with scattered `.contextzip/` folders across `frontend/`, `backend/`, etc. Set `workspace_location: "cwd"` in a project's `.contextzip
|
|
290
|
+
This matters most for monorepos where you sometimes run contextzip from a subdirectory (`cd frontend && contextzip`) — with the default `git-root` setting, the workspace still lands at the repo root no matter where you ran it from, so you don't end up with scattered `.contextzip/` folders across `frontend/`, `backend/`, etc. Set `workspace_location: "cwd"` in a project's `.contextzip.json` instead if you'd rather each subproject keep its own.
|
|
268
291
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
## Project configuration
|
|
272
|
-
|
|
273
|
-
Every project gets a `.contextzip/` workspace at the project root:
|
|
292
|
+
**Workspace layout:**
|
|
274
293
|
|
|
275
294
|
```
|
|
276
295
|
.contextzip/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
```json
|
|
290
|
-
{
|
|
291
|
-
"workspace_location": "git-root",
|
|
292
|
-
"scan_depth": null,
|
|
293
|
-
"always_include": [],
|
|
294
|
-
"always_exclude": [],
|
|
295
|
-
"ai": {
|
|
296
|
-
"enabled": true,
|
|
297
|
-
"provider": "gemini",
|
|
298
|
-
"max_files": 10
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
All keys are optional — start with just the ones you need.
|
|
304
|
-
|
|
305
|
-
| Key | What it does |
|
|
306
|
-
| --- | --- |
|
|
307
|
-
| `workspace_location` | Same as `contextzip config --set-workspace`, but team-shared. See [Workspace location](#workspace-location). |
|
|
308
|
-
| `scan_depth` | Reserved for a future bounded-depth scan mode. |
|
|
309
|
-
| `always_include` | A standing "force include" list (gitwildmatch patterns). Files matching it are packaged even if an auto-rule or `.gitignore` would otherwise exclude them — e.g. `["docs/architecture.md"]` to always pull in a doc that lives in an otherwise-excluded folder. |
|
|
310
|
-
| `always_exclude` | A standing exclusion list, applied on every run without retyping `-e`/`--exclude`. |
|
|
311
|
-
| `ai.enabled` | Set to `false` to disable `--prompt` entirely for this project; contextzip will refuse with a clear message instead of silently ignoring it. |
|
|
312
|
-
| `ai.provider` | Reserved for future AI providers. Only `"gemini"` is currently supported. |
|
|
313
|
-
| `ai.max_files` | Caps how many files `--prompt` mode may return, for both the Gemini and keyword-heuristic paths. |
|
|
314
|
-
|
|
315
|
-
`always_include`/`always_exclude` are additive on top of `--include`/`--exclude` for that run — an explicit `contextzip include PATH` still has the final say over what's actually packaged. Persistent preferences belong in `config.json` rather than an ever-growing list of CLI flags; CLI flags stay for one-off, explicit actions (`--dry-run`, `--prompt "…"`, `--output FILE`, etc.).
|
|
316
|
-
|
|
317
|
-
You don't have to hand-write `config.json` — see [Visual config UI](#visual-config-ui) below for a point-and-click way to produce it.
|
|
318
|
-
|
|
319
|
-
### Deprecation: `.contextzip.json`
|
|
320
|
-
|
|
321
|
-
Earlier versions of contextzip read a `.contextzip.json` file at the project root for `workspace_location`/`scan_depth`. That file is now **deprecated** in favor of `.contextzip/config.json` — contextzip still reads it automatically if `.contextzip/config.json` doesn't exist yet (so nothing breaks), and prints a one-time reminder to migrate. To migrate, just move its contents into the `"workspace_location"`/`"scan_depth"` keys of a new `.contextzip/config.json` and delete the old file.
|
|
322
|
-
|
|
323
|
-
`~/.config/contextzip/config.json` (your personal, per-machine config — API key, personal workspace override) is unrelated and unaffected by any of this.
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
## Visual config UI
|
|
328
|
-
|
|
329
|
-
```
|
|
330
|
-
contextzip config --ui
|
|
296
|
+
config.json # team-shared preferences (committed)
|
|
297
|
+
.gitignore # ignores everything below except itself + config.json
|
|
298
|
+
output/
|
|
299
|
+
codebase.zip # what you generate and send out
|
|
300
|
+
codebase.manifest.json # local-only — never uploaded, used by apply-zip
|
|
301
|
+
inbox/
|
|
302
|
+
<ai-returned>.zip # drop AI-returned zips here for apply-zip to pick up
|
|
303
|
+
applied/
|
|
304
|
+
<timestamp>-name.zip # archived after a successful apply-zip
|
|
305
|
+
backups/
|
|
306
|
+
<timestamp>/ # pre-overwrite copies, one folder per apply-zip run
|
|
331
307
|
```
|
|
332
308
|
|
|
333
|
-
Opens a local browser tab for setting `always_include`/`always_exclude` by clicking through your actual file tree instead of hand-writing patterns — live file counts and packed size update as you go, and one-click chips suggest excluding things like PDFs, office docs, fonts, media, or anything over 1MB that isn't already covered by contextzip's default rules.
|
|
334
|
-
|
|
335
|
-
It's also offered automatically the **first time** you run `contextzip` in a project that has no config at all:
|
|
336
|
-
|
|
337
|
-
```
|
|
338
|
-
$ contextzip
|
|
339
|
-
╭─────────────────────────╮
|
|
340
|
-
│ contextzip v0.3.5 │
|
|
341
|
-
╰─────────────────────────╯
|
|
342
|
-
...
|
|
343
|
-
╭─ First run ─────────────────────────────────────╮
|
|
344
|
-
│ No project config found yet. │
|
|
345
|
-
│ contextzip can open a local browser tab... │
|
|
346
|
-
╰───────────────────────────────────────────────────╯
|
|
347
|
-
Set up include/exclude visually now? [Y/n]:
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Decline once and it won't ask again (run `contextzip config --ui` any time you want it). It's also skipped automatically for non-interactive runs, when `CI` is set, or when `--prompt`/`--output` are passed — it only ever offers on a plain, interactive, first-ever run.
|
|
351
|
-
|
|
352
|
-
**Nothing about your project leaves your machine.** The server binds to `127.0.0.1` only (never `0.0.0.0`), every request needs a random per-session token embedded in the URL (the same approach Jupyter Notebook uses), and the page itself makes no calls anywhere except back to that local server — no CDN scripts, no web fonts, no analytics. Saving writes straight to `.contextzip/config.json` on disk and the server shuts itself down; if you close the tab without saving, it also shuts down after a short idle period so a forgotten session doesn't linger as an open port.
|
|
353
|
-
|
|
354
|
-
If you accept the first-run offer, the same `contextzip` invocation picks up whatever you saved and finishes packaging immediately — no need to run it again.
|
|
355
|
-
|
|
356
309
|
---
|
|
357
310
|
|
|
358
311
|
## Contributing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""contextzip — intelligent codebase packager for AI tools."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.3.
|
|
3
|
+
__version__ = "0.3.9"
|
|
4
4
|
|
|
5
5
|
from contextzip.api import (
|
|
6
6
|
FileCollection,
|
|
@@ -9,26 +9,32 @@ from contextzip.api import (
|
|
|
9
9
|
GitNotFoundError,
|
|
10
10
|
GitCommandError,
|
|
11
11
|
NoFilesError,
|
|
12
|
+
ZipNotFoundError,
|
|
12
13
|
get_git_changes,
|
|
13
14
|
get_files,
|
|
14
15
|
create_zip,
|
|
16
|
+
apply_zip,
|
|
15
17
|
detect_ecosystem,
|
|
16
18
|
)
|
|
17
19
|
from contextzip.packager import PackageResult
|
|
20
|
+
from contextzip.applier import ApplyResult
|
|
18
21
|
|
|
19
22
|
__all__ = [
|
|
20
23
|
# Functions
|
|
21
24
|
"get_git_changes",
|
|
22
25
|
"get_files",
|
|
23
26
|
"create_zip",
|
|
27
|
+
"apply_zip",
|
|
24
28
|
"detect_ecosystem",
|
|
25
29
|
# Data types
|
|
26
30
|
"FileCollection",
|
|
27
31
|
"PackageResult",
|
|
32
|
+
"ApplyResult",
|
|
28
33
|
# Exceptions
|
|
29
34
|
"ContextzipError",
|
|
30
35
|
"NotARepositoryError",
|
|
31
36
|
"GitNotFoundError",
|
|
32
37
|
"GitCommandError",
|
|
33
38
|
"NoFilesError",
|
|
39
|
+
"ZipNotFoundError",
|
|
34
40
|
]
|
|
@@ -39,6 +39,7 @@ import os
|
|
|
39
39
|
from dataclasses import dataclass, field
|
|
40
40
|
from pathlib import Path
|
|
41
41
|
|
|
42
|
+
from contextzip.applier import ApplyResult
|
|
42
43
|
from contextzip.detector import DetectionResult, detect
|
|
43
44
|
from contextzip.filters import (
|
|
44
45
|
ResolveResult,
|
|
@@ -46,7 +47,7 @@ from contextzip.filters import (
|
|
|
46
47
|
resolve_files,
|
|
47
48
|
resolve_files_from_git,
|
|
48
49
|
)
|
|
49
|
-
from contextzip.git import
|
|
50
|
+
from contextzip.git import GitError, GitErrorKind, get_changed_files
|
|
50
51
|
from contextzip.packager import PackageResult, create_zip_silent
|
|
51
52
|
|
|
52
53
|
|
|
@@ -127,6 +128,10 @@ class NoFilesError(ContextzipError):
|
|
|
127
128
|
"""Raised when file resolution produces an empty result."""
|
|
128
129
|
|
|
129
130
|
|
|
131
|
+
class ZipNotFoundError(ContextzipError):
|
|
132
|
+
"""Raised when apply_zip() can't resolve which zip to apply."""
|
|
133
|
+
|
|
134
|
+
|
|
130
135
|
# ---------------------------------------------------------------------------
|
|
131
136
|
# Public API
|
|
132
137
|
# ---------------------------------------------------------------------------
|
|
@@ -335,6 +340,82 @@ def create_zip(
|
|
|
335
340
|
)
|
|
336
341
|
|
|
337
342
|
|
|
343
|
+
def apply_zip(
|
|
344
|
+
zip_path: str | Path | None = None,
|
|
345
|
+
*,
|
|
346
|
+
project_dir: str | Path | None = None,
|
|
347
|
+
manifest: str | Path | None = None,
|
|
348
|
+
) -> ApplyResult:
|
|
349
|
+
"""
|
|
350
|
+
Apply an AI-returned ZIP back into *project_dir* (defaults to cwd).
|
|
351
|
+
|
|
352
|
+
Diffs the zip against the local sidecar manifest most recently written
|
|
353
|
+
to ``.contextzip/output/`` (or the one at *manifest*, if given) to
|
|
354
|
+
classify every file as new, modified, unchanged, or one with no safe
|
|
355
|
+
baseline (locally edited since zipping, or never part of the original
|
|
356
|
+
manifest). Only adds and modifies files — nothing is ever deleted.
|
|
357
|
+
Every overwritten file is backed up first, under
|
|
358
|
+
``.contextzip/backups/<timestamp>/``.
|
|
359
|
+
|
|
360
|
+
Unlike the CLI, this applies immediately and does not prompt — check
|
|
361
|
+
``contextzip.applier.build_plan()`` yourself first if you want to
|
|
362
|
+
inspect risk before writing (see ``ApplyPlan.is_risky`` /
|
|
363
|
+
``.risky_entries``).
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
zip_path:
|
|
368
|
+
Path to the returned zip. If omitted, auto-detects from
|
|
369
|
+
``.contextzip/inbox/`` — exactly one zip must be present there.
|
|
370
|
+
project_dir:
|
|
371
|
+
Project root to apply into. Defaults to ``Path.cwd()``.
|
|
372
|
+
manifest:
|
|
373
|
+
Explicit manifest path to diff against, overriding auto-detection.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
ApplyResult
|
|
378
|
+
``written`` (list of relative paths written), ``backup_dir``
|
|
379
|
+
(``Path`` or ``None`` if nothing needed backing up), and
|
|
380
|
+
``applied_zip_path`` (where the consumed zip ended up).
|
|
381
|
+
|
|
382
|
+
Raises
|
|
383
|
+
------
|
|
384
|
+
ZipNotFoundError
|
|
385
|
+
If no zip could be resolved — missing, or multiple candidates in
|
|
386
|
+
the inbox with none specified.
|
|
387
|
+
|
|
388
|
+
Example
|
|
389
|
+
-------
|
|
390
|
+
::
|
|
391
|
+
|
|
392
|
+
from contextzip import apply_zip
|
|
393
|
+
|
|
394
|
+
result = apply_zip() # picks up .contextzip/inbox/*.zip
|
|
395
|
+
print(f"Wrote {len(result.written)} files")
|
|
396
|
+
if result.backup_dir:
|
|
397
|
+
print(f"Backup at {result.backup_dir}")
|
|
398
|
+
"""
|
|
399
|
+
from contextzip.applier import (
|
|
400
|
+
ApplyError,
|
|
401
|
+
build_plan,
|
|
402
|
+
execute_plan,
|
|
403
|
+
find_latest_manifest,
|
|
404
|
+
find_zip_to_apply,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
pdir = _resolve_dir(project_dir)
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
resolved_zip = find_zip_to_apply(pdir, zip_path)
|
|
411
|
+
except ApplyError as exc:
|
|
412
|
+
raise ZipNotFoundError(str(exc)) from exc
|
|
413
|
+
|
|
414
|
+
manifest_path = find_latest_manifest(pdir, manifest)
|
|
415
|
+
plan = build_plan(resolved_zip, pdir, manifest_path)
|
|
416
|
+
return execute_plan(plan, pdir)
|
|
417
|
+
|
|
418
|
+
|
|
338
419
|
def detect_ecosystem(
|
|
339
420
|
path: str | Path | None = None,
|
|
340
421
|
) -> DetectionResult:
|