contextzip 0.3.4__tar.gz → 0.3.6__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 (45) hide show
  1. {contextzip-0.3.4 → contextzip-0.3.6}/PKG-INFO +93 -42
  2. {contextzip-0.3.4 → contextzip-0.3.6}/README.md +92 -41
  3. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/__init__.py +1 -1
  4. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/ai/gemini.py +15 -7
  5. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/ai/heuristic.py +9 -2
  6. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/ai/selector.py +7 -0
  7. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/cli.py +151 -213
  8. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/cli_ai.py +8 -0
  9. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/cli_display.py +8 -56
  10. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/cli_onboard.py +0 -59
  11. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/clipboard.py +0 -86
  12. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/config.py +24 -19
  13. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/detector.py +122 -12
  14. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/filters.py +44 -8
  15. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/git.py +0 -131
  16. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/packager.py +186 -35
  17. contextzip-0.3.6/contextzip/project_config.py +231 -0
  18. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/watcher.py +6 -6
  19. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip.egg-info/PKG-INFO +93 -42
  20. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip.egg-info/SOURCES.txt +1 -5
  21. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip.egg-info/entry_points.txt +1 -0
  22. {contextzip-0.3.4 → contextzip-0.3.6}/pyproject.toml +2 -1
  23. contextzip-0.3.4/contextzip/brief.py +0 -280
  24. contextzip-0.3.4/contextzip/claude_artifacts.py +0 -150
  25. contextzip-0.3.4/contextzip/claude_export.py +0 -73
  26. contextzip-0.3.4/contextzip/code_changes.py +0 -257
  27. contextzip-0.3.4/contextzip/markers.py +0 -61
  28. {contextzip-0.3.4 → contextzip-0.3.6}/LICENSE +0 -0
  29. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/ai/__init__.py +0 -0
  30. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/api.py +0 -0
  31. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/error_parser.py +0 -0
  32. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/__init__.py +0 -0
  33. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/base.py +0 -0
  34. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/errors/__init__.py +0 -0
  35. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/errors/node.py +0 -0
  36. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/errors/python.py +0 -0
  37. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/go.py +0 -0
  38. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/node.py +0 -0
  39. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/python.py +0 -0
  40. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/ruby.py +0 -0
  41. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip/rules/rust.py +0 -0
  42. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip.egg-info/dependency_links.txt +0 -0
  43. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip.egg-info/requires.txt +0 -0
  44. {contextzip-0.3.4 → contextzip-0.3.6}/contextzip.egg-info/top_level.txt +0 -0
  45. {contextzip-0.3.4 → contextzip-0.3.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextzip
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Intelligently package your codebase for AI tools
5
5
  Author-email: Deepesh <akadeepesh@gmail.com>
6
6
  License-Expression: MIT
@@ -47,13 +47,13 @@ contextzip eliminates that entirely. Run it from your project root — it detect
47
47
 
48
48
  ## Features
49
49
 
50
- - **Smart framework detection** — automatically identifies Node.js, Next.js, Python, Django, FastAPI, Rust, Go, and Ruby, applying the right exclusion rules for each
50
+ - **Smart framework detection** — automatically identifies Node.js, Next.js, Python, Django, FastAPI, Rust, Go, and Ruby, applying the right exclusion rules for each. Detection isn't limited to the project root: a shallow, bounded scan of subdirectories means a monorepo (`frontend/` + `backend/`, etc.) gets every ecosystem it contains detected and excluded correctly, not just whatever sits at the top level
51
51
  - **Respects `.gitignore`** — your existing ignore patterns are honoured automatically
52
52
  - **Git-aware packaging** — use `--git-changes` to package only modified, staged, and untracked files; perfect for incremental debugging and PR review sessions
53
53
  - **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
54
54
  - **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
- - **End-of-day / handoff prompts** — `contextzip eod` and `contextzip handoff` turn a Claude/ChatGPT conversation export plus today's code changes into a paste-ready prompt, copied straight to your clipboard
56
- - **Persistent workspace** — all generated ZIPs land in `.contextzip/` at your project root, discoverable, reusable, and git-ignored automatically
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
57
57
  - **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
58
  - **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
59
59
  - **Full CLI control** — `--include`, `--exclude`, `--dry-run`, `--output`, all composable
@@ -95,7 +95,7 @@ contextzip will:
95
95
 
96
96
  1. Detect your framework (e.g. `Next.js + Node.js`)
97
97
  2. Apply the appropriate exclusion rules
98
- 3. Create a compressed ZIP in `.contextzip/` at your project root
98
+ 3. Create a compressed ZIP in `.contextzip/output/` at your project root
99
99
  4. Open your file manager with the ZIP selected and ready to copy
100
100
 
101
101
  ---
@@ -106,6 +106,8 @@ contextzip will:
106
106
  contextzip [OPTIONS]
107
107
  ```
108
108
 
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
+
109
111
  | Option | Description |
110
112
  |---|---|
111
113
  | `-p`, `--prompt TEXT` | Describe your task in plain English — Gemini selects only the relevant files |
@@ -118,7 +120,7 @@ contextzip [OPTIONS]
118
120
  | `--no-clipboard` | Skip the clipboard / folder-open step |
119
121
  | `--no-gitignore` | Ignore the project's `.gitignore` |
120
122
 
121
- **Subcommands:** `exclude`, `include`, `watch`, `config`, `eod`, `handoff` — run `contextzip --help` for full details.
123
+ **Subcommands:** `exclude`, `include`, `watch`, `config` — run `contextzip --help` for full details.
122
124
 
123
125
  ---
124
126
 
@@ -226,7 +228,7 @@ contextzip starts your process normally. You see output exactly as you would wit
226
228
  ╰───────────────────────────────────────────────────╯
227
229
  ```
228
230
 
229
- Press **D** and contextzip immediately writes `.contextzip/debug-context.zip`. Your server keeps running — no restart, no interruption.
231
+ Press **D** and contextzip immediately writes `.contextzip/output/debug-context.zip`. Your server keeps running — no restart, no interruption.
230
232
 
231
233
  **What's in the ZIP:**
232
234
 
@@ -244,60 +246,109 @@ Press **D** and contextzip immediately writes `.contextzip/debug-context.zip`. Y
244
246
 
245
247
  ---
246
248
 
247
- ## End-of-day reports and chat handoffs
249
+ ## What gets excluded
248
250
 
249
- If you work through a problem in a Claude or ChatGPT conversation and need to either (a) summarize what you did for an end-of-day report, or (b) continue the same work in a fresh chat after hitting a usage limit, `eod` and `handoff` build the prompt for you — contextzip does no summarizing itself; that's left to whichever AI tool you paste the result into.
251
+ contextzip stacks exclusion rules based on your detected stack, on top of your `.gitignore`.
252
+
253
+ **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.
254
+
255
+ **By framework:**
256
+
257
+ | Stack | Additional exclusions |
258
+ |---|---|
259
+ | Node.js / Next.js | `node_modules/`, `.next/`, `dist/`, `build/`, lock files, `*.min.js`, `*.d.ts` |
260
+ | Python / Django / FastAPI | `__pycache__/`, `.venv/`, `*.pyc`, `migrations/`, `.pytest_cache/`, lock files |
261
+ | Rust | `target/`, `Cargo.lock`, `*.rlib` |
262
+ | Go | `vendor/`, `go.sum`, `bin/` |
263
+
264
+ Detection is additive — a monorepo with both `package.json` and `pyproject.toml` gets both rule sets applied. Marker files don't have to sit at the project root: contextzip also does a shallow, bounded scan of subdirectories (2 levels deep, skipping `node_modules/`, `.venv/`, `.git/`, and other dependency/build dirs it would never want to look inside anyway), so a layout like
250
265
 
251
- ```bash
252
- contextzip eod
253
- contextzip handoff
266
+ ```
267
+ root/
268
+ frontend/package.json
269
+ backend/requirements.txt
254
270
  ```
255
271
 
256
- **Setup:** export your conversation (any markdown export works) and drop the `.md` file into `exports/` at your project root. Both commands pick the most recently modified file there automatically.
272
+ detects both Node.js and Python from `root/`, without either marker existing at the top level. Run with default output (not `--dry-run --verbose`) and you'll see which subdirectory each ecosystem came from, e.g. `Next.js (frontend/) + FastAPI (backend/)`.
257
273
 
258
- **What gets built:**
274
+ ---
259
275
 
260
- - The conversation itself — pasted directly into the prompt if it's short, or referenced as an attachment if it's long enough that inlining it would burn through the next chat's context budget
261
- - Whatever code changed, resolved per file in priority order:
262
- 1. **Not pushed** — diffed against your upstream branch, plus the complete current file
263
- 2. **Diverged from Claude** — diffed against the version Claude last produced (if you've set up a session key — see below), plus the complete current codebase file
264
- 3. **Since last run** — diffed against a per-branch checkpoint that `eod`/`handoff` remember automatically, plus the complete current file
265
- - New, untracked files are included as full content (there's nothing to diff them against)
266
- - `eod` ends the prompt with a flat instruction to produce a work-log table; `handoff` frames it as continuing the project in a new chat
276
+ ## Workspace location
267
277
 
268
- The result is copied straight to your clipboard, and also saved to `.contextzip/` if you want to look it over first.
278
+ 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:
269
279
 
270
- **Optional — diffing against Claude's own version (case 2):** if you give contextzip your Claude.ai session key, `eod`/`handoff` will fetch the files Claude actually produced and compare them against your codebase, catching drift even after everything's pushed and in sync.
280
+ 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:
282
+ ```json
283
+ { "workspace_location": "git-root" }
284
+ ```
285
+ 3. **Your personal config** — a per-machine default that doesn't get committed:
286
+ ```bash
287
+ contextzip config --set-workspace cwd # always use ./.contextzip wherever you run it
288
+ contextzip config --set-workspace git-root # back to the default
289
+ contextzip config --set-workspace ~/zips # a fixed custom location, anywhere
290
+ contextzip config --reset-workspace # clear your personal override
291
+ ```
271
292
 
272
- ```bash
273
- contextzip config --set-session-key
274
- ```
293
+ 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.
275
294
 
276
- This is best-effort by design — it depends on an undocumented Claude.ai endpoint, so a missing key, an expired cookie, or the endpoint changing shape just skips this check with a warning rather than failing the whole command.
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
300
+
301
+ Every project gets a `.contextzip/` workspace at the project root:
277
302
 
278
- ```bash
279
- contextzip eod --dry-run # preview without advancing the checkpoint
280
- contextzip handoff --no-fetch # skip the Claude-artifact fetch for this run
303
+ ```
304
+ .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)
281
311
  ```
282
312
 
283
- ---
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
+ ```
284
330
 
285
- ## What gets excluded
331
+ All keys are optional — start with just the ones you need.
286
332
 
287
- contextzip stacks exclusion rules based on your detected stack, on top of your `.gitignore`.
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. |
288
342
 
289
- **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/` and `exports/` working folders.
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.).
290
344
 
291
- **By framework:**
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.
292
346
 
293
- | Stack | Additional exclusions |
294
- |---|---|
295
- | Node.js / Next.js | `node_modules/`, `.next/`, `dist/`, `build/`, lock files, `*.min.js`, `*.d.ts` |
296
- | Python / Django / FastAPI | `__pycache__/`, `.venv/`, `*.pyc`, `migrations/`, `.pytest_cache/`, lock files |
297
- | Rust | `target/`, `Cargo.lock`, `*.rlib` |
298
- | Go | `vendor/`, `go.sum`, `bin/` |
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.
299
350
 
300
- Detection is additive — a monorepo with both `package.json` and `pyproject.toml` gets both rule sets applied.
351
+ `~/.config/contextzip/config.json` (your personal, per-machine config — API key, personal workspace override) is unrelated and unaffected by any of this.
301
352
 
302
353
  ---
303
354
 
@@ -18,13 +18,13 @@ contextzip eliminates that entirely. Run it from your project root — it detect
18
18
 
19
19
  ## Features
20
20
 
21
- - **Smart framework detection** — automatically identifies Node.js, Next.js, Python, Django, FastAPI, Rust, Go, and Ruby, applying the right exclusion rules for each
21
+ - **Smart framework detection** — automatically identifies Node.js, Next.js, Python, Django, FastAPI, Rust, Go, and Ruby, applying the right exclusion rules for each. Detection isn't limited to the project root: a shallow, bounded scan of subdirectories means a monorepo (`frontend/` + `backend/`, etc.) gets every ecosystem it contains detected and excluded correctly, not just whatever sits at the top level
22
22
  - **Respects `.gitignore`** — your existing ignore patterns are honoured automatically
23
23
  - **Git-aware packaging** — use `--git-changes` to package only modified, staged, and untracked files; perfect for incremental debugging and PR review sessions
24
24
  - **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
25
25
  - **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
- - **End-of-day / handoff prompts** — `contextzip eod` and `contextzip handoff` turn a Claude/ChatGPT conversation export plus today's code changes into a paste-ready prompt, copied straight to your clipboard
27
- - **Persistent workspace** — all generated ZIPs land in `.contextzip/` at your project root, discoverable, reusable, and git-ignored automatically
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
28
28
  - **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
29
  - **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
30
30
  - **Full CLI control** — `--include`, `--exclude`, `--dry-run`, `--output`, all composable
@@ -66,7 +66,7 @@ contextzip will:
66
66
 
67
67
  1. Detect your framework (e.g. `Next.js + Node.js`)
68
68
  2. Apply the appropriate exclusion rules
69
- 3. Create a compressed ZIP in `.contextzip/` at your project root
69
+ 3. Create a compressed ZIP in `.contextzip/output/` at your project root
70
70
  4. Open your file manager with the ZIP selected and ready to copy
71
71
 
72
72
  ---
@@ -77,6 +77,8 @@ contextzip will:
77
77
  contextzip [OPTIONS]
78
78
  ```
79
79
 
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
+
80
82
  | Option | Description |
81
83
  |---|---|
82
84
  | `-p`, `--prompt TEXT` | Describe your task in plain English — Gemini selects only the relevant files |
@@ -89,7 +91,7 @@ contextzip [OPTIONS]
89
91
  | `--no-clipboard` | Skip the clipboard / folder-open step |
90
92
  | `--no-gitignore` | Ignore the project's `.gitignore` |
91
93
 
92
- **Subcommands:** `exclude`, `include`, `watch`, `config`, `eod`, `handoff` — run `contextzip --help` for full details.
94
+ **Subcommands:** `exclude`, `include`, `watch`, `config` — run `contextzip --help` for full details.
93
95
 
94
96
  ---
95
97
 
@@ -197,7 +199,7 @@ contextzip starts your process normally. You see output exactly as you would wit
197
199
  ╰───────────────────────────────────────────────────╯
198
200
  ```
199
201
 
200
- Press **D** and contextzip immediately writes `.contextzip/debug-context.zip`. Your server keeps running — no restart, no interruption.
202
+ Press **D** and contextzip immediately writes `.contextzip/output/debug-context.zip`. Your server keeps running — no restart, no interruption.
201
203
 
202
204
  **What's in the ZIP:**
203
205
 
@@ -215,60 +217,109 @@ Press **D** and contextzip immediately writes `.contextzip/debug-context.zip`. Y
215
217
 
216
218
  ---
217
219
 
218
- ## End-of-day reports and chat handoffs
220
+ ## What gets excluded
219
221
 
220
- If you work through a problem in a Claude or ChatGPT conversation and need to either (a) summarize what you did for an end-of-day report, or (b) continue the same work in a fresh chat after hitting a usage limit, `eod` and `handoff` build the prompt for you — contextzip does no summarizing itself; that's left to whichever AI tool you paste the result into.
222
+ contextzip stacks exclusion rules based on your detected stack, on top of your `.gitignore`.
223
+
224
+ **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.
225
+
226
+ **By framework:**
227
+
228
+ | Stack | Additional exclusions |
229
+ |---|---|
230
+ | Node.js / Next.js | `node_modules/`, `.next/`, `dist/`, `build/`, lock files, `*.min.js`, `*.d.ts` |
231
+ | Python / Django / FastAPI | `__pycache__/`, `.venv/`, `*.pyc`, `migrations/`, `.pytest_cache/`, lock files |
232
+ | Rust | `target/`, `Cargo.lock`, `*.rlib` |
233
+ | Go | `vendor/`, `go.sum`, `bin/` |
234
+
235
+ Detection is additive — a monorepo with both `package.json` and `pyproject.toml` gets both rule sets applied. Marker files don't have to sit at the project root: contextzip also does a shallow, bounded scan of subdirectories (2 levels deep, skipping `node_modules/`, `.venv/`, `.git/`, and other dependency/build dirs it would never want to look inside anyway), so a layout like
221
236
 
222
- ```bash
223
- contextzip eod
224
- contextzip handoff
237
+ ```
238
+ root/
239
+ frontend/package.json
240
+ backend/requirements.txt
225
241
  ```
226
242
 
227
- **Setup:** export your conversation (any markdown export works) and drop the `.md` file into `exports/` at your project root. Both commands pick the most recently modified file there automatically.
243
+ detects both Node.js and Python from `root/`, without either marker existing at the top level. Run with default output (not `--dry-run --verbose`) and you'll see which subdirectory each ecosystem came from, e.g. `Next.js (frontend/) + FastAPI (backend/)`.
228
244
 
229
- **What gets built:**
245
+ ---
230
246
 
231
- - The conversation itself — pasted directly into the prompt if it's short, or referenced as an attachment if it's long enough that inlining it would burn through the next chat's context budget
232
- - Whatever code changed, resolved per file in priority order:
233
- 1. **Not pushed** — diffed against your upstream branch, plus the complete current file
234
- 2. **Diverged from Claude** — diffed against the version Claude last produced (if you've set up a session key — see below), plus the complete current codebase file
235
- 3. **Since last run** — diffed against a per-branch checkpoint that `eod`/`handoff` remember automatically, plus the complete current file
236
- - New, untracked files are included as full content (there's nothing to diff them against)
237
- - `eod` ends the prompt with a flat instruction to produce a work-log table; `handoff` frames it as continuing the project in a new chat
247
+ ## Workspace location
238
248
 
239
- The result is copied straight to your clipboard, and also saved to `.contextzip/` if you want to look it over first.
249
+ 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:
240
250
 
241
- **Optional — diffing against Claude's own version (case 2):** if you give contextzip your Claude.ai session key, `eod`/`handoff` will fetch the files Claude actually produced and compare them against your codebase, catching drift even after everything's pushed and in sync.
251
+ 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:
253
+ ```json
254
+ { "workspace_location": "git-root" }
255
+ ```
256
+ 3. **Your personal config** — a per-machine default that doesn't get committed:
257
+ ```bash
258
+ contextzip config --set-workspace cwd # always use ./.contextzip wherever you run it
259
+ contextzip config --set-workspace git-root # back to the default
260
+ contextzip config --set-workspace ~/zips # a fixed custom location, anywhere
261
+ contextzip config --reset-workspace # clear your personal override
262
+ ```
242
263
 
243
- ```bash
244
- contextzip config --set-session-key
245
- ```
264
+ 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.
246
265
 
247
- This is best-effort by design — it depends on an undocumented Claude.ai endpoint, so a missing key, an expired cookie, or the endpoint changing shape just skips this check with a warning rather than failing the whole command.
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
271
+
272
+ Every project gets a `.contextzip/` workspace at the project root:
248
273
 
249
- ```bash
250
- contextzip eod --dry-run # preview without advancing the checkpoint
251
- contextzip handoff --no-fetch # skip the Claude-artifact fetch for this run
274
+ ```
275
+ .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)
252
282
  ```
253
283
 
254
- ---
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
+ ```
255
301
 
256
- ## What gets excluded
302
+ All keys are optional — start with just the ones you need.
257
303
 
258
- contextzip stacks exclusion rules based on your detected stack, on top of your `.gitignore`.
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. |
259
313
 
260
- **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/` and `exports/` working folders.
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.).
261
315
 
262
- **By framework:**
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.
263
317
 
264
- | Stack | Additional exclusions |
265
- |---|---|
266
- | Node.js / Next.js | `node_modules/`, `.next/`, `dist/`, `build/`, lock files, `*.min.js`, `*.d.ts` |
267
- | Python / Django / FastAPI | `__pycache__/`, `.venv/`, `*.pyc`, `migrations/`, `.pytest_cache/`, lock files |
268
- | Rust | `target/`, `Cargo.lock`, `*.rlib` |
269
- | Go | `vendor/`, `go.sum`, `bin/` |
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.
270
321
 
271
- Detection is additive — a monorepo with both `package.json` and `pyproject.toml` gets both rule sets applied.
322
+ `~/.config/contextzip/config.json` (your personal, per-machine config — API key, personal workspace override) is unrelated and unaffected by any of this.
272
323
 
273
324
  ---
274
325
 
@@ -1,6 +1,6 @@
1
1
  """contextzip — intelligent codebase packager for AI tools."""
2
2
 
3
- __version__ = "0.3.4"
3
+ __version__ = "0.3.6"
4
4
 
5
5
  from contextzip.api import (
6
6
  FileCollection,
@@ -70,6 +70,7 @@ def select_files(
70
70
  file_tree: list[tuple[str, int]], # [(rel_path, size_bytes), ...]
71
71
  ecosystem: str,
72
72
  model: str = DEFAULT_MODEL,
73
+ max_files: int | None = None,
73
74
  ) -> list[str]:
74
75
  """
75
76
  Ask Gemini which files are relevant to *prompt* and return their paths.
@@ -89,6 +90,11 @@ def select_files(
89
90
  Gives the model important context for relevance scoring.
90
91
  model:
91
92
  Gemini model identifier. Defaults to gemini-2.5-flash-lite.
93
+ max_files:
94
+ Hard cap on how many files may be returned, regardless of what the
95
+ model outputs. Defaults to _MAX_FILES (12) when omitted — typically
96
+ overridden by a project's `ai.max_files` preference
97
+ (.contextzip/config.json).
92
98
 
93
99
  Returns
94
100
  -------
@@ -110,7 +116,8 @@ def select_files(
110
116
  "Install it with: pip install httpx"
111
117
  )
112
118
 
113
- system_prompt = _build_system_prompt()
119
+ effective_max_files = max_files if max_files and max_files > 0 else _MAX_FILES
120
+ system_prompt = _build_system_prompt(effective_max_files)
114
121
  user_message = _build_user_message(prompt, file_tree, ecosystem)
115
122
 
116
123
  url = _API_URL.format(model=model, key=api_key)
@@ -153,7 +160,7 @@ def select_files(
153
160
  f"Gemini API returned HTTP {response.status_code}: {response.text[:200]}"
154
161
  )
155
162
 
156
- return _parse_response(response.json(), file_tree)
163
+ return _parse_response(response.json(), file_tree, effective_max_files)
157
164
 
158
165
 
159
166
  # ---------------------------------------------------------------------------
@@ -161,8 +168,8 @@ def select_files(
161
168
  # ---------------------------------------------------------------------------
162
169
 
163
170
 
164
- def _build_system_prompt() -> str:
165
- return """\
171
+ def _build_system_prompt(max_files: int = _MAX_FILES) -> str:
172
+ return f"""\
166
173
  You are a precise file relevance assistant for software projects.
167
174
 
168
175
  Your only job: given a developer's task description and a project file tree,
@@ -178,7 +185,7 @@ Rules you must follow:
178
185
  - Prefer files that will be MODIFIED over files that are merely referenced.
179
186
  - Config files, test files, and documentation should only appear if the
180
187
  task explicitly concerns them.
181
- - Never return more than 10 files. For most tasks 2–5 files is correct.
188
+ - Never return more than {max_files} files. For most tasks 2–5 files is correct.
182
189
  - Order by relevance: most directly relevant file first.\
183
190
  """
184
191
 
@@ -209,13 +216,14 @@ Return only a JSON array of the most relevant file paths for this task.\
209
216
  def _parse_response(
210
217
  data: dict,
211
218
  file_tree: list[tuple[str, int]],
219
+ max_files: int = _MAX_FILES,
212
220
  ) -> list[str]:
213
221
  """
214
222
  Extract and validate the file list from the Gemini API response.
215
223
 
216
224
  - Parses the JSON array from the model's text output
217
225
  - Drops any path the model hallucinated (not in the real file tree)
218
- - Enforces the _MAX_FILES hard cap
226
+ - Enforces the *max_files* hard cap
219
227
  - Warns (via exception) if the model returned mostly invalid paths
220
228
  """
221
229
  # Navigate the Gemini response structure
@@ -266,7 +274,7 @@ def _parse_response(
266
274
  )
267
275
 
268
276
  # Enforce hard cap
269
- return validated[:_MAX_FILES]
277
+ return validated[:max_files]
270
278
 
271
279
 
272
280
  # ---------------------------------------------------------------------------
@@ -201,6 +201,7 @@ def select_files(
201
201
  *,
202
202
  prompt: str,
203
203
  file_tree: list[tuple[str, int]],
204
+ max_files: int | None = None,
204
205
  ) -> list[str]:
205
206
  """
206
207
  Score and rank *file_tree* entries by relevance to *prompt*.
@@ -212,18 +213,24 @@ def select_files(
212
213
  file_tree:
213
214
  Candidate files as (relative_posix_path, size_bytes) tuples.
214
215
  Standard exclusions must already have been applied by the caller.
216
+ max_files:
217
+ Cap on how many files may be returned. Defaults to MAX_FILES (8)
218
+ when omitted — typically overridden by a project's `ai.max_files`
219
+ preference (.contextzip/config.json).
215
220
 
216
221
  Returns
217
222
  -------
218
223
  list[str]
219
224
  Relative POSIX paths of the top-scoring files, best first.
220
- Files scoring zero are excluded. Result is capped at MAX_FILES.
225
+ Files scoring zero are excluded. Result is capped at *max_files*.
221
226
  """
222
227
  tokens = _tokenize(prompt)
223
228
  if not tokens:
224
229
  # Degenerate prompt — return nothing rather than random files
225
230
  return []
226
231
 
232
+ effective_max_files = max_files if max_files and max_files > 0 else MAX_FILES
233
+
227
234
  scored: list[tuple[float, str]] = []
228
235
 
229
236
  for rel_path, size_bytes in file_tree:
@@ -234,7 +241,7 @@ def select_files(
234
241
  # Sort descending by score, then alphabetically for determinism on ties
235
242
  scored.sort(key=lambda x: (-x[0], x[1]))
236
243
 
237
- return [path for _, path in scored[:MAX_FILES]]
244
+ return [path for _, path in scored[:effective_max_files]]
238
245
 
239
246
 
240
247
  # ---------------------------------------------------------------------------
@@ -32,10 +32,15 @@ def ai_select(
32
32
  prompt: str,
33
33
  ecosystem: str,
34
34
  api_key: str,
35
+ max_files: int | None = None,
35
36
  ) -> tuple[list[Path], str, str]:
36
37
  """
37
38
  Select the minimum relevant files for *prompt* from *resolved.included*.
38
39
 
40
+ *max_files* caps how many files either backend may return — typically a
41
+ project's `ai.max_files` preference (.contextzip/config.json). None
42
+ falls back to each backend's own built-in default cap.
43
+
39
44
  Returns
40
45
  -------
41
46
  (selected_paths, prompt_txt, method)
@@ -54,12 +59,14 @@ def ai_select(
54
59
  prompt=prompt,
55
60
  file_tree=file_tree,
56
61
  ecosystem=ecosystem,
62
+ max_files=max_files,
57
63
  )
58
64
  except GeminiRateLimitError:
59
65
  # Confirmed HTTP 429 only — fall back to keyword heuristic
60
66
  selected_rel = _heuristic.select_files(
61
67
  prompt=prompt,
62
68
  file_tree=file_tree,
69
+ max_files=max_files,
63
70
  )
64
71
  method = USED_HEURISTIC
65
72
  # All other GeminiErrors (bad key, network, unexpected status) propagate