gitpr-cli 0.0.26__py3-none-any.whl → 0.0.28__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.26
3
+ Version: 0.0.28
4
4
  Summary: Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)
5
5
  Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
6
6
  Requires-Python: >=3.10
@@ -14,12 +14,20 @@ Requires-Dist: cryptography
14
14
  Requires-Dist: pyyaml
15
15
  Requires-Dist: textual
16
16
  Requires-Dist: requests
17
+ Requires-Dist: mcp>=1.0.0
17
18
  Dynamic: license-file
18
19
 
20
+
19
21
  # **GitPR CLI 🚀**
20
22
 
23
+ <p align="center">
24
+ <img src="https://raw.githubusercontent.com/natanfiuza/gitpr/main/docs/logo.png" alt="GitPR Logo" width="150">
25
+ </p>
26
+
21
27
  GitPR CLI is a command-line automation tool that uses **Google Gemini** and **DeepSeek** artificial intelligence to analyze your code changes (git diff) or entire files. The tool automatically generates commit messages in the *Conventional Commits* standard, detailed Pull Request descriptions, and deep Code Reviews aimed at reducing technical debt.
22
28
 
29
+ 🌐 **Website:** [gitpr.natanfiuza.dev.br](https://gitpr.natanfiuza.dev.br/) · 📂 **Repository:** [github.com/natanfiuza/gitpr](https://github.com/natanfiuza/gitpr)
30
+
23
31
  ## **🛠️ Technologies and Libraries Used**
24
32
 
25
33
  This project was developed in Python and uses the following main libraries:
@@ -33,6 +41,7 @@ This project was developed in Python and uses the following main libraries:
33
41
  * [**PyYAML**](https://pyyaml.org/): Used to read and process the custom static analysis rules from the `.gitpr.linter.yml` file.
34
42
  * [**Textual**](https://textual.textualize.io/): Powerful library for creating Terminal Graphical Interfaces (TUI), used in the interactive issue generation and editing panel.
35
43
  * [**Requests**](https://pypi.org/project/requests/): Elegant and robust library for HTTP requests, used to communicate with the GitHub REST API.
44
+ * [**MCP**](https://pypi.org/project/mcp/): Official Python SDK for the Model Context Protocol, enabling GitPR to integrate directly with AI-powered editors and IDEs.
36
45
 
37
46
  ----
38
47
 
@@ -130,6 +139,8 @@ You can pass the following *flags* for specific actions:
130
139
  * `--lang <code>`: Forces the interface language for this execution (e.g.: `en_us`, `pt_br`). Overrides `GITPR_LANG` in `.env` without persisting the change.
131
140
  * `-ch` or `--chat`: Opens the **Interactive Pair Programming Chat** — a TUI terminal where the AI sees your current diff and maintains a contextual conversation. Features memory per branch, slash commands (`/explain`, `/tests`, `/optimize`, `/clear`), auto-patching (F5), diff refresh (F2), and session export (F6).
132
141
  * `-l` or `--linter`: Runs **only the local static linter** (no AI calls). Ideal for use in CI/CD pipelines to block non-compliant code.
142
+ * `--mcp`: Starts GitPR as an **MCP server** (Model Context Protocol) on stdio transport. Enables integration with VS Code, Cursor, Claude Desktop, and other MCP-compatible editors — exposing all GitPR AI capabilities as tools directly inside your IDE. Also available as the standalone `gitpr-mcp` command.
143
+ * `--install`: **Interactive Setup Wizard.** Runs a guided 4-step setup: downloads skill templates, installs Git hooks, configures MCP for detected editors, and checks/requests your AI provider API key. Each step asks for confirmation before proceeding.
133
144
  * `-ih` or `--installhooks`: Automatically installs **local Git Hooks** (`pre-commit` and `prepare-commit-msg`) in your repository.
134
145
  * `-s` or `--skill`: Creates the AI context template files (`.gitpr.commit.md`, `.gitpr.pr.md`, `.gitpr.review.md`, `.gitpr.filereview.md`, `.gitpr.issue.md`, `.gitpr.blame.md`) and the Linter (`.gitpr.linter.yml`) at the project root.
135
146
  * `-is` or `--issue`: Automatically generates a draft of a **standardized Issue** and opens an interactive interface (TUI) for editing or direct submission via REST API. This feature has **3 context engines** depending on the command combination:
@@ -140,6 +151,14 @@ You can pass the following *flags* for specific actions:
140
151
  * `-u` or `--update`: Checks and installs the latest version of GitPR (Auto-Updater).
141
152
 
142
153
  > **⚙️ Technical Note (--hook):** GitPR has a hidden flag `--hook <file>` that is triggered exclusively by the Git Hooks system in the background. It allows the AI to inject the suggested message directly into Git's temporary file, without cluttering your terminal.
154
+ >
155
+ > **⚙️ Technical Note (--pre-save):** GitPR has a hidden debug flag `--pre-save` that can be combined with any AI command (e.g.: `gitpr -c --pre-save`). Before each AI call, it saves the full payload that will be sent to the model (system instruction + prompt + character counters) to a `_{action}-{datetime}.json` file in the current folder, and then proceeds normally. Useful for inspecting very large prompts. Note: when the response comes from the local cache, no call is made and no file is generated.
156
+
157
+ ### 📦 Huge Diffs (Map-Reduce)
158
+
159
+ When your diff is too large for a single AI call (over ~90k estimated tokens), GitPR automatically splits it into batches by file, asks the AI for a technical summary of each part (Map), and unifies everything into the final commit message, review, or PR description (Reduce). No flags needed — it activates on demand and shows the progress in the console.
160
+
161
+ 📚 Full documentation: [docs/map-reduce-diff.md](https://github.com/natanfiuza/gitpr/blob/main/docs/map-reduce-diff.md)
143
162
 
144
163
  ## 🛡️ Local Linter (Static Analysis)
145
164
 
@@ -194,7 +213,64 @@ GitPR automatically detects your system language and displays messages in your n
194
213
 
195
214
  To force a specific language, set `GITPR_LANG=pt_br` or `GITPR_LANG=en` in `~/.gitpr/.env`.
196
215
 
197
- > 📖 **Full developer guide:** [docs/i18n_explanation.md](docs/i18n_explanation.md) — architecture, usage patterns, circular import precautions, and how to add new languages.
216
+ > 📖 **Full developer guide:** [docs/i18n_explanation.md](https://github.com/natanfiuza/gitpr/blob/main/docs/i18n_explanation.md) — architecture, usage patterns, circular import precautions, and how to add new languages.
217
+
218
+ ## 🔌 MCP Integration (Model Context Protocol)
219
+
220
+ GitPR can run as an **MCP server**, exposing its AI-powered capabilities as tools that your editor's AI assistant can invoke directly — no terminal needed. This enables a fully integrated workflow where you can generate commit messages, review code, run linters, trace code origins, and create issues without leaving your IDE.
221
+
222
+ ### Supported Editors
223
+
224
+ | Editor | Config File |
225
+ | ------ | ----------- |
226
+ | **VS Code** | `.vscode/mcp.json` |
227
+ | **Cursor** | `.cursor/mcp.json` |
228
+ | **Claude Code** | `.mcp.json` |
229
+ | **Claude Desktop** | `claude_desktop_config.json` |
230
+ | **Zed** | `settings.json` |
231
+
232
+ ### Quick Setup
233
+
234
+ Use the built-in installer to configure your editor automatically:
235
+
236
+ ```bash
237
+ gitpr-mcp --install vscode # Creates .vscode/mcp.json
238
+ gitpr-mcp --install cursor # Creates .cursor/mcp.json
239
+ gitpr-mcp --install claude-code # Creates .mcp.json
240
+ gitpr-mcp --install claude # Updates Claude Desktop config
241
+ gitpr-mcp --install zed # Updates Zed settings
242
+ gitpr-mcp --install auto # Auto-detect and install for all found
243
+ ```
244
+
245
+ The installer creates the config directory if needed, merges with any existing
246
+ config (never overwrites other servers), and is safe to run multiple times.
247
+
248
+ > Manual setup is also supported — see [docs/mcp-integration.md](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-integration.md)
249
+ > for the JSON config format for each editor.
250
+
251
+ Once configured, use natural language in your editor's AI chat:
252
+
253
+ * *"Review my current changes"* → calls `review_code`
254
+ * *"Generate a commit message"* → calls `generate_commit_message`
255
+ * *"Create a PR description"* → calls `generate_pr_description`
256
+ * *"Run the linter on my diff"* → calls `run_linter`
257
+
258
+ ### Available MCP Tools
259
+
260
+ | Tool | Description |
261
+ | ---- | ----------- |
262
+ | `get_git_context` | Current branch, repository name, and remote URL |
263
+ | `analyze_diff` | Git diff of uncommitted changes |
264
+ | `get_full_diff` | Full diff against origin/main |
265
+ | `generate_commit_message` | AI-generated Conventional Commits message |
266
+ | `review_code` | AI code review of local changes |
267
+ | `full_review` | AI code review of all changes since origin/main |
268
+ | `generate_pr_description` | Complete PR description (title + body) |
269
+ | `run_linter` | Static linter against `.gitpr.linter.yml` |
270
+ | `analyze_blame` | Git blame + AI classification |
271
+ | `generate_issue` | Structured issue from diff, history, or blame |
272
+
273
+ 📖 **Full documentation:** [docs/mcp-integration.md](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-integration.md) — available in 5 languages (EN, PT-BR, PT-PT, ES, FR).
198
274
 
199
275
  ## 📚 Technical Documentation and Advanced Guides
200
276
 
@@ -225,10 +301,12 @@ If you want to implement GitPR as an automated quality barrier in your team, che
225
301
 
226
302
  ### Configuration & Infrastructure
227
303
 
304
+ * [**Install Wizard**](https://github.com/natanfiuza/gitpr/blob/main/docs/install-wizard.md) — Step-by-step guided setup for configuring GitPR in a new project.
228
305
  * [**AI Providers**](https://github.com/natanfiuza/gitpr/blob/main/docs/providers-ia.md) — Configuration and selection between Google Gemini, DeepSeek, and Ollama.
229
306
  * [**Auto-Updater**](https://github.com/natanfiuza/gitpr/blob/main/docs/auto-update.md) — How GitPR's automatic update (hot-swap) works.
230
307
  * [**GitHub Token (PAT) Integration and Security**](https://github.com/natanfiuza/gitpr/blob/main/docs/github-pat-integration.md) — Understand how GitPR creates issues directly in the repository with authentication.
231
308
  * [**Internationalization (i18n)**](https://github.com/natanfiuza/gitpr/blob/main/docs/i18n_explanation.md) — Architecture, usage patterns, and how to add new languages.
309
+ * [**MCP Integration**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-integration.md) — Connect GitPR to VS Code, Cursor, and Claude Desktop via Model Context Protocol.
232
310
 
233
311
  ## ⚡ Local Cache System (Quota Savings)
234
312
 
@@ -0,0 +1,26 @@
1
+ gitpr_cli-0.0.28.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
2
+ src/__init__.py,sha256=U6ksarernNcryHO20aj4rNG7w0thT7xdMs5KzBJyPvE,28
3
+ src/ai_providers.py,sha256=cxPuytZ9LfvhQbWFPBJ4dud3lHLpAQpa3cbY8bKESk8,10954
4
+ src/blame_engine.py,sha256=z-uh8mHOF--JxY_nnbp5r_9y_a_IO4Zn42P0AeLBpto,10436
5
+ src/cache.py,sha256=XggMTy00GlZfeF1VoLEYCdxL5h0eTg11TJfPIRPx7fs,3201
6
+ src/chat_memory.py,sha256=vdYuElIQb6-flFm4SfzapnQZGRIwgbLECzIcsAc0Oo8,8724
7
+ src/config.py,sha256=VJ9ekDlTVG5ETWDuO1kZWlgrncXbrvduxa5Zg7wN_ZY,9064
8
+ src/core.py,sha256=PqwqWHA5h_EplQ6mURxriS5Ql9_QPq73S2R9zJ11-78,28580
9
+ src/i18n.py,sha256=QED7hTSdWCC1lSNJ-l6VuOba18OrWgFTMQcZ7cQmXbY,3599
10
+ src/issue_engine.py,sha256=XJ7WNLoeg3wct079wS7OQqU62f_avn3A8LmMuPWchzE,3642
11
+ src/linter_engine.py,sha256=D04wvOgRa-vZEYknm-z-Cg7V_3gnoQz_64PMorWnISo,4414
12
+ src/main.py,sha256=BoI2aN3l1fS4UzoWL67Yw2QtkeRxh9ITuVIQEc9JOPA,32724
13
+ src/mcp_server.py,sha256=14BRJX7-etSPztBdUfzocbL9KML99nogQfjkZFRFFIU,31406
14
+ src/security.py,sha256=Li0zM8ZURTK2nKRHIt49TWLO4DIddBEHzXPrVycv0Rs,1343
15
+ src/spinner.py,sha256=16ngB2oT1t7O-c3D2DWCJbFb9nMyD-HWv6KcvhKTelQ,7651
16
+ src/tui_issue.py,sha256=gcAJCIfMcdeMjrFJUWQQvdgBv7fnOBtxQxwdBk2Sk40,1789
17
+ src/updater.py,sha256=M30nc_eAO7CZl6628TkSwuTwXbzZ9sdIgaoilLL1z2A,5886
18
+ src/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ src/ui/chat_app.py,sha256=y-py5UpisQbPLqqDON8AIkyA7X1IM5LAn1GbeYr2p_U,25598
20
+ src/ui/help_screen.py,sha256=tjbokXhwyzLnkOC0S56NbpX-78hcFQnvsQMpNU1wt0c,1862
21
+ src/ui/issue_app.py,sha256=8MdBcOVdCHPFx6EUhs80CiZ2G-29KulskkCz9qMZ_Lg,4737
22
+ gitpr_cli-0.0.28.dist-info/METADATA,sha256=w1I_R_77I4BP4ajS81C7KcE_PSxpSGqaECzTayg64iM,22656
23
+ gitpr_cli-0.0.28.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
24
+ gitpr_cli-0.0.28.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
25
+ gitpr_cli-0.0.28.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
26
+ gitpr_cli-0.0.28.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  gitpr = src.main:cli
3
+ gitpr-mcp = src.mcp_server:main
src/ai_providers.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import time
3
+ from datetime import datetime
3
4
  import click
4
5
  from google import genai
5
6
  from openai import OpenAI
@@ -10,13 +11,65 @@ from src.spinner import Spinner
10
11
  from src.i18n import __,CURRENT_LANG
11
12
 
12
13
 
13
- def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quiet=False):
14
+ # Hidden --pre-save debug flag: when enabled, every AI payload is dumped
15
+ # to a JSON file in the current directory before being sent to the model.
16
+ PRE_SAVE_ENABLED = False
17
+
18
+
19
+ def set_pre_save(enabled):
20
+ """Enable/disable the pre-save payload dump (set once from the CLI)."""
21
+ global PRE_SAVE_ENABLED
22
+ PRE_SAVE_ENABLED = enabled
23
+
24
+
25
+ def _save_pre_save_payload(action, provider, api_model, system_instruction, prompt=None, chat_history=None, new_message=None):
26
+ """
27
+ Dump the full AI payload to a _{action}-{datetime}.json file in the current directory.
28
+ Returns the filename on success or None on failure (a debug dump must never break the flow).
29
+ """
30
+ filename = f"_{action}-{datetime.now().strftime('%Y%m%d%H%M%S%f')}.json"
31
+
32
+ payload = {
33
+ "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
34
+ "action": action,
35
+ "provider": provider,
36
+ "model": api_model,
37
+ "system_instruction": system_instruction,
38
+ "system_instruction_chars": len(system_instruction) if system_instruction else 0,
39
+ }
40
+
41
+ if chat_history is not None:
42
+ payload["chat_history"] = chat_history
43
+ payload["new_message"] = new_message
44
+ payload["chat_history_chars"] = len(json.dumps(chat_history, ensure_ascii=False)) if chat_history else 0
45
+ payload["new_message_chars"] = len(new_message) if new_message else 0
46
+ payload["total_chars"] = payload["system_instruction_chars"] + payload["chat_history_chars"] + payload["new_message_chars"]
47
+ else:
48
+ payload["prompt"] = prompt
49
+ payload["prompt_chars"] = len(prompt) if prompt else 0
50
+ payload["total_chars"] = payload["system_instruction_chars"] + payload["prompt_chars"]
51
+
52
+ try:
53
+ with open(filename, "w", encoding="utf-8") as f:
54
+ json.dump(payload, f, ensure_ascii=False, indent=2)
55
+ return filename
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quiet=False, action="ai_call"):
14
61
  """
15
62
  Unified engine for AI calls.
16
63
  Supports 'gemini' and 'deepseek'.
17
64
  """
18
65
  max_retries = 3
19
66
  retry_delay = 2
67
+
68
+ if PRE_SAVE_ENABLED:
69
+ saved_file = _save_pre_save_payload(action, provider, api_model, system_instruction, prompt=prompt)
70
+ if saved_file and not quiet:
71
+ click.secho(__("📝 Pre-save: AI payload saved to {filename}", filename=saved_file), fg="yellow", dim=True)
72
+
20
73
  spinner = Spinner(quiet=quiet)
21
74
  spinner.start()
22
75
 
@@ -141,6 +194,11 @@ def call_ai_chat(provider, api_key, api_model, system_instruction, chat_history,
141
194
  Dedicated engine for the Interactive Chat.
142
195
  Keeps the historical context and returns free Markdown (does not force JSON).
143
196
  """
197
+ if PRE_SAVE_ENABLED:
198
+ saved_file = _save_pre_save_payload("chat", provider, api_model, system_instruction, chat_history=chat_history, new_message=new_message)
199
+ if saved_file and not quiet:
200
+ click.secho(__("📝 Pre-save: AI payload saved to {filename}", filename=saved_file), fg="yellow", dim=True)
201
+
144
202
  spinner = Spinner(quiet=quiet)
145
203
  spinner.start()
146
204
 
src/blame_engine.py CHANGED
@@ -83,7 +83,7 @@ def analyze_commit_with_ai(commit_hash, file_path):
83
83
 
84
84
  click.secho(__(" 🤖 Consulting AI ({api_model}) about commit {commit_hash}...", api_model=api_model, commit_hash=commit_hash[:8]), fg="cyan", dim=True)
85
85
 
86
- result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst)
86
+ result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst, action="blame")
87
87
 
88
88
  if result_json and "status" in result_json:
89
89
  return result_json
@@ -210,7 +210,7 @@ def run_blame_analysis(file_path, start_line, end_line, return_data=False):
210
210
  sys_inst = __('You are a Software Architect. Generate ONLY a JSON object in the format {"resumo": "summary text"}.')
211
211
 
212
212
  click.secho(__(" 🤖 Consulting AI ({api_model}) for the Executive Summary...", api_model=api_model), fg="cyan", dim=True)
213
- summary_json = call_ai_model(provider, api_key, api_model, summary_prompt, sys_inst)
213
+ summary_json = call_ai_model(provider, api_key, api_model, summary_prompt, sys_inst, action="blame_summary")
214
214
 
215
215
  resumo_texto = summary_json.get("resumo", __("Summary not available.")) if summary_json else __("Summary not available.")
216
216
 
src/core.py CHANGED
@@ -2,42 +2,114 @@ import os
2
2
  import re
3
3
  import json
4
4
  import stat
5
+ import time
5
6
  import click
6
7
  import subprocess
7
8
  import urllib.request
8
9
  import urllib.error
10
+ from pathlib import Path
9
11
  from google import genai
12
+ from dotenv import load_dotenv, set_key
10
13
  from src.security import decrypt_data
11
14
  from src.cache import get_cached_response, save_cached_response, get_cached_pr_descriptions
12
- from src.config import get_api_key, get_api_model, get_skill_dir, resolve_skill_path
15
+ from src.config import get_api_key, get_api_model, get_skill_dir, resolve_skill_path, get_ai_provider, setup_environment
13
16
  from src.ai_providers import call_ai_model
14
17
  from src.i18n import __, CURRENT_LANG
15
-
16
- # Filtro Smart Diff: ficheiros que gastam tokens da IA sem acrescentar valor semântico
17
- SMART_EXCLUDES = [
18
- ":(exclude)*.lock",
19
- ":(exclude)package-lock.json",
20
- ":(exclude)yarn.lock",
21
- ":(exclude)pnpm-lock.yaml",
22
- ":(exclude)composer.lock",
23
- ":(exclude)poetry.lock",
24
- ":(exclude)Pipfile.lock",
25
- ":(exclude)Gemfile.lock",
26
- ":(exclude)go.sum",
27
- ":(exclude)*.min.js",
28
- ":(exclude)*.min.css",
29
- ":(exclude)*.svg"
18
+ from src.updater import __lang_version__
19
+
20
+ # Smart Diff filter: files that consume AI tokens without adding semantic value.
21
+ # The pattern list is managed remotely (templates/gitpr.smart-excludes.json) and
22
+ # cached at ~/.gitpr/conf/ — see _load_smart_excludes() for the update logic.
23
+ _FALLBACK_SMART_EXCLUDES = [
24
+ "*.lock",
25
+ "package-lock.json",
26
+ "yarn.lock",
27
+ "pnpm-lock.yaml",
28
+ "composer.lock",
29
+ "poetry.lock",
30
+ "Pipfile.lock",
31
+ "Gemfile.lock",
32
+ "go.sum",
33
+ "*.min.js",
34
+ "*.min.css",
35
+ "*.svg"
30
36
  ]
31
37
 
38
+ SMART_EXCLUDES_URL = "https://raw.githubusercontent.com/natanfiuza/gitpr/main/templates/gitpr.smart-excludes.json"
39
+
40
+
41
+ def _load_smart_excludes():
42
+ """
43
+ Load the smart-exclude patterns and return them as git pathspec exclusions.
44
+
45
+ Resolution order:
46
+ 1. Local copy at ~/.gitpr/conf/gitpr.smart-excludes.json when its version
47
+ marker (SMART_EXCLUDES_VERSION in ~/.gitpr/.env) matches __lang_version__.
48
+ 2. Fresh download from the remote template (saved locally + marker updated).
49
+ 3. Stale local copy when the download fails.
50
+ 4. _FALLBACK_SMART_EXCLUDES as last resort.
51
+ Silent on failure — diff generation must never break because of this list.
52
+ """
53
+ env_file = Path.home() / ".gitpr" / ".env"
54
+ load_dotenv(env_file)
55
+
56
+ conf_dir = Path.home() / ".gitpr" / "conf"
57
+ local_file = conf_dir / "gitpr.smart-excludes.json"
58
+ needs_update = os.getenv("SMART_EXCLUDES_VERSION") != __lang_version__
59
+
60
+ def _to_pathspecs(data):
61
+ return [f":(exclude){pattern}" for pattern in data.get("excludes", [])]
62
+
63
+ # 1. Local copy is present and up to date
64
+ if local_file.exists() and not needs_update:
65
+ try:
66
+ with open(local_file, "r", encoding="utf-8", errors="replace") as f:
67
+ return _to_pathspecs(json.load(f))
68
+ except Exception:
69
+ pass
70
+
71
+ # 2. Download the updated list from the remote template
72
+ try:
73
+ with urllib.request.urlopen(SMART_EXCLUDES_URL, timeout=3) as response:
74
+ data = json.loads(response.read().decode("utf-8"))
75
+ conf_dir.mkdir(parents=True, exist_ok=True)
76
+ with open(local_file, "w", encoding="utf-8") as f:
77
+ json.dump(data, f, ensure_ascii=False, indent=2)
78
+ set_key(str(env_file), "SMART_EXCLUDES_VERSION", __lang_version__)
79
+ return _to_pathspecs(data)
80
+ except Exception:
81
+ pass
82
+
83
+ # 3. Offline fallback: reuse the local copy even if outdated
84
+ if local_file.exists():
85
+ try:
86
+ with open(local_file, "r", encoding="utf-8", errors="replace") as f:
87
+ return _to_pathspecs(json.load(f))
88
+ except Exception:
89
+ pass
90
+
91
+ # 4. Last resort: built-in defaults
92
+ return [f":(exclude){pattern}" for pattern in _FALLBACK_SMART_EXCLUDES]
93
+
94
+
95
+ SMART_EXCLUDES = _load_smart_excludes()
96
+
32
97
 
33
98
  def get_doc_url(filename):
34
- """Returns the complete URL for a docs/ file, with language suffix if needed."""
35
- # e.g.: get_doc_url("untracked-files.md") -> ".../docs/untracked-files.md" (EN) or ".../docs/untracked-files.pt_br.md" (PT)
36
- if CURRENT_LANG.startswith("en"):
37
- return f"https://github.com/natanfiuza/gitpr/blob/main/docs/{filename}"
38
- else:
39
- base, ext = filename.rsplit(".", 1)
40
- return f"https://github.com/natanfiuza/gitpr/blob/main/docs/{base}.{CURRENT_LANG}.{ext}"
99
+ """Returns the complete URL for the official GitPR documentation website.
100
+
101
+ Transforms a docs/ filename like 'commit-message-ia.md' into a clean website
102
+ URL with language query parameter. English is the site default (no ?lang=).
103
+
104
+ Examples:
105
+ get_doc_url("untracked-files.md") -> "https://gitpr.natanfiuza.dev.br/docs/untracked-files"
106
+ get_doc_url("untracked-files.md") -> "https://gitpr.natanfiuza.dev.br/docs/untracked-files?lang=pt_br" (when CURRENT_LANG is pt_br)
107
+ """
108
+ base, _ = filename.rsplit(".", 1)
109
+ url = f"https://gitpr.natanfiuza.dev.br/docs/{base}"
110
+ if not CURRENT_LANG.startswith("en"):
111
+ url += f"?lang={CURRENT_LANG}"
112
+ return url
41
113
 
42
114
 
43
115
  def get_git_diff(quiet=False):
@@ -62,7 +134,7 @@ def get_git_diff(quiet=False):
62
134
  click.secho(f"📚 {__('Understand why:')} {get_doc_url('untracked-files.md')}\n", fg="blue", underline=True)
63
135
 
64
136
  # Run the normal diff that captures tracked and staged files
65
- cmd = ["git", "diff", "HEAD", "--"] + SMART_EXCLUDES
137
+ cmd = ["git", "diff", "-U1", "-w", "-M", "-B", "HEAD", "--"] + SMART_EXCLUDES
66
138
  result = subprocess.run(
67
139
  cmd,
68
140
  capture_output=True,
@@ -153,6 +225,35 @@ def get_skill_context(action_type="pr"):
153
225
  # Return empty if it does not exist
154
226
  return ""
155
227
 
228
+ def estimate_token_count(text):
229
+ """Estimates the token count using the safe rule of 4 characters per token."""
230
+ return len(text) // 4
231
+
232
+ def split_diff_into_chunks(diff_text, max_tokens=90000):
233
+ """Splits the diff based on the token limit while preserving file header integrity."""
234
+ if estimate_token_count(diff_text) <= max_tokens:
235
+ return [diff_text]
236
+
237
+ parts = re.split(r'(^diff --git a/)', diff_text, flags=re.MULTILINE)
238
+ chunks = []
239
+ current_chunk = ""
240
+
241
+ for i in range(1, len(parts), 2):
242
+ file_diff = parts[i] + parts[i+1] if i + 1 < len(parts) else parts[i]
243
+
244
+ if estimate_token_count(current_chunk + file_diff) > max_tokens and current_chunk:
245
+ chunks.append(current_chunk)
246
+ current_chunk = file_diff
247
+ else:
248
+ current_chunk += file_diff
249
+
250
+ if current_chunk:
251
+ chunks.append(current_chunk)
252
+
253
+ if not chunks:
254
+ chunks = [diff_text]
255
+
256
+ return chunks
156
257
 
157
258
  def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"):
158
259
  """Sends the diff to the AI using System Instruction and returns a parsed JSON."""
@@ -212,10 +313,46 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
212
313
  click.secho(__("❌ Error: Could not determine model for provider '{provider}'.", provider=provider), fg="red")
213
314
  return None
214
315
 
316
+ # API CALL
215
317
  # API CALL
216
318
  click.secho(__("🤖 GitPR is analyzing your code using {provider} ({model})...\n", provider=provider.capitalize(), model=api_model), fg="cyan")
217
319
 
218
- result_json = call_ai_model(provider, api_key, api_model, prompt, instrucao_sistema)
320
+ chunks = split_diff_into_chunks(diff_text, max_tokens=90000)
321
+
322
+ if len(chunks) == 1:
323
+ result_json = call_ai_model(provider, api_key, api_model, prompt, instrucao_sistema, action=action_folder)
324
+ else:
325
+ click.secho(__("📦 Huge diff detected! Processing in {count} batches (Map-Reduce)...", count=len(chunks)), fg="yellow", bold=True)
326
+ click.secho(f"📚 {__('Understand why:')} {get_doc_url('map-reduce-diff.md')}\n", fg="blue", underline=True)
327
+ resumos_parciais = []
328
+
329
+ for i, chunk in enumerate(chunks, 1):
330
+ click.secho(__("⏳ Analyzing batch {current}/{total}...", current=i, total=len(chunks)), fg="cyan", dim=True)
331
+
332
+ prompt_parcial = __("Generate ONLY a JSON object in the format {json_format} containing a technical summary of what was changed in this part ({idx}) of the diff:\n", json_format='{"resumo": "..."}', idx=i) + chunk
333
+
334
+ resposta_parcial = call_ai_model(provider, api_key, api_model, prompt_parcial, instrucao_sistema, quiet=True, action=f"{action_folder}_chunk_{i}")
335
+
336
+ if resposta_parcial and "resumo" in resposta_parcial:
337
+ resumos_parciais.append(f"### Batch {i}\n{resposta_parcial['resumo']}")
338
+
339
+ time.sleep(1)
340
+
341
+ if not resumos_parciais:
342
+ click.secho(__("❌ Failed to extract context from the partial batches."), fg="red")
343
+ return None
344
+
345
+ click.secho(__("🔄 Unifying intelligence and generating the final analysis..."), fg="yellow")
346
+ diff_unificado = "\n\n".join(resumos_parciais)
347
+
348
+ if action_type == "commit":
349
+ prompt = __("Generate ONLY a JSON object in the format {json_format} for the commit message, unifying these technical summaries:\n", json_format='{"commit_message": "..."}') + diff_unificado
350
+ elif action_type in ["review", "fullreview", "filereview"]:
351
+ prompt = __("Generate ONLY a JSON object in the format {json_format} with a code review focused on improvements, using these summaries:\n", json_format='{"review": "..."}') + diff_unificado
352
+ else:
353
+ prompt = __("Unify these technical summaries and generate ONLY a JSON object in the format {json_format} describing the Pull Request:\n", json_format='{"commit_message": "...", "pr_description": "..."}') + diff_unificado
354
+
355
+ result_json = call_ai_model(provider, api_key, api_model, prompt, instrucao_sistema, action=action_folder)
219
356
 
220
357
  # SAVE TO CACHE AND RETURN
221
358
  if result_json:
@@ -325,7 +462,7 @@ def get_git_full_diff():
325
462
 
326
463
  # Diff between that HASH and your CURRENT WORKSPACE (without using HEAD)
327
464
  # By passing only the hash, Git compares that commit with the files on your disk.
328
- cmd = ["git", "diff", ancestor_hash, "--"] + SMART_EXCLUDES
465
+ cmd = ["git", "diff", "-U1", "-w", "-M", "-B", ancestor_hash, "--"] + SMART_EXCLUDES
329
466
  result = subprocess.run(
330
467
  cmd,
331
468
  capture_output=True,
@@ -379,6 +516,82 @@ def install_git_hooks():
379
516
 
380
517
  return success_count == len(hooks_to_install)
381
518
 
519
+
520
+ def run_install_wizard():
521
+ """
522
+ Interactive setup wizard combining --skill, --installhooks, MCP install, and API key check.
523
+
524
+ Asks for confirmation before each step and prints a documentation URL at the end.
525
+ """
526
+ click.secho(__("\n🔧 Starting GitPR Interactive Setup Wizard..."), fg="cyan", bold=True)
527
+ click.echo(__("This wizard will guide you through the essential GitPR setup steps.\n"))
528
+
529
+ # ------------------------------------------------------------------
530
+ # Step 1: Skill Templates (equivalent to --skill)
531
+ # ------------------------------------------------------------------
532
+ click.secho(__("Step 1 of 4: Skill Templates"), fg="yellow", bold=True)
533
+ click.echo(__("Downloads template files (.gitpr.*.md, .gitpr.linter.yml) into the .gitpr/skill/ folder."))
534
+ click.echo(__("These files allow customizing AI behavior for your team's conventions."))
535
+ if click.confirm(__("Proceed with downloading skill templates?"), default=True):
536
+ generate_skill_template()
537
+ else:
538
+ click.echo(__("Skipped.\n"))
539
+
540
+ # ------------------------------------------------------------------
541
+ # Step 2: Git Hooks (equivalent to --installhooks)
542
+ # ------------------------------------------------------------------
543
+ click.secho(__("\nStep 2 of 4: Git Hooks"), fg="yellow", bold=True)
544
+ click.echo(__("Installs pre-commit (static linter) and prepare-commit-msg (AI commit messages) hooks."))
545
+ click.echo(__("This enables automatic validation and AI assistance before every commit."))
546
+ if click.confirm(__("Proceed with installing Git hooks?"), default=True):
547
+ if install_git_hooks():
548
+ click.secho(__("✅ Git Hooks successfully installed!"), fg="green", bold=True)
549
+ else:
550
+ click.secho(__("⚠️ Some hooks could not be installed."), fg="yellow")
551
+ else:
552
+ click.echo(__("Skipped.\n"))
553
+
554
+ # ------------------------------------------------------------------
555
+ # Step 3: MCP Configuration (equivalent to gitpr-mcp --install auto)
556
+ # ------------------------------------------------------------------
557
+ click.secho(__("\nStep 3 of 4: MCP Configuration"), fg="yellow", bold=True)
558
+ click.echo(__("Auto-detects and configures GitPR for VS Code, Cursor, Claude Desktop, and Zed."))
559
+ click.echo(__("This lets AI-powered editors use GitPR tools directly without the terminal."))
560
+ if click.confirm(__("Proceed with MCP configuration?"), default=True):
561
+ # Lazy import to avoid circular dependency at module level
562
+ from src.mcp_server import _run_install
563
+ _run_install("auto")
564
+ else:
565
+ click.echo(__("Skipped.\n"))
566
+
567
+ # ------------------------------------------------------------------
568
+ # Step 4: API Key Check
569
+ # ------------------------------------------------------------------
570
+ click.secho(__("\nStep 4 of 4: API Key Configuration"), fg="yellow", bold=True)
571
+ provider = get_ai_provider()
572
+ existing_key = get_api_key(provider)
573
+ if existing_key:
574
+ click.secho(
575
+ __("✅ API key for {provider} is already configured.", provider=provider.capitalize()),
576
+ fg="green",
577
+ )
578
+ else:
579
+ click.echo(__("No API key found for {provider}.", provider=provider.capitalize()))
580
+ if click.confirm(__("Would you like to configure it now?"), default=True):
581
+ setup_environment()
582
+ else:
583
+ click.echo(__("You can configure it later by running 'gitpr' or editing ~/.gitpr/.env manually."))
584
+
585
+ # ------------------------------------------------------------------
586
+ # Final: documentation URL
587
+ # ------------------------------------------------------------------
588
+ click.echo("")
589
+ click.secho(__("\n✅ Setup wizard complete!"), fg="green", bold=True)
590
+ click.echo(__("For more details, see the full documentation:"))
591
+ click.secho(f" {get_doc_url('install-wizard.md')}", fg="blue", underline=True)
592
+ click.echo("")
593
+
594
+
382
595
  def get_branch_history_text():
383
596
  """Compiles the Git Log and PR Cache of the current branch to generate the epic context."""
384
597
  branch = get_current_branch()
src/issue_engine.py CHANGED
@@ -78,7 +78,7 @@ def generate_issue_content(context_text, context_type="diff"):
78
78
 
79
79
  click.secho(__("🤖 Structuring Issue using {provider} ({api_model})...", provider=provider.capitalize(), api_model=api_model), fg="cyan", dim=True)
80
80
 
81
- result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst)
81
+ result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst, action="issue")
82
82
 
83
83
  if result_json and "titulo" in result_json and "corpo" in result_json:
84
84
  # Save to Cache