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.
Files changed (46) hide show
  1. {contextzip-0.3.7 → contextzip-0.3.9}/PKG-INFO +46 -93
  2. {contextzip-0.3.7 → contextzip-0.3.9}/README.md +45 -92
  3. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/__init__.py +7 -1
  4. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/api.py +82 -1
  5. contextzip-0.3.9/contextzip/applier.py +635 -0
  6. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/cli.py +148 -0
  7. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/cli_display.py +105 -0
  8. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/config.py +24 -16
  9. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/packager.py +47 -0
  10. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/base.py +31 -0
  11. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/server.py +32 -10
  12. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/PKG-INFO +46 -93
  13. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/SOURCES.txt +1 -0
  14. {contextzip-0.3.7 → contextzip-0.3.9}/pyproject.toml +1 -1
  15. {contextzip-0.3.7 → contextzip-0.3.9}/LICENSE +0 -0
  16. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/ai/__init__.py +0 -0
  17. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/ai/gemini.py +0 -0
  18. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/ai/heuristic.py +0 -0
  19. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/ai/selector.py +0 -0
  20. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/cli_ai.py +0 -0
  21. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/cli_onboard.py +0 -0
  22. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/clipboard.py +0 -0
  23. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/detector.py +0 -0
  24. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/error_parser.py +0 -0
  25. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/filters.py +0 -0
  26. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/git.py +0 -0
  27. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/project_config.py +0 -0
  28. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/__init__.py +0 -0
  29. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/errors/__init__.py +0 -0
  30. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/errors/node.py +0 -0
  31. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/errors/python.py +0 -0
  32. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/go.py +0 -0
  33. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/node.py +0 -0
  34. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/python.py +0 -0
  35. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/ruby.py +0 -0
  36. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/rules/rust.py +0 -0
  37. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/watcher.py +0 -0
  38. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/__init__.py +0 -0
  39. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/assets.py +0 -0
  40. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/persist.py +0 -0
  41. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip/webui/suggestions.py +0 -0
  42. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/dependency_links.txt +0 -0
  43. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/entry_points.txt +0 -0
  44. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/requires.txt +0 -0
  45. {contextzip-0.3.7 → contextzip-0.3.9}/contextzip.egg-info/top_level.txt +0 -0
  46. {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.7
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/config.json`
56
- - **Visual config UI** — `contextzip config --ui` opens a local browser tab to set include/exclude by clicking through your actual file tree, with live counts and one-click suggestions for PDFs, media, and other non-code files — nothing leaves your machine
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/output/` at your project root
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/output/debug-context.zip`. Your server keeps running — no restart, no interruption.
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/config.json` at the project root** — team-shared, applies to everyone who clones the repo:
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/config.json` instead if you'd rather each subproject keep its own.
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
- ├── config.json # project preferences — commit this
307
- ├── .gitignore # keeps output/ untracked, config.json trackable
308
- └── output/ # generated ZIPs — never committed
309
- ├── codebase.zip
310
- ├── changes.zip (--git-changes)
311
- └── debug-context.zip (contextzip watch)
312
- ```
313
-
314
- `.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.
315
-
316
- `config.json` currently supports:
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/config.json`
27
- - **Visual config UI** — `contextzip config --ui` opens a local browser tab to set include/exclude by clicking through your actual file tree, with live counts and one-click suggestions for PDFs, media, and other non-code files — nothing leaves your machine
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/output/` at your project root
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/output/debug-context.zip`. Your server keeps running — no restart, no interruption.
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/config.json` at the project root** — team-shared, applies to everyone who clones the repo:
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/config.json` instead if you'd rather each subproject keep its own.
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
- ├── config.json # project preferences — commit this
278
- ├── .gitignore # keeps output/ untracked, config.json trackable
279
- └── output/ # generated ZIPs — never committed
280
- ├── codebase.zip
281
- ├── changes.zip (--git-changes)
282
- └── debug-context.zip (contextzip watch)
283
- ```
284
-
285
- `.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.
286
-
287
- `config.json` currently supports:
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.7"
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 GitChanges, GitError, GitErrorKind, get_changed_files
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: