contextzip 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {contextzip-0.4.0 → contextzip-0.4.2}/PKG-INFO +3 -1
- {contextzip-0.4.0 → contextzip-0.4.2}/README.md +2 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/__init__.py +1 -1
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/applier.py +7 -5
- contextzip-0.4.2/contextzip/cleanup.py +207 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/cli.py +179 -230
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/cli_ai.py +6 -35
- contextzip-0.4.2/contextzip/cli_display.py +426 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/cli_onboard.py +13 -35
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/config.py +3 -1
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/packager.py +197 -45
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/project_config.py +81 -11
- contextzip-0.4.2/contextzip/redact.py +175 -0
- contextzip-0.4.2/contextzip/report.py +174 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/watcher.py +30 -65
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/persist.py +2 -2
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/server.py +9 -24
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/PKG-INFO +3 -1
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/SOURCES.txt +3 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/pyproject.toml +1 -1
- contextzip-0.4.0/contextzip/cli_display.py +0 -454
- {contextzip-0.4.0 → contextzip-0.4.2}/LICENSE +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/ai/__init__.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/ai/gemini.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/ai/heuristic.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/ai/selector.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/api.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/clipboard.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/detector.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/error_parser.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/filters.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/git.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/base.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/errors/__init__.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/errors/node.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/errors/python.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/go.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/node.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/python.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/ruby.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/rules/rust.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/__init__.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/assets.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip/webui/suggestions.py +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.4.0 → contextzip-0.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextzip
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Intelligently package your codebase for AI tools
|
|
5
5
|
Author-email: Deepesh <akadeepesh@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,6 +27,8 @@ Requires-Dist: pathspec>=0.12
|
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
|
+
<img width="1536" height="1024" alt="cz" src="https://github.com/user-attachments/assets/a930b49c-34a0-447b-8f3a-1909eae977c8" />
|
|
31
|
+
|
|
30
32
|
# contextzip
|
|
31
33
|
|
|
32
34
|
> Package exactly the right parts of your codebase and paste it into any AI tool — in one command.
|
|
@@ -291,10 +291,12 @@ def find_latest_manifest(
|
|
|
291
291
|
Resolve which manifest to diff against.
|
|
292
292
|
|
|
293
293
|
An explicit path always wins (if it exists). Otherwise picks the most
|
|
294
|
-
recently modified *.manifest.json under .contextzip/output/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
recently modified *.manifest.json anywhere under .contextzip/output/
|
|
295
|
+
— including its per-mode subfolders (output/codebase/, output/git-changes/,
|
|
296
|
+
output/prompt/) — since in the normal one-round-trip-at-a-time flow,
|
|
297
|
+
that's the zip you most recently generated regardless of which mode
|
|
298
|
+
produced it, which is almost always the right baseline. Returns None
|
|
299
|
+
if nothing is found (apply-zip still works, just more conservatively).
|
|
298
300
|
"""
|
|
299
301
|
if explicit_manifest is not None:
|
|
300
302
|
path = Path(explicit_manifest).expanduser().resolve()
|
|
@@ -304,7 +306,7 @@ def find_latest_manifest(
|
|
|
304
306
|
if not out_dir.is_dir():
|
|
305
307
|
return None
|
|
306
308
|
|
|
307
|
-
candidates = [p for p in out_dir.
|
|
309
|
+
candidates = [p for p in out_dir.rglob(f"*{_MANIFEST_SUFFIX}") if p.is_file()]
|
|
308
310
|
if not candidates:
|
|
309
311
|
return None
|
|
310
312
|
return max(candidates, key=lambda p: p.stat().st_mtime)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cleanup.py — Automatic workspace housekeeping for contextzip.
|
|
3
|
+
|
|
4
|
+
.contextzip/ accumulates disposable artifacts over time: a zip+manifest+report
|
|
5
|
+
set every run, a timestamped backup folder every risky `apply-zip`, and (until
|
|
6
|
+
pruned) old archived applied-zips. None of it is precious — every zip is
|
|
7
|
+
trivially reproducible by just re-running contextzip, and a backup only
|
|
8
|
+
matters until the apply that made it is confirmed good. So cleanup here is
|
|
9
|
+
deliberately brutal rather than cautious: it runs automatically after every
|
|
10
|
+
successful command (see `_auto_cleanup` in cli.py), with no prompt and no
|
|
11
|
+
separate command to remember, and keeps only the most recent
|
|
12
|
+
`cleanup.keep_recent` item(s) in each category — everything else is deleted
|
|
13
|
+
immediately, every time.
|
|
14
|
+
|
|
15
|
+
The heavier caution used elsewhere in contextzip (backing up before overwrite
|
|
16
|
+
in apply-zip, never inferring deletions) is reserved for the project's own
|
|
17
|
+
files, where losing something can mean losing real work. Nothing here ever
|
|
18
|
+
touches the project — only disposable metadata under .contextzip/ that costs
|
|
19
|
+
nothing to regenerate.
|
|
20
|
+
|
|
21
|
+
Never touched, under any settings:
|
|
22
|
+
- .contextzip/config.json (project preferences)
|
|
23
|
+
- .contextzip/inbox/*.zip (zips waiting to be applied — never assumed
|
|
24
|
+
stale just because they've been sitting there; only inbox/applied/,
|
|
25
|
+
the archive of zips already consumed, is ever pruned)
|
|
26
|
+
- anything outside .contextzip/
|
|
27
|
+
|
|
28
|
+
Kept only-the-most-recent-N, everything older deleted immediately:
|
|
29
|
+
- zip/manifest/report sets under .contextzip/output/<mode>/, per mode
|
|
30
|
+
folder
|
|
31
|
+
- timestamped folders under .contextzip/backups/
|
|
32
|
+
- archived zips under .contextzip/inbox/applied/ (this is also governed
|
|
33
|
+
by `applied_zip_retention`, already enforced on every apply-zip run —
|
|
34
|
+
scanning it here too just catches the case where zips piled up before
|
|
35
|
+
that setting existed, or the setting was since lowered)
|
|
36
|
+
|
|
37
|
+
Scanning only ever touches .contextzip/ directory listings and file mtimes —
|
|
38
|
+
no hashing, no reading file contents — so this stays fast even called on
|
|
39
|
+
every command, on a large project.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import shutil
|
|
45
|
+
from dataclasses import dataclass, field
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_OUTPUT_DIRNAME = "output"
|
|
50
|
+
_BACKUPS_DIRNAME = "backups"
|
|
51
|
+
_INBOX_DIRNAME = "inbox"
|
|
52
|
+
_APPLIED_DIRNAME = "applied"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Data model
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class CleanupItem:
|
|
62
|
+
path: Path
|
|
63
|
+
kind: str # "zip-set" | "backup" | "applied-zip"
|
|
64
|
+
size_bytes: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class CleanupPlan:
|
|
69
|
+
workspace: Path
|
|
70
|
+
items: list[CleanupItem] = field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def total_bytes(self) -> int:
|
|
74
|
+
return sum(i.size_bytes for i in self.items)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_empty(self) -> bool:
|
|
78
|
+
return not self.items
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class CleanupResult:
|
|
83
|
+
removed: list[Path] = field(default_factory=list)
|
|
84
|
+
failed: list[tuple[Path, str]] = field(default_factory=list)
|
|
85
|
+
freed_bytes: int = 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Helpers
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _size_of(path: Path) -> int:
|
|
94
|
+
"""Size of a file, or total size of everything under a directory."""
|
|
95
|
+
if path.is_file():
|
|
96
|
+
try:
|
|
97
|
+
return path.stat().st_size
|
|
98
|
+
except OSError:
|
|
99
|
+
return 0
|
|
100
|
+
if not path.is_dir():
|
|
101
|
+
return 0
|
|
102
|
+
total = 0
|
|
103
|
+
for p in path.rglob("*"):
|
|
104
|
+
if p.is_file():
|
|
105
|
+
try:
|
|
106
|
+
total += p.stat().st_size
|
|
107
|
+
except OSError:
|
|
108
|
+
pass
|
|
109
|
+
return total
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _zip_set_files(zip_path: Path) -> list[Path]:
|
|
113
|
+
"""The zip and any sidecar manifest/report files that sit next to it."""
|
|
114
|
+
files = [zip_path]
|
|
115
|
+
for suffix in (".manifest.json", ".report.txt", ".apply-report.txt"):
|
|
116
|
+
sidecar = zip_path.with_name(zip_path.stem + suffix)
|
|
117
|
+
if sidecar.is_file():
|
|
118
|
+
files.append(sidecar)
|
|
119
|
+
return files
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _newest_first(paths: list[Path]) -> list[Path]:
|
|
123
|
+
return sorted(paths, key=lambda p: p.stat().st_mtime, reverse=True)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# Scan
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def scan(
|
|
132
|
+
project_dir: Path,
|
|
133
|
+
*,
|
|
134
|
+
keep_recent: int = 1,
|
|
135
|
+
applied_zip_retention: int = 1,
|
|
136
|
+
) -> CleanupPlan:
|
|
137
|
+
"""
|
|
138
|
+
Build a plan of everything beyond the most recent `keep_recent` item(s)
|
|
139
|
+
per category. Read-only — never deletes anything itself; pass the
|
|
140
|
+
result to `execute()` to actually do so.
|
|
141
|
+
"""
|
|
142
|
+
from contextzip.packager import _workspace_dir
|
|
143
|
+
|
|
144
|
+
workspace, _is_git_repo = _workspace_dir(project_dir)
|
|
145
|
+
items: list[CleanupItem] = []
|
|
146
|
+
keep = max(0, keep_recent)
|
|
147
|
+
|
|
148
|
+
# ── output/ — per-mode subfolders, plus any flat legacy files left
|
|
149
|
+
# behind by a contextzip version predating the per-mode split ──────
|
|
150
|
+
output_root = workspace / _OUTPUT_DIRNAME
|
|
151
|
+
if output_root.is_dir():
|
|
152
|
+
for mode_dir in sorted(p for p in output_root.iterdir() if p.is_dir()):
|
|
153
|
+
zips = _newest_first([p for p in mode_dir.glob("*.zip") if p.is_file()])
|
|
154
|
+
for zp in zips[keep:]:
|
|
155
|
+
for f in _zip_set_files(zp):
|
|
156
|
+
items.append(CleanupItem(path=f, kind="zip-set", size_bytes=_size_of(f)))
|
|
157
|
+
|
|
158
|
+
legacy_zips = _newest_first([p for p in output_root.glob("*.zip") if p.is_file()])
|
|
159
|
+
for zp in legacy_zips[keep:]:
|
|
160
|
+
for f in _zip_set_files(zp):
|
|
161
|
+
items.append(CleanupItem(path=f, kind="zip-set", size_bytes=_size_of(f)))
|
|
162
|
+
|
|
163
|
+
# ── backups/<timestamp>/ ─────────────────────────────────────────────
|
|
164
|
+
backups_root = workspace / _BACKUPS_DIRNAME
|
|
165
|
+
if backups_root.is_dir():
|
|
166
|
+
backups = _newest_first([p for p in backups_root.iterdir() if p.is_dir()])
|
|
167
|
+
for backup_dir in backups[keep:]:
|
|
168
|
+
items.append(
|
|
169
|
+
CleanupItem(path=backup_dir, kind="backup", size_bytes=_size_of(backup_dir))
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# ── inbox/applied/ ───────────────────────────────────────────────────
|
|
173
|
+
applied_root = workspace / _INBOX_DIRNAME / _APPLIED_DIRNAME
|
|
174
|
+
if applied_root.is_dir():
|
|
175
|
+
applied_zips = _newest_first([p for p in applied_root.glob("*.zip") if p.is_file()])
|
|
176
|
+
retain = max(1, applied_zip_retention)
|
|
177
|
+
for zp in applied_zips[retain:]:
|
|
178
|
+
items.append(CleanupItem(path=zp, kind="applied-zip", size_bytes=_size_of(zp)))
|
|
179
|
+
|
|
180
|
+
return CleanupPlan(workspace=workspace, items=items)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# Execute
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def execute(plan: CleanupPlan) -> CleanupResult:
|
|
189
|
+
"""
|
|
190
|
+
Delete everything in *plan*, immediately, no confirmation. Best-effort:
|
|
191
|
+
one failure doesn't stop the rest, and every failure is reported rather
|
|
192
|
+
than silently swallowed.
|
|
193
|
+
"""
|
|
194
|
+
result = CleanupResult()
|
|
195
|
+
for item in plan.items:
|
|
196
|
+
try:
|
|
197
|
+
if item.path.is_dir():
|
|
198
|
+
shutil.rmtree(item.path)
|
|
199
|
+
elif item.path.is_file():
|
|
200
|
+
item.path.unlink()
|
|
201
|
+
else:
|
|
202
|
+
continue # already gone — nothing to do, not a failure
|
|
203
|
+
result.removed.append(item.path)
|
|
204
|
+
result.freed_bytes += item.size_bytes
|
|
205
|
+
except OSError as exc:
|
|
206
|
+
result.failed.append((item.path, str(exc)))
|
|
207
|
+
return result
|