contextzip 0.3.6__tar.gz → 0.3.8__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.
Files changed (46) hide show
  1. {contextzip-0.3.6 → contextzip-0.3.8}/PKG-INFO +87 -62
  2. {contextzip-0.3.6 → contextzip-0.3.8}/README.md +86 -61
  3. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/__init__.py +7 -1
  4. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/api.py +81 -0
  5. contextzip-0.3.8/contextzip/applier.py +496 -0
  6. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/cli.py +240 -1
  7. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/cli_display.py +92 -0
  8. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/config.py +30 -0
  9. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/filters.py +145 -2
  10. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/packager.py +47 -0
  11. contextzip-0.3.8/contextzip/webui/__init__.py +9 -0
  12. contextzip-0.3.8/contextzip/webui/assets.py +1012 -0
  13. contextzip-0.3.8/contextzip/webui/persist.py +59 -0
  14. contextzip-0.3.8/contextzip/webui/server.py +426 -0
  15. contextzip-0.3.8/contextzip/webui/suggestions.py +136 -0
  16. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/PKG-INFO +87 -62
  17. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/SOURCES.txt +7 -1
  18. {contextzip-0.3.6 → contextzip-0.3.8}/pyproject.toml +1 -1
  19. {contextzip-0.3.6 → contextzip-0.3.8}/LICENSE +0 -0
  20. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/ai/__init__.py +0 -0
  21. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/ai/gemini.py +0 -0
  22. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/ai/heuristic.py +0 -0
  23. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/ai/selector.py +0 -0
  24. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/cli_ai.py +0 -0
  25. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/cli_onboard.py +0 -0
  26. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/clipboard.py +0 -0
  27. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/detector.py +0 -0
  28. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/error_parser.py +0 -0
  29. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/git.py +0 -0
  30. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/project_config.py +0 -0
  31. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/__init__.py +0 -0
  32. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/base.py +0 -0
  33. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/errors/__init__.py +0 -0
  34. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/errors/node.py +0 -0
  35. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/errors/python.py +0 -0
  36. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/go.py +0 -0
  37. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/node.py +0 -0
  38. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/python.py +0 -0
  39. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/ruby.py +0 -0
  40. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/rust.py +0 -0
  41. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/watcher.py +0 -0
  42. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/dependency_links.txt +0 -0
  43. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/entry_points.txt +0 -0
  44. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/requires.txt +0 -0
  45. {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/top_level.txt +0 -0
  46. {contextzip-0.3.6 → contextzip-0.3.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextzip
3
- Version: 0.3.6
3
+ Version: 0.3.8
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,9 +53,10 @@ 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/config.json`
56
- - **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
57
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
58
61
  - **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
59
62
  - **Full CLI control** — `--include`, `--exclude`, `--dry-run`, `--output`, all composable
@@ -95,7 +98,7 @@ contextzip will:
95
98
 
96
99
  1. Detect your framework (e.g. `Next.js + Node.js`)
97
100
  2. Apply the appropriate exclusion rules
98
- 3. Create a compressed ZIP in `.contextzip/output/` at your project root
101
+ 3. Create a compressed ZIP in `.contextzip/` at your project root
99
102
  4. Open your file manager with the ZIP selected and ready to copy
100
103
 
101
104
  ---
@@ -106,8 +109,6 @@ contextzip will:
106
109
  contextzip [OPTIONS]
107
110
  ```
108
111
 
109
- > `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`).
110
-
111
112
  | Option | Description |
112
113
  |---|---|
113
114
  | `-p`, `--prompt TEXT` | Describe your task in plain English — Gemini selects only the relevant files |
@@ -120,7 +121,7 @@ contextzip [OPTIONS]
120
121
  | `--no-clipboard` | Skip the clipboard / folder-open step |
121
122
  | `--no-gitignore` | Ignore the project's `.gitignore` |
122
123
 
123
- **Subcommands:** `exclude`, `include`, `watch`, `config` — run `contextzip --help` for full details.
124
+ **Subcommands:** `exclude`, `include`, `apply-zip`, `watch`, `config` — run `contextzip --help` for full details.
124
125
 
125
126
  ---
126
127
 
@@ -147,6 +148,9 @@ contextzip --prompt "Refactor auth middleware" --dry-run
147
148
 
148
149
  # Save to a custom path
149
150
  contextzip --output ~/Desktop/project-context.zip
151
+
152
+ # Apply the ZIP an AI tool handed back
153
+ contextzip apply-zip
150
154
  ```
151
155
 
152
156
  ---
@@ -156,7 +160,7 @@ contextzip --output ~/Desktop/project-context.zip
156
160
  contextzip is also usable as a library. All CLI capabilities are available as plain Python functions — no Click, no Rich output, no `SystemExit`.
157
161
 
158
162
  ```python
159
- from contextzip import get_git_changes, get_files, create_zip
163
+ from contextzip import get_git_changes, get_files, create_zip, apply_zip
160
164
 
161
165
  # Get changed files and use them directly
162
166
  collection = get_git_changes()
@@ -172,6 +176,10 @@ with open(pkg.zip_path, "rb") as f:
172
176
  collection = get_files(include=["src/"], exclude=["tests/"])
173
177
  pkg = create_zip(collection, output="/tmp/upload.zip")
174
178
  print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
179
+
180
+ # Apply a ZIP an AI tool returned (auto-detects from .contextzip/inbox/)
181
+ result = apply_zip()
182
+ print(f"Wrote {len(result.written)} files, backup at {result.backup_dir}")
175
183
  ```
176
184
 
177
185
  | Function | Description |
@@ -179,9 +187,10 @@ print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
179
187
  | `get_git_changes(path?)` | Modified, added, and untracked files from git |
180
188
  | `get_files(path?, include?, exclude?)` | All project files after exclusion rules |
181
189
  | `create_zip(collection, output?)` | Write a `FileCollection` to a ZIP archive |
190
+ | `apply_zip(zip_path?, project_dir?, manifest?)` | Apply an AI-returned ZIP back into the project |
182
191
  | `detect_ecosystem(path?)` | Detect framework and confidence level |
183
192
 
184
- All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, etc.) rather than exiting.
193
+ All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, `ZipNotFoundError`, etc.) rather than exiting.
185
194
 
186
195
  ---
187
196
 
@@ -211,6 +220,61 @@ contextzip config --reset-key # clear and re-run setup
211
220
 
212
221
  ---
213
222
 
223
+ ## Applying AI-returned changes
224
+
225
+ Once an AI tool has made its edits and handed you back a ZIP, `contextzip apply-zip` writes those changes into your project — safely.
226
+
227
+ ```bash
228
+ # Drop the AI's returned zip into .contextzip/inbox/, then:
229
+ contextzip apply-zip
230
+
231
+ # Or point at it directly, wherever it landed:
232
+ contextzip apply-zip ~/Downloads/fixed-project.zip
233
+
234
+ # Preview first, with full per-file detail:
235
+ contextzip apply-zip --dry-run --verbose
236
+
237
+ # Skip the confirmation prompt (e.g. in a script):
238
+ contextzip apply-zip --yes
239
+ ```
240
+
241
+ ### How it knows what's safe to apply
242
+
243
+ Every ZIP `contextzip` creates gets a small manifest written next to it in `.contextzip/output/` — a hash of each included file, taken at zip-time. This manifest is **never added to the ZIP itself**. It stays local, so it's never uploaded and never visible to whatever AI tool the ZIP is pasted into — nothing for a model, or a curious teammate, to notice or ask about.
244
+
245
+ When you run `apply-zip`, it diffs the returned ZIP against that manifest and classifies every file:
246
+
247
+ | Status | Meaning | Applied automatically? |
248
+ |---|---|---|
249
+ | **New** | Wasn't part of the original ZIP | Yes |
250
+ | **Modified** | Was sent, content changed, and your local copy hasn't moved since | Yes |
251
+ | **Unchanged** | Identical to what's already on disk | Skipped — nothing to do |
252
+ | **Drifted** | Your local file changed (or was deleted) since you zipped it | No — flagged, asks first |
253
+ | **Untracked** | In the returned ZIP but has no baseline to compare against | No — flagged, asks first |
254
+
255
+ The common case — you zip some files, the AI edits them, nothing else touched your project meanwhile — applies straight through with just a summary printed, no prompt. Anything that could clobber your own work, or introduce a path you didn't expect, stops and asks before writing.
256
+
257
+ If a ZIP arrives with no matching manifest at all (e.g. it wasn't produced by a `contextzip` round trip), every already-existing path is treated as untracked and you'll be asked to confirm the whole batch.
258
+
259
+ ### What it does and doesn't do
260
+
261
+ - **Only adds and modifies files.** Deletions are never inferred from a ZIP's contents — a file that's simply missing from the returned ZIP is left alone.
262
+ - **Backs up before overwriting.** Every file about to be touched is copied into `.contextzip/backups/<timestamp>/` first, preserving its relative path.
263
+ - **Rejects unsafe paths outright.** Any ZIP entry that would resolve outside your project (`../../etc/passwd`-style path traversal) is refused before anything is written — no override flag, no exceptions.
264
+ - **Archives what it consumes.** Once applied, the ZIP is moved to `.contextzip/inbox/applied/<timestamp>-name.zip` — it won't be picked up again by accident, and stays around as an audit trail. ZIPs applied via an explicit path outside the inbox are left where you put them.
265
+
266
+ ### Finding the right ZIP and manifest
267
+
268
+ ```bash
269
+ contextzip apply-zip # exactly one *.zip in .contextzip/inbox/ → uses it
270
+ contextzip apply-zip path/to/fix.zip # explicit path always overrides the inbox
271
+ contextzip apply-zip --manifest path/to/codebase.manifest.json # override auto-detection
272
+ ```
273
+
274
+ If `.contextzip/inbox/` has more than one ZIP and you don't specify which, `apply-zip` lists them and asks you to be explicit rather than guessing. Manifest auto-detection picks the most recently created `*.manifest.json` under `.contextzip/output/` — correct for the normal one-round-trip-at-a-time flow; pass `--manifest` explicitly if you've generated several ZIPs before getting a response back.
275
+
276
+ ---
277
+
214
278
  ## Terminal error watcher
215
279
 
216
280
  The `watch` command wraps your dev server, buffers its output, and packages a debug-ready ZIP the moment you spot an error — no manual file hunting, no copy-pasting stack traces.
@@ -278,7 +342,7 @@ detects both Node.js and Python from `root/`, without either marker existing at
278
342
  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:
279
343
 
280
344
  1. **`CONTEXTZIP_WORKSPACE_LOCATION` environment variable** — for a one-off override on a single run
281
- 2. **A committed `.contextzip/config.json` at the project root** — team-shared, applies to everyone who clones the repo:
345
+ 2. **A committed `.contextzip.json` at the project root** — team-shared, applies to everyone who clones the repo:
282
346
  ```json
283
347
  { "workspace_location": "git-root" }
284
348
  ```
@@ -292,64 +356,25 @@ By default, `.contextzip/` is created at your git root — that's `_find_git_roo
292
356
 
293
357
  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.
294
358
 
295
- 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/config.json` instead if you'd rather each subproject keep its own.
296
-
297
- ---
298
-
299
- ## Project configuration
359
+ 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.
300
360
 
301
- Every project gets a `.contextzip/` workspace at the project root:
361
+ **Workspace layout:**
302
362
 
303
363
  ```
304
364
  .contextzip/
305
- ├── config.json # project preferences — commit this
306
- ├── .gitignore # keeps output/ untracked, config.json trackable
307
- └── output/ # generated ZIPs — never committed
308
- ├── codebase.zip
309
- ├── changes.zip (--git-changes)
310
- └── debug-context.zip (contextzip watch)
365
+ config.json # team-shared preferences (committed)
366
+ .gitignore # ignores everything below except itself + config.json
367
+ output/
368
+ codebase.zip # what you generate and send out
369
+ codebase.manifest.json # local-only — never uploaded, used by apply-zip
370
+ inbox/
371
+ <ai-returned>.zip # drop AI-returned zips here for apply-zip to pick up
372
+ applied/
373
+ <timestamp>-name.zip # archived after a successful apply-zip
374
+ backups/
375
+ <timestamp>/ # pre-overwrite copies, one folder per apply-zip run
311
376
  ```
312
377
 
313
- `.contextzip/` is **not** committed to Git by default — `.contextzip/.gitignore` is written automatically the first time you run contextzip in a repo, and it ignores everything in the folder *except* `config.json` and itself. That's deliberate: `output/` is per-machine, disposable scratch space, while `config.json` holds team-shared preferences you'll usually want everyone on the same page about.
314
-
315
- `config.json` currently supports:
316
-
317
- ```json
318
- {
319
- "workspace_location": "git-root",
320
- "scan_depth": null,
321
- "always_include": [],
322
- "always_exclude": [],
323
- "ai": {
324
- "enabled": true,
325
- "provider": "gemini",
326
- "max_files": 10
327
- }
328
- }
329
- ```
330
-
331
- All keys are optional — start with just the ones you need.
332
-
333
- | Key | What it does |
334
- | --- | --- |
335
- | `workspace_location` | Same as `contextzip config --set-workspace`, but team-shared. See [Workspace location](#workspace-location). |
336
- | `scan_depth` | Reserved for a future bounded-depth scan mode. |
337
- | `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. |
338
- | `always_exclude` | A standing exclusion list, applied on every run without retyping `-e`/`--exclude`. |
339
- | `ai.enabled` | Set to `false` to disable `--prompt` entirely for this project; contextzip will refuse with a clear message instead of silently ignoring it. |
340
- | `ai.provider` | Reserved for future AI providers. Only `"gemini"` is currently supported. |
341
- | `ai.max_files` | Caps how many files `--prompt` mode may return, for both the Gemini and keyword-heuristic paths. |
342
-
343
- `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.).
344
-
345
- A **web-based configuration UI** that generates `config.json` for you is on the roadmap — the schema above is designed to grow additively, so future preferences (including ones set visually) won't require another migration.
346
-
347
- ### Deprecation: `.contextzip.json`
348
-
349
- 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.
350
-
351
- `~/.config/contextzip/config.json` (your personal, per-machine config — API key, personal workspace override) is unrelated and unaffected by any of this.
352
-
353
378
  ---
354
379
 
355
380
  ## 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,9 +24,10 @@ 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/config.json`
27
- - **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
28
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
29
32
  - **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
30
33
  - **Full CLI control** — `--include`, `--exclude`, `--dry-run`, `--output`, all composable
@@ -66,7 +69,7 @@ contextzip will:
66
69
 
67
70
  1. Detect your framework (e.g. `Next.js + Node.js`)
68
71
  2. Apply the appropriate exclusion rules
69
- 3. Create a compressed ZIP in `.contextzip/output/` at your project root
72
+ 3. Create a compressed ZIP in `.contextzip/` at your project root
70
73
  4. Open your file manager with the ZIP selected and ready to copy
71
74
 
72
75
  ---
@@ -77,8 +80,6 @@ contextzip will:
77
80
  contextzip [OPTIONS]
78
81
  ```
79
82
 
80
- > `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`).
81
-
82
83
  | Option | Description |
83
84
  |---|---|
84
85
  | `-p`, `--prompt TEXT` | Describe your task in plain English — Gemini selects only the relevant files |
@@ -91,7 +92,7 @@ contextzip [OPTIONS]
91
92
  | `--no-clipboard` | Skip the clipboard / folder-open step |
92
93
  | `--no-gitignore` | Ignore the project's `.gitignore` |
93
94
 
94
- **Subcommands:** `exclude`, `include`, `watch`, `config` — run `contextzip --help` for full details.
95
+ **Subcommands:** `exclude`, `include`, `apply-zip`, `watch`, `config` — run `contextzip --help` for full details.
95
96
 
96
97
  ---
97
98
 
@@ -118,6 +119,9 @@ contextzip --prompt "Refactor auth middleware" --dry-run
118
119
 
119
120
  # Save to a custom path
120
121
  contextzip --output ~/Desktop/project-context.zip
122
+
123
+ # Apply the ZIP an AI tool handed back
124
+ contextzip apply-zip
121
125
  ```
122
126
 
123
127
  ---
@@ -127,7 +131,7 @@ contextzip --output ~/Desktop/project-context.zip
127
131
  contextzip is also usable as a library. All CLI capabilities are available as plain Python functions — no Click, no Rich output, no `SystemExit`.
128
132
 
129
133
  ```python
130
- from contextzip import get_git_changes, get_files, create_zip
134
+ from contextzip import get_git_changes, get_files, create_zip, apply_zip
131
135
 
132
136
  # Get changed files and use them directly
133
137
  collection = get_git_changes()
@@ -143,6 +147,10 @@ with open(pkg.zip_path, "rb") as f:
143
147
  collection = get_files(include=["src/"], exclude=["tests/"])
144
148
  pkg = create_zip(collection, output="/tmp/upload.zip")
145
149
  print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
150
+
151
+ # Apply a ZIP an AI tool returned (auto-detects from .contextzip/inbox/)
152
+ result = apply_zip()
153
+ print(f"Wrote {len(result.written)} files, backup at {result.backup_dir}")
146
154
  ```
147
155
 
148
156
  | Function | Description |
@@ -150,9 +158,10 @@ print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
150
158
  | `get_git_changes(path?)` | Modified, added, and untracked files from git |
151
159
  | `get_files(path?, include?, exclude?)` | All project files after exclusion rules |
152
160
  | `create_zip(collection, output?)` | Write a `FileCollection` to a ZIP archive |
161
+ | `apply_zip(zip_path?, project_dir?, manifest?)` | Apply an AI-returned ZIP back into the project |
153
162
  | `detect_ecosystem(path?)` | Detect framework and confidence level |
154
163
 
155
- All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, etc.) rather than exiting.
164
+ All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, `ZipNotFoundError`, etc.) rather than exiting.
156
165
 
157
166
  ---
158
167
 
@@ -182,6 +191,61 @@ contextzip config --reset-key # clear and re-run setup
182
191
 
183
192
  ---
184
193
 
194
+ ## Applying AI-returned changes
195
+
196
+ Once an AI tool has made its edits and handed you back a ZIP, `contextzip apply-zip` writes those changes into your project — safely.
197
+
198
+ ```bash
199
+ # Drop the AI's returned zip into .contextzip/inbox/, then:
200
+ contextzip apply-zip
201
+
202
+ # Or point at it directly, wherever it landed:
203
+ contextzip apply-zip ~/Downloads/fixed-project.zip
204
+
205
+ # Preview first, with full per-file detail:
206
+ contextzip apply-zip --dry-run --verbose
207
+
208
+ # Skip the confirmation prompt (e.g. in a script):
209
+ contextzip apply-zip --yes
210
+ ```
211
+
212
+ ### How it knows what's safe to apply
213
+
214
+ Every ZIP `contextzip` creates gets a small manifest written next to it in `.contextzip/output/` — a hash of each included file, taken at zip-time. This manifest is **never added to the ZIP itself**. It stays local, so it's never uploaded and never visible to whatever AI tool the ZIP is pasted into — nothing for a model, or a curious teammate, to notice or ask about.
215
+
216
+ When you run `apply-zip`, it diffs the returned ZIP against that manifest and classifies every file:
217
+
218
+ | Status | Meaning | Applied automatically? |
219
+ |---|---|---|
220
+ | **New** | Wasn't part of the original ZIP | Yes |
221
+ | **Modified** | Was sent, content changed, and your local copy hasn't moved since | Yes |
222
+ | **Unchanged** | Identical to what's already on disk | Skipped — nothing to do |
223
+ | **Drifted** | Your local file changed (or was deleted) since you zipped it | No — flagged, asks first |
224
+ | **Untracked** | In the returned ZIP but has no baseline to compare against | No — flagged, asks first |
225
+
226
+ The common case — you zip some files, the AI edits them, nothing else touched your project meanwhile — applies straight through with just a summary printed, no prompt. Anything that could clobber your own work, or introduce a path you didn't expect, stops and asks before writing.
227
+
228
+ If a ZIP arrives with no matching manifest at all (e.g. it wasn't produced by a `contextzip` round trip), every already-existing path is treated as untracked and you'll be asked to confirm the whole batch.
229
+
230
+ ### What it does and doesn't do
231
+
232
+ - **Only adds and modifies files.** Deletions are never inferred from a ZIP's contents — a file that's simply missing from the returned ZIP is left alone.
233
+ - **Backs up before overwriting.** Every file about to be touched is copied into `.contextzip/backups/<timestamp>/` first, preserving its relative path.
234
+ - **Rejects unsafe paths outright.** Any ZIP entry that would resolve outside your project (`../../etc/passwd`-style path traversal) is refused before anything is written — no override flag, no exceptions.
235
+ - **Archives what it consumes.** Once applied, the ZIP is moved to `.contextzip/inbox/applied/<timestamp>-name.zip` — it won't be picked up again by accident, and stays around as an audit trail. ZIPs applied via an explicit path outside the inbox are left where you put them.
236
+
237
+ ### Finding the right ZIP and manifest
238
+
239
+ ```bash
240
+ contextzip apply-zip # exactly one *.zip in .contextzip/inbox/ → uses it
241
+ contextzip apply-zip path/to/fix.zip # explicit path always overrides the inbox
242
+ contextzip apply-zip --manifest path/to/codebase.manifest.json # override auto-detection
243
+ ```
244
+
245
+ If `.contextzip/inbox/` has more than one ZIP and you don't specify which, `apply-zip` lists them and asks you to be explicit rather than guessing. Manifest auto-detection picks the most recently created `*.manifest.json` under `.contextzip/output/` — correct for the normal one-round-trip-at-a-time flow; pass `--manifest` explicitly if you've generated several ZIPs before getting a response back.
246
+
247
+ ---
248
+
185
249
  ## Terminal error watcher
186
250
 
187
251
  The `watch` command wraps your dev server, buffers its output, and packages a debug-ready ZIP the moment you spot an error — no manual file hunting, no copy-pasting stack traces.
@@ -249,7 +313,7 @@ detects both Node.js and Python from `root/`, without either marker existing at
249
313
  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:
250
314
 
251
315
  1. **`CONTEXTZIP_WORKSPACE_LOCATION` environment variable** — for a one-off override on a single run
252
- 2. **A committed `.contextzip/config.json` at the project root** — team-shared, applies to everyone who clones the repo:
316
+ 2. **A committed `.contextzip.json` at the project root** — team-shared, applies to everyone who clones the repo:
253
317
  ```json
254
318
  { "workspace_location": "git-root" }
255
319
  ```
@@ -263,64 +327,25 @@ By default, `.contextzip/` is created at your git root — that's `_find_git_roo
263
327
 
264
328
  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.
265
329
 
266
- 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/config.json` instead if you'd rather each subproject keep its own.
267
-
268
- ---
269
-
270
- ## Project configuration
330
+ 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.
271
331
 
272
- Every project gets a `.contextzip/` workspace at the project root:
332
+ **Workspace layout:**
273
333
 
274
334
  ```
275
335
  .contextzip/
276
- ├── config.json # project preferences — commit this
277
- ├── .gitignore # keeps output/ untracked, config.json trackable
278
- └── output/ # generated ZIPs — never committed
279
- ├── codebase.zip
280
- ├── changes.zip (--git-changes)
281
- └── debug-context.zip (contextzip watch)
336
+ config.json # team-shared preferences (committed)
337
+ .gitignore # ignores everything below except itself + config.json
338
+ output/
339
+ codebase.zip # what you generate and send out
340
+ codebase.manifest.json # local-only — never uploaded, used by apply-zip
341
+ inbox/
342
+ <ai-returned>.zip # drop AI-returned zips here for apply-zip to pick up
343
+ applied/
344
+ <timestamp>-name.zip # archived after a successful apply-zip
345
+ backups/
346
+ <timestamp>/ # pre-overwrite copies, one folder per apply-zip run
282
347
  ```
283
348
 
284
- `.contextzip/` is **not** committed to Git by default — `.contextzip/.gitignore` is written automatically the first time you run contextzip in a repo, and it ignores everything in the folder *except* `config.json` and itself. That's deliberate: `output/` is per-machine, disposable scratch space, while `config.json` holds team-shared preferences you'll usually want everyone on the same page about.
285
-
286
- `config.json` currently supports:
287
-
288
- ```json
289
- {
290
- "workspace_location": "git-root",
291
- "scan_depth": null,
292
- "always_include": [],
293
- "always_exclude": [],
294
- "ai": {
295
- "enabled": true,
296
- "provider": "gemini",
297
- "max_files": 10
298
- }
299
- }
300
- ```
301
-
302
- All keys are optional — start with just the ones you need.
303
-
304
- | Key | What it does |
305
- | --- | --- |
306
- | `workspace_location` | Same as `contextzip config --set-workspace`, but team-shared. See [Workspace location](#workspace-location). |
307
- | `scan_depth` | Reserved for a future bounded-depth scan mode. |
308
- | `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. |
309
- | `always_exclude` | A standing exclusion list, applied on every run without retyping `-e`/`--exclude`. |
310
- | `ai.enabled` | Set to `false` to disable `--prompt` entirely for this project; contextzip will refuse with a clear message instead of silently ignoring it. |
311
- | `ai.provider` | Reserved for future AI providers. Only `"gemini"` is currently supported. |
312
- | `ai.max_files` | Caps how many files `--prompt` mode may return, for both the Gemini and keyword-heuristic paths. |
313
-
314
- `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.).
315
-
316
- A **web-based configuration UI** that generates `config.json` for you is on the roadmap — the schema above is designed to grow additively, so future preferences (including ones set visually) won't require another migration.
317
-
318
- ### Deprecation: `.contextzip.json`
319
-
320
- 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.
321
-
322
- `~/.config/contextzip/config.json` (your personal, per-machine config — API key, personal workspace override) is unrelated and unaffected by any of this.
323
-
324
349
  ---
325
350
 
326
351
  ## Contributing
@@ -1,6 +1,6 @@
1
1
  """contextzip — intelligent codebase packager for AI tools."""
2
2
 
3
- __version__ = "0.3.6"
3
+ __version__ = "0.3.8"
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,
@@ -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: