contextzip 0.3.9__tar.gz → 0.4.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.
- contextzip-0.4.0/PKG-INFO +98 -0
- contextzip-0.4.0/README.md +69 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/__init__.py +1 -1
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/ai/selector.py +33 -11
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/applier.py +23 -9
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/cli.py +14 -2
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/cli_ai.py +5 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/cli_display.py +14 -3
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/filters.py +10 -2
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/project_config.py +137 -9
- contextzip-0.4.0/contextzip/webui/assets.py +1185 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/webui/persist.py +32 -4
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/webui/server.py +124 -6
- contextzip-0.4.0/contextzip.egg-info/PKG-INFO +98 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/pyproject.toml +1 -1
- contextzip-0.3.9/PKG-INFO +0 -350
- contextzip-0.3.9/README.md +0 -321
- contextzip-0.3.9/contextzip/webui/assets.py +0 -1012
- contextzip-0.3.9/contextzip.egg-info/PKG-INFO +0 -350
- {contextzip-0.3.9 → contextzip-0.4.0}/LICENSE +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/ai/__init__.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/ai/gemini.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/ai/heuristic.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/api.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/cli_onboard.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/clipboard.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/config.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/detector.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/error_parser.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/git.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/packager.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/base.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/errors/__init__.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/errors/node.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/errors/python.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/go.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/node.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/python.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/ruby.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/rust.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/watcher.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/webui/__init__.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/webui/suggestions.py +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/SOURCES.txt +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.3.9 → contextzip-0.4.0}/setup.cfg +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contextzip
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Intelligently package your codebase for AI tools
|
|
5
|
+
Author-email: Deepesh <akadeepesh@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/akadeepesh/contextzip
|
|
8
|
+
Project-URL: Repository, https://github.com/akadeepesh/contextzip
|
|
9
|
+
Project-URL: Issues, https://github.com/akadeepesh/contextzip/issues
|
|
10
|
+
Keywords: ai,cli,developer-tools,context,zip,llm,claude,chatgpt,code-packaging
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: click>=8.1
|
|
25
|
+
Requires-Dist: rich>=13.0
|
|
26
|
+
Requires-Dist: pathspec>=0.12
|
|
27
|
+
Requires-Dist: httpx>=0.27
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# contextzip
|
|
31
|
+
|
|
32
|
+
> Package exactly the right parts of your codebase and paste it into any AI tool — in one command.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install contextzip
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**📖 Full docs, every flag, every config option: [contextzip.vercel.app](https://contextzip.vercel.app)**
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why contextzip
|
|
43
|
+
|
|
44
|
+
Every AI session starts the same way: hunt down the relevant files, skip `node_modules` and build artifacts by hand, zip it, find the zip, upload it — then do it all again next session.
|
|
45
|
+
|
|
46
|
+
contextzip eliminates that. Run it from your project root — it detects your stack, applies smart exclusions, and produces a lean ZIP ready to paste into Claude, ChatGPT, or any AI tool. When the AI hands changes back, `contextzip apply-zip` writes them into your project safely.
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd ~/projects/my-app
|
|
52
|
+
contextzip
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's it — no flags required. contextzip detects your framework, excludes the noise, and writes a ZIP to `.contextzip/`.
|
|
56
|
+
|
|
57
|
+
## The essentials
|
|
58
|
+
|
|
59
|
+
| Command | What it does |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `contextzip` | Detect stack, exclude noise, package the project |
|
|
62
|
+
| `contextzip --prompt "task"` | Let Gemini pick only the files relevant to a task |
|
|
63
|
+
| `contextzip --git-changes` | Package only modified/staged/untracked files |
|
|
64
|
+
| `contextzip apply-zip` | Write an AI-returned ZIP back into your project, safely |
|
|
65
|
+
| `contextzip watch -- npm run dev` | Auto-package debug context the moment an error appears |
|
|
66
|
+
| `contextzip config --ui` | Set include/exclude rules visually in a local browser tab |
|
|
67
|
+
|
|
68
|
+
Every command supports `--dry-run`, `--verbose`, `--include`, `--exclude`, and more — see the [full CLI reference](https://contextzip.vercel.app/cli).
|
|
69
|
+
|
|
70
|
+
contextzip respects your `.gitignore`, detects Node.js, Next.js, Python, Django, FastAPI, Rust, Go, and Ruby (including in monorepos), and never packages secrets — SSH keys, cloud credentials, and Terraform state are always excluded. Details: [contextzip.vercel.app/features](https://contextzip.vercel.app/features).
|
|
71
|
+
|
|
72
|
+
## Also a Python library
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from contextzip import get_git_changes, create_zip
|
|
76
|
+
|
|
77
|
+
collection = get_git_changes()
|
|
78
|
+
pkg = create_zip(collection, output="/tmp/changes.zip")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Full function reference: [contextzip.vercel.app/cli#python-api](https://contextzip.vercel.app/cli#python-api)
|
|
82
|
+
|
|
83
|
+
## Learn more
|
|
84
|
+
|
|
85
|
+
- [Packaging & detection](https://contextzip.vercel.app/features) — frameworks, exclusion rules, safety
|
|
86
|
+
- [AI-powered file selection](https://contextzip.vercel.app/ai-selection) — `--prompt`, Gemini setup
|
|
87
|
+
- [Terminal error watcher](https://contextzip.vercel.app/watch) — `contextzip watch`
|
|
88
|
+
- [Applying changes back](https://contextzip.vercel.app/apply-zip) — `apply-zip`, backups, safety
|
|
89
|
+
- [Configuration](https://contextzip.vercel.app/configuration) — personal & project config, visual config UI
|
|
90
|
+
- [CLI & Python API reference](https://contextzip.vercel.app/cli)
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
Contributions are welcome — especially new framework rule sets and edge-case fixes. See [CONTRIBUTING.md](https://github.com/akadeepesh/contextzip/blob/main/CONTRIBUTING.md). Please open an issue before starting a large PR.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT — see [LICENSE](https://github.com/akadeepesh/contextzip/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# contextzip
|
|
2
|
+
|
|
3
|
+
> Package exactly the right parts of your codebase and paste it into any AI tool — in one command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install contextzip
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**📖 Full docs, every flag, every config option: [contextzip.vercel.app](https://contextzip.vercel.app)**
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why contextzip
|
|
14
|
+
|
|
15
|
+
Every AI session starts the same way: hunt down the relevant files, skip `node_modules` and build artifacts by hand, zip it, find the zip, upload it — then do it all again next session.
|
|
16
|
+
|
|
17
|
+
contextzip eliminates that. Run it from your project root — it detects your stack, applies smart exclusions, and produces a lean ZIP ready to paste into Claude, ChatGPT, or any AI tool. When the AI hands changes back, `contextzip apply-zip` writes them into your project safely.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd ~/projects/my-app
|
|
23
|
+
contextzip
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it — no flags required. contextzip detects your framework, excludes the noise, and writes a ZIP to `.contextzip/`.
|
|
27
|
+
|
|
28
|
+
## The essentials
|
|
29
|
+
|
|
30
|
+
| Command | What it does |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `contextzip` | Detect stack, exclude noise, package the project |
|
|
33
|
+
| `contextzip --prompt "task"` | Let Gemini pick only the files relevant to a task |
|
|
34
|
+
| `contextzip --git-changes` | Package only modified/staged/untracked files |
|
|
35
|
+
| `contextzip apply-zip` | Write an AI-returned ZIP back into your project, safely |
|
|
36
|
+
| `contextzip watch -- npm run dev` | Auto-package debug context the moment an error appears |
|
|
37
|
+
| `contextzip config --ui` | Set include/exclude rules visually in a local browser tab |
|
|
38
|
+
|
|
39
|
+
Every command supports `--dry-run`, `--verbose`, `--include`, `--exclude`, and more — see the [full CLI reference](https://contextzip.vercel.app/cli).
|
|
40
|
+
|
|
41
|
+
contextzip respects your `.gitignore`, detects Node.js, Next.js, Python, Django, FastAPI, Rust, Go, and Ruby (including in monorepos), and never packages secrets — SSH keys, cloud credentials, and Terraform state are always excluded. Details: [contextzip.vercel.app/features](https://contextzip.vercel.app/features).
|
|
42
|
+
|
|
43
|
+
## Also a Python library
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from contextzip import get_git_changes, create_zip
|
|
47
|
+
|
|
48
|
+
collection = get_git_changes()
|
|
49
|
+
pkg = create_zip(collection, output="/tmp/changes.zip")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Full function reference: [contextzip.vercel.app/cli#python-api](https://contextzip.vercel.app/cli#python-api)
|
|
53
|
+
|
|
54
|
+
## Learn more
|
|
55
|
+
|
|
56
|
+
- [Packaging & detection](https://contextzip.vercel.app/features) — frameworks, exclusion rules, safety
|
|
57
|
+
- [AI-powered file selection](https://contextzip.vercel.app/ai-selection) — `--prompt`, Gemini setup
|
|
58
|
+
- [Terminal error watcher](https://contextzip.vercel.app/watch) — `contextzip watch`
|
|
59
|
+
- [Applying changes back](https://contextzip.vercel.app/apply-zip) — `apply-zip`, backups, safety
|
|
60
|
+
- [Configuration](https://contextzip.vercel.app/configuration) — personal & project config, visual config UI
|
|
61
|
+
- [CLI & Python API reference](https://contextzip.vercel.app/cli)
|
|
62
|
+
|
|
63
|
+
## Contributing
|
|
64
|
+
|
|
65
|
+
Contributions are welcome — especially new framework rule sets and edge-case fixes. See [CONTRIBUTING.md](https://github.com/akadeepesh/contextzip/blob/main/CONTRIBUTING.md). Please open an issue before starting a large PR.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT — see [LICENSE](https://github.com/akadeepesh/contextzip/blob/main/LICENSE) for details.
|
|
@@ -33,6 +33,7 @@ def ai_select(
|
|
|
33
33
|
ecosystem: str,
|
|
34
34
|
api_key: str,
|
|
35
35
|
max_files: int | None = None,
|
|
36
|
+
prompt_template: str = "",
|
|
36
37
|
) -> tuple[list[Path], str, str]:
|
|
37
38
|
"""
|
|
38
39
|
Select the minimum relevant files for *prompt* from *resolved.included*.
|
|
@@ -41,6 +42,10 @@ def ai_select(
|
|
|
41
42
|
project's `ai.max_files` preference (.contextzip/config.json). None
|
|
42
43
|
falls back to each backend's own built-in default cap.
|
|
43
44
|
|
|
45
|
+
*prompt_template*, if non-empty, is a project's `ai.prompt_template`
|
|
46
|
+
preference — house conventions prepended to the generated prompt.txt
|
|
47
|
+
ahead of the task description.
|
|
48
|
+
|
|
44
49
|
Returns
|
|
45
50
|
-------
|
|
46
51
|
(selected_paths, prompt_txt, method)
|
|
@@ -78,14 +83,20 @@ def ai_select(
|
|
|
78
83
|
}
|
|
79
84
|
selected_paths = [rel_to_abs[rel] for rel in selected_rel if rel in rel_to_abs]
|
|
80
85
|
|
|
81
|
-
prompt_txt = _build_prompt_txt(
|
|
86
|
+
prompt_txt = _build_prompt_txt(
|
|
87
|
+
prompt, selected_rel, ecosystem, method, prompt_template=prompt_template
|
|
88
|
+
)
|
|
82
89
|
|
|
83
90
|
return selected_paths, prompt_txt, method
|
|
84
91
|
|
|
85
92
|
|
|
86
|
-
def build_prompt_only_txt(
|
|
93
|
+
def build_prompt_only_txt(
|
|
94
|
+
prompt: str, ecosystem: str, prompt_template: str = ""
|
|
95
|
+
) -> str:
|
|
87
96
|
"""Build a minimal prompt.txt when no AI selection was performed."""
|
|
88
|
-
return _build_prompt_txt(
|
|
97
|
+
return _build_prompt_txt(
|
|
98
|
+
prompt, [], ecosystem, USED_GEMINI, prompt_template=prompt_template
|
|
99
|
+
)
|
|
89
100
|
|
|
90
101
|
|
|
91
102
|
# ---------------------------------------------------------------------------
|
|
@@ -125,12 +136,14 @@ def _build_prompt_txt(
|
|
|
125
136
|
selected_rel: list[str],
|
|
126
137
|
ecosystem: str,
|
|
127
138
|
method: str,
|
|
139
|
+
prompt_template: str = "",
|
|
128
140
|
) -> str:
|
|
129
141
|
"""
|
|
130
142
|
Build the prompt.txt to include inside the ZIP.
|
|
131
143
|
|
|
132
|
-
Any AI tool that receives the ZIP immediately sees the
|
|
133
|
-
|
|
144
|
+
Any AI tool that receives the ZIP immediately sees the project's own
|
|
145
|
+
conventions (if any are configured), the task description, the
|
|
146
|
+
framework, and exactly which files were selected and why.
|
|
134
147
|
"""
|
|
135
148
|
|
|
136
149
|
selector_label = (
|
|
@@ -139,12 +152,21 @@ def _build_prompt_txt(
|
|
|
139
152
|
else "contextzip (keyword heuristic — Gemini was rate limited)"
|
|
140
153
|
)
|
|
141
154
|
|
|
142
|
-
lines: list[str] = [
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
lines: list[str] = []
|
|
156
|
+
|
|
157
|
+
if prompt_template:
|
|
158
|
+
lines.append("Project conventions:")
|
|
159
|
+
lines.append(prompt_template)
|
|
160
|
+
lines.append("")
|
|
161
|
+
|
|
162
|
+
lines.extend(
|
|
163
|
+
[
|
|
164
|
+
f"Task: {prompt}",
|
|
165
|
+
"",
|
|
166
|
+
f"Framework: {ecosystem}",
|
|
167
|
+
"",
|
|
168
|
+
]
|
|
169
|
+
)
|
|
148
170
|
|
|
149
171
|
if selected_rel:
|
|
150
172
|
lines.append(f"Files selected by {selector_label}:")
|
|
@@ -573,18 +573,21 @@ def _backup_entries(
|
|
|
573
573
|
return backup_dir
|
|
574
574
|
|
|
575
575
|
|
|
576
|
-
def _move_zip_to_applied(
|
|
576
|
+
def _move_zip_to_applied(
|
|
577
|
+
zip_path: Path, project_dir: Path, workspace_root: Path, retain: int = 1
|
|
578
|
+
) -> Path:
|
|
577
579
|
"""
|
|
578
580
|
Move a consumed inbox zip into .contextzip/inbox/applied/, timestamped,
|
|
579
581
|
so it can't be accidentally re-applied. Zips passed by an explicit path
|
|
580
582
|
outside the inbox are left where the user put them rather than being
|
|
581
583
|
moved unexpectedly.
|
|
582
584
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
585
|
+
Keeps at most *retain* applied zips (default 1 — only the most recent).
|
|
586
|
+
Filenames are timestamp-prefixed, so a lexicographic sort is also a
|
|
587
|
+
chronological one; the oldest are pruned first, before this run's zip
|
|
588
|
+
is moved in, so the folder never holds more than *retain* at once.
|
|
589
|
+
Typically a project's `applied_zip_retention` preference
|
|
590
|
+
(.contextzip/config.json) — raise it to keep a longer audit trail.
|
|
588
591
|
"""
|
|
589
592
|
if zip_path.parent.resolve() != inbox_dir(project_dir).resolve():
|
|
590
593
|
return zip_path
|
|
@@ -592,7 +595,12 @@ def _move_zip_to_applied(zip_path: Path, project_dir: Path, workspace_root: Path
|
|
|
592
595
|
applied_dir = workspace_root / _INBOX_DIRNAME / _APPLIED_DIRNAME
|
|
593
596
|
applied_dir.mkdir(parents=True, exist_ok=True)
|
|
594
597
|
|
|
595
|
-
|
|
598
|
+
retain = max(1, retain)
|
|
599
|
+
existing = sorted(applied_dir.glob("*.zip"))
|
|
600
|
+
# Keep room for the zip we're about to add: prune down to retain - 1
|
|
601
|
+
# existing entries before moving the new one in.
|
|
602
|
+
overflow = len(existing) - (retain - 1)
|
|
603
|
+
for old in existing[: max(0, overflow)]:
|
|
596
604
|
try:
|
|
597
605
|
old.unlink()
|
|
598
606
|
except OSError:
|
|
@@ -607,13 +615,17 @@ def _move_zip_to_applied(zip_path: Path, project_dir: Path, workspace_root: Path
|
|
|
607
615
|
return dest
|
|
608
616
|
|
|
609
617
|
|
|
610
|
-
def execute_plan(plan: ApplyPlan, project_dir: Path) -> ApplyResult:
|
|
618
|
+
def execute_plan(plan: ApplyPlan, project_dir: Path, retain: int = 1) -> ApplyResult:
|
|
611
619
|
"""
|
|
612
620
|
Write every non-unchanged entry from *plan* into the project.
|
|
613
621
|
|
|
614
622
|
Backs up whatever's about to be overwritten first. Assumes the caller
|
|
615
623
|
has already decided to proceed — dry-run and confirmation prompts are
|
|
616
624
|
handled upstream (see cli.py's `apply-zip` command).
|
|
625
|
+
|
|
626
|
+
*retain* caps how many applied zips are kept in
|
|
627
|
+
.contextzip/inbox/applied/ — typically a project's
|
|
628
|
+
`applied_zip_retention` preference (.contextzip/config.json).
|
|
617
629
|
"""
|
|
618
630
|
workspace_root = _workspace_dir(project_dir)
|
|
619
631
|
to_write = plan.writable_entries
|
|
@@ -627,7 +639,9 @@ def execute_plan(plan: ApplyPlan, project_dir: Path) -> ApplyResult:
|
|
|
627
639
|
shutil.copy2(entry.extracted_path, dest)
|
|
628
640
|
written.append(entry.rel_path)
|
|
629
641
|
|
|
630
|
-
applied_zip_path = _move_zip_to_applied(
|
|
642
|
+
applied_zip_path = _move_zip_to_applied(
|
|
643
|
+
plan.zip_path, project_dir, workspace_root, retain=retain
|
|
644
|
+
)
|
|
631
645
|
shutil.rmtree(plan.extraction_dir, ignore_errors=True)
|
|
632
646
|
|
|
633
647
|
return ApplyResult(
|
|
@@ -373,6 +373,7 @@ def cmd_apply_zip(
|
|
|
373
373
|
.contextzip/backups/<timestamp>/.
|
|
374
374
|
"""
|
|
375
375
|
project_dir = Path(os.getcwd()).resolve()
|
|
376
|
+
project_cfg = load_project_config(project_dir)
|
|
376
377
|
|
|
377
378
|
console.print()
|
|
378
379
|
console.print(
|
|
@@ -436,7 +437,7 @@ def cmd_apply_zip(
|
|
|
436
437
|
return
|
|
437
438
|
console.print()
|
|
438
439
|
|
|
439
|
-
result = execute_plan(plan, project_dir)
|
|
440
|
+
result = execute_plan(plan, project_dir, retain=project_cfg.applied_zip_retention)
|
|
440
441
|
print_apply_result(result)
|
|
441
442
|
|
|
442
443
|
|
|
@@ -922,6 +923,9 @@ def _run(
|
|
|
922
923
|
resolved = resolve_files_from_git(
|
|
923
924
|
git_files=git_result.files,
|
|
924
925
|
project_dir=project_dir,
|
|
926
|
+
large_file_warn_bytes=int(
|
|
927
|
+
project_cfg.limits.max_file_size_mb * 1024 * 1024
|
|
928
|
+
),
|
|
925
929
|
)
|
|
926
930
|
|
|
927
931
|
else:
|
|
@@ -960,11 +964,18 @@ def _run(
|
|
|
960
964
|
spec=spec,
|
|
961
965
|
include_only=include_only if include_only else None,
|
|
962
966
|
force_include=force_include,
|
|
967
|
+
large_file_warn_bytes=int(
|
|
968
|
+
project_cfg.limits.max_file_size_mb * 1024 * 1024
|
|
969
|
+
),
|
|
963
970
|
)
|
|
964
971
|
|
|
965
972
|
# ── File scan summary + warnings ─────────────────────────────────────────
|
|
966
973
|
print_scan_summary(resolved, project_dir, verbose, git_mode=git_changes)
|
|
967
|
-
print_file_warnings(
|
|
974
|
+
print_file_warnings(
|
|
975
|
+
resolved,
|
|
976
|
+
project_dir,
|
|
977
|
+
large_file_warn_bytes=int(project_cfg.limits.max_file_size_mb * 1024 * 1024),
|
|
978
|
+
)
|
|
968
979
|
|
|
969
980
|
# ── Dry run ──────────────────────────────────────────────────────────────
|
|
970
981
|
if dry_run:
|
|
@@ -1026,6 +1037,7 @@ def _run(
|
|
|
1026
1037
|
ecosystem=detection.display_name,
|
|
1027
1038
|
api_key=api_key,
|
|
1028
1039
|
max_files=project_cfg.ai.max_files,
|
|
1040
|
+
prompt_template=project_cfg.ai.prompt_template,
|
|
1029
1041
|
)
|
|
1030
1042
|
|
|
1031
1043
|
if not selected_paths:
|
|
@@ -61,6 +61,7 @@ def run_ai_selection(
|
|
|
61
61
|
ecosystem: str,
|
|
62
62
|
api_key: str,
|
|
63
63
|
max_files: int | None = None,
|
|
64
|
+
prompt_template: str = "",
|
|
64
65
|
con: Console = console,
|
|
65
66
|
) -> tuple[list[Path], str]:
|
|
66
67
|
"""
|
|
@@ -70,6 +71,9 @@ def run_ai_selection(
|
|
|
70
71
|
project's `ai.max_files` preference (.contextzip/config.json). None
|
|
71
72
|
falls back to each backend's own built-in default.
|
|
72
73
|
|
|
74
|
+
*prompt_template*, if non-empty, is a project's `ai.prompt_template`
|
|
75
|
+
preference, prepended to the generated prompt.txt.
|
|
76
|
+
|
|
73
77
|
Handles progress display, heuristic-fallback warning, and Gemini
|
|
74
78
|
errors. Raises SystemExit(1) on hard failure.
|
|
75
79
|
"""
|
|
@@ -86,6 +90,7 @@ def run_ai_selection(
|
|
|
86
90
|
ecosystem=ecosystem,
|
|
87
91
|
api_key=api_key,
|
|
88
92
|
max_files=max_files,
|
|
93
|
+
prompt_template=prompt_template,
|
|
89
94
|
)
|
|
90
95
|
except GeminiError as exc:
|
|
91
96
|
con.print(
|
|
@@ -170,14 +170,25 @@ def print_scan_summary(
|
|
|
170
170
|
# ---------------------------------------------------------------------------
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
def print_file_warnings(
|
|
174
|
-
|
|
173
|
+
def print_file_warnings(
|
|
174
|
+
resolved,
|
|
175
|
+
project_dir: Path,
|
|
176
|
+
*,
|
|
177
|
+
large_file_warn_bytes: int = LARGE_FILE_WARN_BYTES,
|
|
178
|
+
con: Console = console,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Render large-file, binary-file, and skipped-file warnings.
|
|
181
|
+
|
|
182
|
+
*large_file_warn_bytes* only affects the displayed threshold text —
|
|
183
|
+
typically a project's `limits.max_file_size_mb` preference — the actual
|
|
184
|
+
filtering already happened in resolve_files()/resolve_files_from_git().
|
|
185
|
+
"""
|
|
175
186
|
if resolved.large_files:
|
|
176
187
|
con.print()
|
|
177
188
|
con.print(
|
|
178
189
|
f" [yellow]⚠[/] [bold]{len(resolved.large_files)} large file"
|
|
179
190
|
f"{'s' if len(resolved.large_files) != 1 else ''}[/] "
|
|
180
|
-
f"[dim](≥ {human_size(
|
|
191
|
+
f"[dim](≥ {human_size(large_file_warn_bytes)}) will be included:[/]"
|
|
181
192
|
)
|
|
182
193
|
for p, size in resolved.large_files[:5]:
|
|
183
194
|
rel = p.relative_to(project_dir).as_posix()
|
|
@@ -269,6 +269,7 @@ def resolve_files(
|
|
|
269
269
|
spec: pathspec.PathSpec,
|
|
270
270
|
include_only: list[str] | None = None,
|
|
271
271
|
force_include: pathspec.PathSpec | None = None,
|
|
272
|
+
large_file_warn_bytes: int = LARGE_FILE_WARN_BYTES,
|
|
272
273
|
) -> ResolveResult:
|
|
273
274
|
"""
|
|
274
275
|
Walk *project_dir* and classify every file.
|
|
@@ -281,6 +282,9 @@ def resolve_files(
|
|
|
281
282
|
an explicit `contextzip include PATH` for this run still has the final
|
|
282
283
|
say on what actually gets included.
|
|
283
284
|
|
|
285
|
+
*large_file_warn_bytes* overrides the module default (1 MB) — typically
|
|
286
|
+
a project's `limits.max_file_size_mb` preference (.contextzip/config.json).
|
|
287
|
+
|
|
284
288
|
Returns a :class:`ResolveResult` with included/excluded/skipped/large/binary lists.
|
|
285
289
|
"""
|
|
286
290
|
result = ResolveResult()
|
|
@@ -347,7 +351,7 @@ def resolve_files(
|
|
|
347
351
|
continue
|
|
348
352
|
|
|
349
353
|
# ── Large file warning ───────────────────────────────────────────────
|
|
350
|
-
if file_size >=
|
|
354
|
+
if file_size >= large_file_warn_bytes:
|
|
351
355
|
result.large_files.append((abs_path, file_size))
|
|
352
356
|
|
|
353
357
|
# ── Binary file detection ────────────────────────────────────────────
|
|
@@ -363,6 +367,7 @@ def resolve_files(
|
|
|
363
367
|
def resolve_files_from_git(
|
|
364
368
|
git_files: list[Path],
|
|
365
369
|
project_dir: Path,
|
|
370
|
+
large_file_warn_bytes: int = LARGE_FILE_WARN_BYTES,
|
|
366
371
|
) -> ResolveResult:
|
|
367
372
|
"""
|
|
368
373
|
Build a :class:`ResolveResult` from a pre-selected list of files reported
|
|
@@ -380,6 +385,9 @@ def resolve_files_from_git(
|
|
|
380
385
|
:attr:`GitChanges.files`.
|
|
381
386
|
project_dir:
|
|
382
387
|
Absolute path to the project root (used for relative-path guards).
|
|
388
|
+
large_file_warn_bytes:
|
|
389
|
+
Overrides the module default (1 MB) — typically a project's
|
|
390
|
+
`limits.max_file_size_mb` preference (.contextzip/config.json).
|
|
383
391
|
"""
|
|
384
392
|
# Build the base spec once — this is our safety floor regardless of what
|
|
385
393
|
# git reports as changed. It blocks .env, secrets, binaries, etc.
|
|
@@ -423,7 +431,7 @@ def resolve_files_from_git(
|
|
|
423
431
|
continue
|
|
424
432
|
|
|
425
433
|
# ── Large file warning ───────────────────────────────────────────────
|
|
426
|
-
if file_size >=
|
|
434
|
+
if file_size >= large_file_warn_bytes:
|
|
427
435
|
result.large_files.append((abs_path, file_size))
|
|
428
436
|
|
|
429
437
|
# ── Binary file detection ────────────────────────────────────────────
|