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.
Files changed (50) hide show
  1. contextzip-0.4.0/PKG-INFO +98 -0
  2. contextzip-0.4.0/README.md +69 -0
  3. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/__init__.py +1 -1
  4. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/ai/selector.py +33 -11
  5. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/applier.py +23 -9
  6. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/cli.py +14 -2
  7. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/cli_ai.py +5 -0
  8. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/cli_display.py +14 -3
  9. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/filters.py +10 -2
  10. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/project_config.py +137 -9
  11. contextzip-0.4.0/contextzip/webui/assets.py +1185 -0
  12. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/webui/persist.py +32 -4
  13. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/webui/server.py +124 -6
  14. contextzip-0.4.0/contextzip.egg-info/PKG-INFO +98 -0
  15. {contextzip-0.3.9 → contextzip-0.4.0}/pyproject.toml +1 -1
  16. contextzip-0.3.9/PKG-INFO +0 -350
  17. contextzip-0.3.9/README.md +0 -321
  18. contextzip-0.3.9/contextzip/webui/assets.py +0 -1012
  19. contextzip-0.3.9/contextzip.egg-info/PKG-INFO +0 -350
  20. {contextzip-0.3.9 → contextzip-0.4.0}/LICENSE +0 -0
  21. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/ai/__init__.py +0 -0
  22. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/ai/gemini.py +0 -0
  23. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/ai/heuristic.py +0 -0
  24. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/api.py +0 -0
  25. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/cli_onboard.py +0 -0
  26. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/clipboard.py +0 -0
  27. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/config.py +0 -0
  28. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/detector.py +0 -0
  29. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/error_parser.py +0 -0
  30. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/git.py +0 -0
  31. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/packager.py +0 -0
  32. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/__init__.py +0 -0
  33. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/base.py +0 -0
  34. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/errors/__init__.py +0 -0
  35. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/errors/node.py +0 -0
  36. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/errors/python.py +0 -0
  37. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/go.py +0 -0
  38. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/node.py +0 -0
  39. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/python.py +0 -0
  40. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/ruby.py +0 -0
  41. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/rules/rust.py +0 -0
  42. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/watcher.py +0 -0
  43. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/webui/__init__.py +0 -0
  44. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip/webui/suggestions.py +0 -0
  45. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/SOURCES.txt +0 -0
  46. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/dependency_links.txt +0 -0
  47. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/entry_points.txt +0 -0
  48. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/requires.txt +0 -0
  49. {contextzip-0.3.9 → contextzip-0.4.0}/contextzip.egg-info/top_level.txt +0 -0
  50. {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.
@@ -1,6 +1,6 @@
1
1
  """contextzip — intelligent codebase packager for AI tools."""
2
2
 
3
- __version__ = "0.3.9"
3
+ __version__ = "0.4.0"
4
4
 
5
5
  from contextzip.api import (
6
6
  FileCollection,
@@ -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(prompt, selected_rel, ecosystem, method)
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(prompt: str, ecosystem: str) -> str:
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(prompt, [], ecosystem, USED_GEMINI)
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 task description,
133
- the framework, and exactly which files were selected and why.
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
- f"Task: {prompt}",
144
- "",
145
- f"Framework: {ecosystem}",
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(zip_path: Path, project_dir: Path, workspace_root: Path) -> Path:
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
- Only the most recently applied zip is kept: anything already in
584
- applied/ from a prior run is removed first, so this folder holds one
585
- zip at most and doesn't grow across sessions. If keeping a full
586
- history ever matters, that's a deliberate future feature, not a
587
- default behavior.
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
- for old in applied_dir.glob("*.zip"):
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(plan.zip_path, project_dir, workspace_root)
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(resolved, project_dir)
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(resolved, project_dir: Path, *, con: Console = console) -> None:
174
- """Render large-file, binary-file, and skipped-file warnings."""
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(LARGE_FILE_WARN_BYTES)}) will be included:[/]"
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 >= LARGE_FILE_WARN_BYTES:
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 >= LARGE_FILE_WARN_BYTES:
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 ────────────────────────────────────────────