contextzip 0.4.7__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {contextzip-0.4.7 → contextzip-0.5.0}/PKG-INFO +23 -11
  2. {contextzip-0.4.7 → contextzip-0.5.0}/README.md +22 -10
  3. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/__init__.py +1 -1
  4. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/api.py +39 -20
  5. contextzip-0.5.0/contextzip/applier.py +1357 -0
  6. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/cleanup.py +16 -1
  7. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/cli.py +387 -44
  8. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/cli_ai.py +2 -2
  9. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/cli_display.py +132 -49
  10. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/config.py +28 -0
  11. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/detector.py +12 -1
  12. contextzip-0.5.0/contextzip/editor.py +124 -0
  13. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/error_parser.py +54 -12
  14. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/filters.py +17 -2
  15. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/git.py +102 -36
  16. contextzip-0.5.0/contextzip/guide.py +134 -0
  17. contextzip-0.5.0/contextzip/merge3.py +149 -0
  18. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/packager.py +89 -35
  19. contextzip-0.5.0/contextzip/pathmatch.py +265 -0
  20. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/project_config.py +57 -5
  21. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/redact.py +2 -3
  22. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/report.py +43 -3
  23. contextzip-0.5.0/contextzip/root.py +331 -0
  24. contextzip-0.5.0/contextzip/snapshots.py +259 -0
  25. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/watcher.py +11 -1
  26. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/webui/assets.py +17 -2
  27. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/webui/persist.py +3 -0
  28. contextzip-0.5.0/contextzip/webui/review.py +360 -0
  29. contextzip-0.5.0/contextzip/webui/review_assets.py +407 -0
  30. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/webui/server.py +7 -0
  31. contextzip-0.5.0/contextzip/workspaces.py +138 -0
  32. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip.egg-info/PKG-INFO +23 -11
  33. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip.egg-info/SOURCES.txt +9 -0
  34. {contextzip-0.4.7 → contextzip-0.5.0}/pyproject.toml +1 -1
  35. contextzip-0.4.7/contextzip/applier.py +0 -808
  36. {contextzip-0.4.7 → contextzip-0.5.0}/LICENSE +0 -0
  37. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/_x11_clip.py +0 -0
  38. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/ai/__init__.py +0 -0
  39. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/ai/gemini.py +0 -0
  40. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/ai/heuristic.py +0 -0
  41. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/ai/selector.py +0 -0
  42. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/cli_onboard.py +0 -0
  43. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/clipboard.py +0 -0
  44. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/__init__.py +0 -0
  45. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/base.py +0 -0
  46. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/errors/__init__.py +0 -0
  47. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/errors/node.py +0 -0
  48. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/errors/python.py +0 -0
  49. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/go.py +0 -0
  50. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/node.py +0 -0
  51. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/python.py +0 -0
  52. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/ruby.py +0 -0
  53. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/rules/rust.py +0 -0
  54. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/updater.py +0 -0
  55. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/webui/__init__.py +0 -0
  56. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip/webui/suggestions.py +0 -0
  57. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip.egg-info/dependency_links.txt +0 -0
  58. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip.egg-info/entry_points.txt +0 -0
  59. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip.egg-info/requires.txt +0 -0
  60. {contextzip-0.4.7 → contextzip-0.5.0}/contextzip.egg-info/top_level.txt +0 -0
  61. {contextzip-0.4.7 → contextzip-0.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextzip
3
- Version: 0.4.7
3
+ Version: 0.5.0
4
4
  Summary: Intelligently package your codebase for AI tools
5
5
  Author-email: Deepesh <akadeepesh@gmail.com>
6
6
  License-Expression: MIT
@@ -120,6 +120,7 @@ contextzip --git-changes # only what you've changed
120
120
  contextzip --prompt "fix the login redirect bug" # let AI pick the relevant files
121
121
  contextzip include src/ app/ # only these folders
122
122
  contextzip include src/ .env # .env too, values redacted
123
+ contextzip --here # only the folder you're in (monorepos)
123
124
  contextzip exclude tests/ '*.snap' # everything except these
124
125
  contextzip apply-zip # write the AI's changes back
125
126
  ```
@@ -163,7 +164,7 @@ Describe your task in plain language and [Gemini](https://ai.google.dev/) picks
163
164
 
164
165
  - **Setup:** you need a free Gemini API key. contextzip walks you through it the first time you use `--prompt`, or you can set the `GEMINI_API_KEY` environment variable.
165
166
  - **What is sent:** only the list of file **paths and sizes** that already passed the exclusion rules, plus your prompt. File contents never leave your machine.
166
- - **Fallback:** if Gemini is rate-limited or unreachable, a local keyword-based selector takes over, so `--prompt` keeps working.
167
+ - **Fallback:** if Gemini is rate-limited, a local keyword-based selector takes over, so `--prompt` keeps working. Other errors (a bad key, no network) stop with a clear message.
167
168
  - **Model:** `gemini-2.5-flash-lite` by default.
168
169
 
169
170
  ### Applying AI changes back
@@ -174,17 +175,25 @@ contextzip apply-zip fix.zip # or point at one directly
174
175
  contextzip apply-zip --dry-run # preview without writing anything
175
176
  ```
176
177
 
177
- Every ZIP contextzip creates gets a **local manifest** containing a hash of each file. It's stored next to the ZIP and never inside it, so the AI tool never sees it. When a ZIP comes back, `apply-zip` compares every file against that manifest:
178
+ AI tools return ZIPs in every shape: wrapped in an extra folder, relative to a subfolder, flattened, with Windows separators. `apply-zip` matches each file to its real place in your project anyway, and new files follow their siblings.
178
179
 
179
- | Status | Meaning | Action |
180
+ Every ZIP contextzip makes gets a **local snapshot** (a hash of each file, never uploaded). `apply-zip` picks the snapshot that best matches the returned ZIP and compares three versions of each file: what you sent, what's on disk now, and what came back.
181
+
182
+ | Status | Meaning | Default |
180
183
  |---|---|---|
181
- | **New** | File didn't exist before | Written |
182
- | **Modified** | AI changed it, and you haven't touched it locally | Written |
183
- | **Unchanged** | Identical to what's on disk | Skipped |
184
- | **Drifted** | *You* edited it locally since zipping | ⚠️ Asks before overwriting |
185
- | **Untracked** | Exists locally but wasn't in the original ZIP | ⚠️ Asks before overwriting |
184
+ | **New** / **Modified** | New file, or only the AI changed it | Written |
185
+ | **Merged** | You both changed it, in different places | Written (three-way merge) |
186
+ | **Unchanged** | Already matches — including changes you already applied | Skipped |
187
+ | **Conflict** | You both changed the same lines | Needs your call |
188
+ | **Partial** | The AI returned a cut-down file (`// ... rest unchanged`) | Needs your call |
189
+ | **Moved** | The AI moved or renamed it | Written at the new path; the old file is kept unless you say otherwise |
190
+ | **Drifted** / **Deleted** / **Untracked** / **Ambiguous** / **Delete requested** | Can't be applied safely without you | Needs your call |
191
+
192
+ When something needs a decision you choose: apply the safe changes now, **review side by side in your browser** (`--review`), **resolve in VS Code's merge editor** (`--editor`), or apply everything. Line endings never count as a change, redacted secrets are never overwritten with placeholders, and **files are only removed with your explicit OK** — never one you've changed since.
193
+
194
+ ### Snapshots and CONTEXTZIP.md
186
195
 
187
- `apply-zip` only adds and modifies files. **It never deletes anything.** It also unwraps the extra top-level folder that AI tools and "Download ZIP" buttons often add.
196
+ Every package is kept locally as a numbered **snapshot** (`#14`), so a reply can be merged against exactly what you sent, even after you've packaged again. Each ZIP also carries a short **`CONTEXTZIP.md`** for the AI: the snapshot ID, and how to reply so the answer applies cleanly (complete files, same paths, secrets left as `[REDACTED]`, moves and deletions listed). When the reply echoes it back, `apply-zip` knows the exact snapshot. `contextzip snapshots` lists them; turn the file off with `"zip_guide": false`.
188
197
 
189
198
  ### Terminal error watcher
190
199
 
@@ -266,6 +275,8 @@ This holds per-project settings. Edit it by hand or with `contextzip config --ui
266
275
  },
267
276
  "applied_zip_retention": 1, // past apply-zip archives to keep
268
277
  "cleanup": { "enabled": true, "keep_recent": 1 },
278
+ "snapshots": { "keep": 10, "max_age_days": 14 }, // history apply-zip merges against
279
+ "zip_guide": true, // add CONTEXTZIP.md to every ZIP
269
280
  "webui": { "auto_open": true, "port": null }
270
281
  }
271
282
  ```
@@ -328,11 +339,12 @@ contextzip keeps everything in a `.contextzip/` folder at your git root (change
328
339
  │ ├── git-changes/ changes.zip + manifest + report (--git-changes)
329
340
  │ ├── prompt/ vibe.zip + manifest + report (--prompt)
330
341
  │ └── watch/ debug-context.zip (contextzip watch)
342
+ ├── snapshots/ numbered copies of what you sent (contextzip snapshots)
331
343
  ├── inbox/ drop AI-returned ZIPs here for apply-zip
332
344
  └── config.json project config (optional)
333
345
  ```
334
346
 
335
- Old outputs are cleaned up automatically, and only the most recent set is kept. You can change this with the `cleanup` setting.
347
+ Old outputs are cleaned up automatically: `output/` keeps the latest ZIP per mode (`cleanup`), and `snapshots/` keeps the newest 10 or anything under 14 days old (`snapshots`) — never one a reply in the inbox is waiting on.
336
348
 
337
349
  ## Python API
338
350
 
@@ -91,6 +91,7 @@ contextzip --git-changes # only what you've changed
91
91
  contextzip --prompt "fix the login redirect bug" # let AI pick the relevant files
92
92
  contextzip include src/ app/ # only these folders
93
93
  contextzip include src/ .env # .env too, values redacted
94
+ contextzip --here # only the folder you're in (monorepos)
94
95
  contextzip exclude tests/ '*.snap' # everything except these
95
96
  contextzip apply-zip # write the AI's changes back
96
97
  ```
@@ -134,7 +135,7 @@ Describe your task in plain language and [Gemini](https://ai.google.dev/) picks
134
135
 
135
136
  - **Setup:** you need a free Gemini API key. contextzip walks you through it the first time you use `--prompt`, or you can set the `GEMINI_API_KEY` environment variable.
136
137
  - **What is sent:** only the list of file **paths and sizes** that already passed the exclusion rules, plus your prompt. File contents never leave your machine.
137
- - **Fallback:** if Gemini is rate-limited or unreachable, a local keyword-based selector takes over, so `--prompt` keeps working.
138
+ - **Fallback:** if Gemini is rate-limited, a local keyword-based selector takes over, so `--prompt` keeps working. Other errors (a bad key, no network) stop with a clear message.
138
139
  - **Model:** `gemini-2.5-flash-lite` by default.
139
140
 
140
141
  ### Applying AI changes back
@@ -145,17 +146,25 @@ contextzip apply-zip fix.zip # or point at one directly
145
146
  contextzip apply-zip --dry-run # preview without writing anything
146
147
  ```
147
148
 
148
- Every ZIP contextzip creates gets a **local manifest** containing a hash of each file. It's stored next to the ZIP and never inside it, so the AI tool never sees it. When a ZIP comes back, `apply-zip` compares every file against that manifest:
149
+ AI tools return ZIPs in every shape: wrapped in an extra folder, relative to a subfolder, flattened, with Windows separators. `apply-zip` matches each file to its real place in your project anyway, and new files follow their siblings.
149
150
 
150
- | Status | Meaning | Action |
151
+ Every ZIP contextzip makes gets a **local snapshot** (a hash of each file, never uploaded). `apply-zip` picks the snapshot that best matches the returned ZIP and compares three versions of each file: what you sent, what's on disk now, and what came back.
152
+
153
+ | Status | Meaning | Default |
151
154
  |---|---|---|
152
- | **New** | File didn't exist before | Written |
153
- | **Modified** | AI changed it, and you haven't touched it locally | Written |
154
- | **Unchanged** | Identical to what's on disk | Skipped |
155
- | **Drifted** | *You* edited it locally since zipping | ⚠️ Asks before overwriting |
156
- | **Untracked** | Exists locally but wasn't in the original ZIP | ⚠️ Asks before overwriting |
155
+ | **New** / **Modified** | New file, or only the AI changed it | Written |
156
+ | **Merged** | You both changed it, in different places | Written (three-way merge) |
157
+ | **Unchanged** | Already matches — including changes you already applied | Skipped |
158
+ | **Conflict** | You both changed the same lines | Needs your call |
159
+ | **Partial** | The AI returned a cut-down file (`// ... rest unchanged`) | Needs your call |
160
+ | **Moved** | The AI moved or renamed it | Written at the new path; the old file is kept unless you say otherwise |
161
+ | **Drifted** / **Deleted** / **Untracked** / **Ambiguous** / **Delete requested** | Can't be applied safely without you | Needs your call |
162
+
163
+ When something needs a decision you choose: apply the safe changes now, **review side by side in your browser** (`--review`), **resolve in VS Code's merge editor** (`--editor`), or apply everything. Line endings never count as a change, redacted secrets are never overwritten with placeholders, and **files are only removed with your explicit OK** — never one you've changed since.
164
+
165
+ ### Snapshots and CONTEXTZIP.md
157
166
 
158
- `apply-zip` only adds and modifies files. **It never deletes anything.** It also unwraps the extra top-level folder that AI tools and "Download ZIP" buttons often add.
167
+ Every package is kept locally as a numbered **snapshot** (`#14`), so a reply can be merged against exactly what you sent, even after you've packaged again. Each ZIP also carries a short **`CONTEXTZIP.md`** for the AI: the snapshot ID, and how to reply so the answer applies cleanly (complete files, same paths, secrets left as `[REDACTED]`, moves and deletions listed). When the reply echoes it back, `apply-zip` knows the exact snapshot. `contextzip snapshots` lists them; turn the file off with `"zip_guide": false`.
159
168
 
160
169
  ### Terminal error watcher
161
170
 
@@ -237,6 +246,8 @@ This holds per-project settings. Edit it by hand or with `contextzip config --ui
237
246
  },
238
247
  "applied_zip_retention": 1, // past apply-zip archives to keep
239
248
  "cleanup": { "enabled": true, "keep_recent": 1 },
249
+ "snapshots": { "keep": 10, "max_age_days": 14 }, // history apply-zip merges against
250
+ "zip_guide": true, // add CONTEXTZIP.md to every ZIP
240
251
  "webui": { "auto_open": true, "port": null }
241
252
  }
242
253
  ```
@@ -299,11 +310,12 @@ contextzip keeps everything in a `.contextzip/` folder at your git root (change
299
310
  │ ├── git-changes/ changes.zip + manifest + report (--git-changes)
300
311
  │ ├── prompt/ vibe.zip + manifest + report (--prompt)
301
312
  │ └── watch/ debug-context.zip (contextzip watch)
313
+ ├── snapshots/ numbered copies of what you sent (contextzip snapshots)
302
314
  ├── inbox/ drop AI-returned ZIPs here for apply-zip
303
315
  └── config.json project config (optional)
304
316
  ```
305
317
 
306
- Old outputs are cleaned up automatically, and only the most recent set is kept. You can change this with the `cleanup` setting.
318
+ Old outputs are cleaned up automatically: `output/` keeps the latest ZIP per mode (`cleanup`), and `snapshots/` keeps the newest 10 or anything under 14 days old (`snapshots`) — never one a reply in the inbox is waiting on.
307
319
 
308
320
  ## Python API
309
321
 
@@ -1,6 +1,6 @@
1
1
  """contextzip — intelligent codebase packager for AI tools."""
2
2
 
3
- __version__ = "0.4.7"
3
+ __version__ = "0.5.0"
4
4
 
5
5
  from contextzip.api import (
6
6
  FileCollection,
@@ -35,7 +35,6 @@ Quickstart
35
35
 
36
36
  from __future__ import annotations
37
37
 
38
- import os
39
38
  from dataclasses import dataclass, field
40
39
  from pathlib import Path
41
40
 
@@ -158,7 +157,7 @@ def get_git_changes(
158
157
  Parameters
159
158
  ----------
160
159
  path:
161
- Directory to start from. Defaults to ``Path.cwd()``. The actual
160
+ Directory to start from. Defaults to the project root found from the current folder. The actual
162
161
  git root may be a parent of this directory.
163
162
  use_project_config:
164
163
  Apply the project's ``.contextzip/config.json``. Defaults to
@@ -234,7 +233,7 @@ def get_files(
234
233
  Parameters
235
234
  ----------
236
235
  path:
237
- Project root to scan. Defaults to ``Path.cwd()``.
236
+ Project root to scan. Defaults to the project root found from the current folder.
238
237
  include:
239
238
  If given, only files under these paths are returned (e.g.
240
239
  ``["src/", "app/"]``). Matched as exact path prefixes.
@@ -358,10 +357,14 @@ def create_zip(
358
357
 
359
358
  output_path = Path(output).resolve() if output is not None else None
360
359
 
360
+ project_cfg = load_project_config(collection.project_dir)
361
361
  return create_zip_silent(
362
362
  resolve_result=resolve_result,
363
363
  project_dir=collection.project_dir,
364
364
  output_path=output_path,
365
+ redact_secrets_enabled=project_cfg.limits.redact_secrets,
366
+ ecosystem=collection.ecosystem,
367
+ guide=project_cfg.zip_guide,
365
368
  )
366
369
 
367
370
 
@@ -370,20 +373,23 @@ def apply_zip(
370
373
  *,
371
374
  project_dir: str | Path | None = None,
372
375
  manifest: str | Path | None = None,
376
+ include_risky: bool = False,
373
377
  ) -> ApplyResult:
374
378
  """
375
379
  Apply an AI-returned ZIP back into *project_dir* (defaults to cwd).
376
380
 
377
- Diffs the zip against the local sidecar manifest most recently written
378
- to ``.contextzip/output/`` (or the one at *manifest*, if given) to
379
- classify every file as new, modified, unchanged, or one with no safe
380
- baseline (locally edited since zipping, or never part of the original
381
- manifest). Only adds and modifies files — nothing is ever deleted.
381
+ Each file is matched to its place in the project (even when the zip is
382
+ wrapped, relative to a subfolder, or flattened), then compared with the
383
+ snapshot of what was sent and with the file on disk. New, modified and
384
+ cleanly merged files are written. Files that need a decision —
385
+ conflicts, partial files, ones you deleted or that weren't sent, and
386
+ ambiguous ones — are left alone unless *include_risky* is True, in which
387
+ case the AI's version wins. Nothing is removed — moved files keep their
388
+ old copy and requested deletions are skipped; use
389
+ ``execute_plan(..., remove_moved=True, allow_delete=True)`` to opt in.
382
390
 
383
- Unlike the CLI, this applies immediately and does not prompt — check
384
- ``contextzip.applier.build_plan()`` yourself first if you want to
385
- inspect risk before writing (see ``ApplyPlan.is_risky`` /
386
- ``.risky_entries``).
391
+ Doesn't prompt. To inspect first, use ``contextzip.applier.build_plan()``
392
+ and ``execute_plan(..., decisions=...)`` directly.
387
393
 
388
394
  Parameters
389
395
  ----------
@@ -391,15 +397,20 @@ def apply_zip(
391
397
  Path to the returned zip. If omitted, auto-detects from
392
398
  ``.contextzip/inbox/`` — exactly one zip must be present there.
393
399
  project_dir:
394
- Project root to apply into. Defaults to ``Path.cwd()``.
400
+ Project root to apply into. Defaults to the project root found from the current folder.
395
401
  manifest:
396
- Explicit manifest path to diff against, overriding auto-detection.
402
+ Snapshot (manifest) to compare against. By default the one that
403
+ best matches the zip is picked automatically.
404
+ include_risky:
405
+ Also write files that need a decision, with the AI's version
406
+ winning. Defaults to False.
397
407
 
398
408
  Returns
399
409
  -------
400
410
  ApplyResult
401
- ``written`` (list of relative paths written) and
402
- ``applied_zip_path`` (where the consumed zip ended up).
411
+ ``written`` (relative paths written), ``skipped`` (paths left alone)
412
+ and ``applied_zip_path`` (where the zip ended up — it stays in the
413
+ inbox while something still needs a decision).
403
414
 
404
415
  Raises
405
416
  ------
@@ -431,9 +442,9 @@ def apply_zip(
431
442
  except ApplyError as exc:
432
443
  raise ZipNotFoundError(str(exc)) from exc
433
444
 
434
- manifest_path = find_latest_manifest(pdir, manifest)
445
+ manifest_path = find_latest_manifest(pdir, manifest) if manifest is not None else None
435
446
  plan = build_plan(resolved_zip, pdir, manifest_path)
436
- return execute_plan(plan, pdir)
447
+ return execute_plan(plan, pdir, include_risky=include_risky)
437
448
 
438
449
 
439
450
  def detect_ecosystem(
@@ -445,7 +456,7 @@ def detect_ecosystem(
445
456
  Parameters
446
457
  ----------
447
458
  path:
448
- Directory to inspect. Defaults to ``Path.cwd()``.
459
+ Directory to inspect. Defaults to the project root found from the current folder.
449
460
 
450
461
  Returns
451
462
  -------
@@ -498,8 +509,16 @@ def _config_kwargs(cfg, extra_exclude: list[str] | None = None) -> dict:
498
509
 
499
510
 
500
511
  def _resolve_dir(path: str | Path | None) -> Path:
512
+ """
513
+ An explicit *path* is used exactly as given. Without one, resolve the
514
+ project root from the current folder — the same root the CLI uses (see
515
+ root.find_project_root) — so a script run from a subfolder of a
516
+ monorepo still works on the whole project with root-relative paths.
517
+ """
501
518
  if path is None:
502
- return Path(os.getcwd()).resolve()
519
+ from contextzip.root import find_project_root
520
+
521
+ return find_project_root().root
503
522
  return Path(path).resolve()
504
523
 
505
524