gitpr-cli 0.0.29__tar.gz → 0.0.31__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 (44) hide show
  1. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/PKG-INFO +26 -2
  2. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/README.md +24 -0
  3. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/PKG-INFO +26 -2
  4. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/SOURCES.txt +3 -0
  5. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/pyproject.toml +2 -2
  6. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ai_providers.py +23 -0
  7. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/cache.py +17 -1
  8. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/config.py +30 -2
  9. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/core.py +54 -8
  10. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/issue_engine.py +24 -4
  11. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/main.py +108 -16
  12. gitpr_cli-0.0.31/src/metrics.py +588 -0
  13. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/spinner.py +6 -6
  14. gitpr_cli-0.0.31/src/tui_issue.py +122 -0
  15. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ui/issue_app.py +16 -4
  16. gitpr_cli-0.0.31/src/ui/metrics_app.py +434 -0
  17. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/updater.py +2 -2
  18. gitpr_cli-0.0.31/tests/test_metrics.py +833 -0
  19. gitpr_cli-0.0.29/src/tui_issue.py +0 -37
  20. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/LICENSE +0 -0
  21. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/dependency_links.txt +0 -0
  22. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/entry_points.txt +0 -0
  23. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/requires.txt +0 -0
  24. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/top_level.txt +0 -0
  25. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/setup.cfg +0 -0
  26. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/__init__.py +0 -0
  27. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/blame_engine.py +0 -0
  28. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/chat_memory.py +0 -0
  29. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/i18n.py +0 -0
  30. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/linter_engine.py +0 -0
  31. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/mcp_server.py +0 -0
  32. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/security.py +0 -0
  33. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ui/__init__.py +0 -0
  34. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ui/chat_app.py +0 -0
  35. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ui/help_screen.py +0 -0
  36. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_chat_backend.py +0 -0
  37. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_core.py +0 -0
  38. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_install_wizard.py +0 -0
  39. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_mcp_prompts.py +0 -0
  40. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_mcp_server.py +0 -0
  41. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_pre_save.py +0 -0
  42. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_skill_command.py +0 -0
  43. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_smart_excludes.py +0 -0
  44. {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_thinking_words.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.29
4
- Summary: Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)
3
+ Version: 0.0.31
4
+ Summary: AI-powered PR automation, commit messages, and code review (Gemini, DeepSeek, and Ollama)
5
5
  Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
@@ -28,6 +28,29 @@ GitPR CLI is a command-line automation tool that uses **Google Gemini** and **De
28
28
 
29
29
  🌐 **Website:** [gitpr.natanfiuza.dev.br](https://gitpr.natanfiuza.dev.br/) · 📂 **Repository:** [github.com/natanfiuza/gitpr](https://github.com/natanfiuza/gitpr)
30
30
 
31
+ ----
32
+
33
+ ## ⚡ **Quick Start**
34
+
35
+ ### **1. Installation via PyPI**
36
+
37
+ Install GitPR CLI using `pip`:
38
+
39
+ ```bash
40
+ pip install gitpr-cli
41
+ ```
42
+
43
+ ### **2. Initializing in a Repository**
44
+
45
+ To set up GitPR in a folder of a new repository, run:
46
+
47
+ ```bash
48
+ gitpr --install
49
+ ```
50
+
51
+ > **Guided Setup:** Guided configuration that downloads skill templates, installs Git Hooks, configures MCP for your editors, and verifies the API key for your AI provider.
52
+ > 📖 **Full Documentation:** [https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us](https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us)
53
+
31
54
  ## **🛠️ Technologies and Libraries Used**
32
55
 
33
56
  This project was developed in Python and uses the following main libraries:
@@ -311,6 +334,7 @@ If you want to implement GitPR as an automated quality barrier in your team, che
311
334
  * [**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.
312
335
  * [**MCP Prompts**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-prompts.md) — Pre-built message templates (7 prompts, 35 language variants) for common GitPR workflows in your editor's AI chat.
313
336
  * [**MCP Tool Annotations**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-annotations.md) — IDE integration hints (`readOnlyHint`, `destructiveHint`) for smarter UI behavior and safer tool execution.
337
+ * [**Metrics & Telemetry**](https://github.com/natanfiuza/gitpr/blob/main/docs/metricas-telemetria.md) — Local offline analytics for team usage metrics, exportable CSV reports, and interactive TUI dashboard.
314
338
 
315
339
  ## ⚡ Local Cache System (Quota Savings)
316
340
 
@@ -9,6 +9,29 @@ GitPR CLI is a command-line automation tool that uses **Google Gemini** and **De
9
9
 
10
10
  🌐 **Website:** [gitpr.natanfiuza.dev.br](https://gitpr.natanfiuza.dev.br/) · 📂 **Repository:** [github.com/natanfiuza/gitpr](https://github.com/natanfiuza/gitpr)
11
11
 
12
+ ----
13
+
14
+ ## ⚡ **Quick Start**
15
+
16
+ ### **1. Installation via PyPI**
17
+
18
+ Install GitPR CLI using `pip`:
19
+
20
+ ```bash
21
+ pip install gitpr-cli
22
+ ```
23
+
24
+ ### **2. Initializing in a Repository**
25
+
26
+ To set up GitPR in a folder of a new repository, run:
27
+
28
+ ```bash
29
+ gitpr --install
30
+ ```
31
+
32
+ > **Guided Setup:** Guided configuration that downloads skill templates, installs Git Hooks, configures MCP for your editors, and verifies the API key for your AI provider.
33
+ > 📖 **Full Documentation:** [https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us](https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us)
34
+
12
35
  ## **🛠️ Technologies and Libraries Used**
13
36
 
14
37
  This project was developed in Python and uses the following main libraries:
@@ -292,6 +315,7 @@ If you want to implement GitPR as an automated quality barrier in your team, che
292
315
  * [**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.
293
316
  * [**MCP Prompts**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-prompts.md) — Pre-built message templates (7 prompts, 35 language variants) for common GitPR workflows in your editor's AI chat.
294
317
  * [**MCP Tool Annotations**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-annotations.md) — IDE integration hints (`readOnlyHint`, `destructiveHint`) for smarter UI behavior and safer tool execution.
318
+ * [**Metrics & Telemetry**](https://github.com/natanfiuza/gitpr/blob/main/docs/metricas-telemetria.md) — Local offline analytics for team usage metrics, exportable CSV reports, and interactive TUI dashboard.
295
319
 
296
320
  ## ⚡ Local Cache System (Quota Savings)
297
321
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.29
4
- Summary: Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)
3
+ Version: 0.0.31
4
+ Summary: AI-powered PR automation, commit messages, and code review (Gemini, DeepSeek, and Ollama)
5
5
  Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
@@ -28,6 +28,29 @@ GitPR CLI is a command-line automation tool that uses **Google Gemini** and **De
28
28
 
29
29
  🌐 **Website:** [gitpr.natanfiuza.dev.br](https://gitpr.natanfiuza.dev.br/) · 📂 **Repository:** [github.com/natanfiuza/gitpr](https://github.com/natanfiuza/gitpr)
30
30
 
31
+ ----
32
+
33
+ ## ⚡ **Quick Start**
34
+
35
+ ### **1. Installation via PyPI**
36
+
37
+ Install GitPR CLI using `pip`:
38
+
39
+ ```bash
40
+ pip install gitpr-cli
41
+ ```
42
+
43
+ ### **2. Initializing in a Repository**
44
+
45
+ To set up GitPR in a folder of a new repository, run:
46
+
47
+ ```bash
48
+ gitpr --install
49
+ ```
50
+
51
+ > **Guided Setup:** Guided configuration that downloads skill templates, installs Git Hooks, configures MCP for your editors, and verifies the API key for your AI provider.
52
+ > 📖 **Full Documentation:** [https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us](https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us)
53
+
31
54
  ## **🛠️ Technologies and Libraries Used**
32
55
 
33
56
  This project was developed in Python and uses the following main libraries:
@@ -311,6 +334,7 @@ If you want to implement GitPR as an automated quality barrier in your team, che
311
334
  * [**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.
312
335
  * [**MCP Prompts**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-prompts.md) — Pre-built message templates (7 prompts, 35 language variants) for common GitPR workflows in your editor's AI chat.
313
336
  * [**MCP Tool Annotations**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-annotations.md) — IDE integration hints (`readOnlyHint`, `destructiveHint`) for smarter UI behavior and safer tool execution.
337
+ * [**Metrics & Telemetry**](https://github.com/natanfiuza/gitpr/blob/main/docs/metricas-telemetria.md) — Local offline analytics for team usage metrics, exportable CSV reports, and interactive TUI dashboard.
314
338
 
315
339
  ## ⚡ Local Cache System (Quota Savings)
316
340
 
@@ -19,6 +19,7 @@ src/issue_engine.py
19
19
  src/linter_engine.py
20
20
  src/main.py
21
21
  src/mcp_server.py
22
+ src/metrics.py
22
23
  src/security.py
23
24
  src/spinner.py
24
25
  src/tui_issue.py
@@ -27,11 +28,13 @@ src/ui/__init__.py
27
28
  src/ui/chat_app.py
28
29
  src/ui/help_screen.py
29
30
  src/ui/issue_app.py
31
+ src/ui/metrics_app.py
30
32
  tests/test_chat_backend.py
31
33
  tests/test_core.py
32
34
  tests/test_install_wizard.py
33
35
  tests/test_mcp_prompts.py
34
36
  tests/test_mcp_server.py
37
+ tests/test_metrics.py
35
38
  tests/test_pre_save.py
36
39
  tests/test_skill_command.py
37
40
  tests/test_smart_excludes.py
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitpr-cli"
7
- version = "0.0.29"
8
- description = "Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)"
7
+ version = "0.0.31"
8
+ description = "AI-powered PR automation, commit messages, and code review (Gemini, DeepSeek, and Ollama)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  authors = [
@@ -73,9 +73,12 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
73
73
  spinner = Spinner(quiet=quiet)
74
74
  spinner.start()
75
75
 
76
+ start_time = time.perf_counter()
77
+
76
78
  try:
77
79
  for attempt in range(1, max_retries + 1):
78
80
  try:
81
+ meta_raw = {}
79
82
  if provider == "gemini":
80
83
  client = genai.Client(api_key=api_key)
81
84
  response = client.models.generate_content(
@@ -90,6 +93,13 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
90
93
  }
91
94
  )
92
95
  result_text = response.text
96
+
97
+ if hasattr(response, 'usage_metadata') and response.usage_metadata:
98
+ meta_raw = {
99
+ "prompt_tokens": getattr(response.usage_metadata, 'prompt_token_count', 0),
100
+ "completion_tokens": getattr(response.usage_metadata, 'candidates_token_count', 0),
101
+ "total_tokens": getattr(response.usage_metadata, 'total_token_count', 0)
102
+ }
93
103
 
94
104
  elif provider in ["deepseek", "ollama"]:
95
105
  # DeepSeek and Ollama are 100% compatible with the OpenAI library.
@@ -106,6 +116,13 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
106
116
  temperature=0.0
107
117
  )
108
118
  result_text = response.choices[0].message.content
119
+
120
+ if hasattr(response, 'usage') and response.usage:
121
+ meta_raw = {
122
+ "prompt_tokens": getattr(response.usage, 'prompt_tokens', 0),
123
+ "completion_tokens": getattr(response.usage, 'completion_tokens', 0),
124
+ "total_tokens": getattr(response.usage, 'total_tokens', 0)
125
+ }
109
126
 
110
127
  else:
111
128
  spinner.stop()
@@ -119,6 +136,12 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
119
136
  if isinstance(result_json, list):
120
137
  result_json = result_json[0] if result_json else {}
121
138
 
139
+ # Inject telemetry metadata silently into the response dictionary
140
+ meta_raw["duration_ms"] = int((time.perf_counter() - start_time) * 1000)
141
+ meta_raw["provider"] = provider
142
+ meta_raw["model"] = api_model
143
+ result_json["_telemetry_meta"] = meta_raw
144
+
122
145
  spinner.stop()
123
146
  return result_json
124
147
 
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import json
3
3
  import hashlib
4
+ import subprocess
4
5
  from datetime import datetime
5
6
  from pathlib import Path
6
7
 
@@ -13,6 +14,15 @@ def generate_md5(text):
13
14
  """Generates the MD5 hash of a string."""
14
15
  return hashlib.md5(text.encode('utf-8')).hexdigest()
15
16
 
17
+ def get_git_user_info():
18
+ """Recupera o nome e email configurados no git local."""
19
+ try:
20
+ name = subprocess.run(["git", "config", "user.name"], capture_output=True, text=True, check=True).stdout.strip()
21
+ email = subprocess.run(["git", "config", "user.email"], capture_output=True, text=True, check=True).stdout.strip()
22
+ return name, email
23
+ except subprocess.CalledProcessError:
24
+ return "unknown", "unknown"
25
+
16
26
  def get_cached_response(action_folder, prompt_text):
17
27
  """Checks if a valid cache exists for the prompt and returns the content."""
18
28
  md5_hash = generate_md5(prompt_text)
@@ -27,7 +37,7 @@ def get_cached_response(action_folder, prompt_text):
27
37
  return None
28
38
  return None
29
39
 
30
- def save_cached_response(action_folder, action_type, prompt_text, response_dict):
40
+ def save_cached_response(action_folder, action_type, prompt_text, response_dict, meta_raw=None):
31
41
  """Saves the AI response to the local cache."""
32
42
  md5_hash = generate_md5(prompt_text)
33
43
  folder_path = get_cache_base_dir() / action_folder
@@ -37,11 +47,17 @@ def save_cached_response(action_folder, action_type, prompt_text, response_dict)
37
47
  from src.core import get_current_branch, get_repo_name
38
48
  current_branch = get_current_branch()
39
49
  repo_name = get_repo_name()
50
+ user_name, user_email = get_git_user_info()
51
+
52
+ if meta_raw is not None:
53
+ response_dict["meta_raw"] = meta_raw
40
54
 
41
55
  cache_data = {
42
56
  "md5": md5_hash,
43
57
  "repo": repo_name,
44
58
  "branch": current_branch,
59
+ "author_name": user_name,
60
+ "author_email": user_email,
45
61
  "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
46
62
  "action_type": action_type,
47
63
  "prompt": prompt_text,
@@ -211,7 +211,35 @@ def get_github_token():
211
211
  """Reads and decrypts the GitHub Personal Access Token (PAT)."""
212
212
  load_dotenv(ENV_FILE)
213
213
  encrypted_token = os.getenv("GITHUB_TOKEN_ENCRYPTED")
214
-
214
+
215
215
  if encrypted_token:
216
216
  return decrypt_data(encrypted_token)
217
- return None
217
+ return None
218
+
219
+
220
+ def validate_github_token(token):
221
+ """
222
+ Validates a GitHub PAT by making a lightweight API call to /user.
223
+ Returns (is_valid: bool, error_message: str).
224
+ """
225
+ import requests
226
+
227
+ try:
228
+ headers = {
229
+ "Authorization": f"token {token}",
230
+ "Accept": "application/vnd.github.v3+json"
231
+ }
232
+ response = requests.get("https://api.github.com/user", headers=headers, timeout=10)
233
+
234
+ if response.status_code == 200:
235
+ return True, ""
236
+ elif response.status_code == 401:
237
+ return False, __("Token expired or invalid. Please generate a new one.")
238
+ else:
239
+ return False, __("Unexpected response from GitHub (HTTP {code})", code=response.status_code)
240
+ except requests.exceptions.ConnectionError:
241
+ return False, __("No internet connection. Cannot validate GitHub token.")
242
+ except requests.exceptions.Timeout:
243
+ return False, __("GitHub API timeout. Check your connection and try again.")
244
+ except Exception as e:
245
+ return False, __("Failed to validate token: {error}", error=str(e))
@@ -16,6 +16,7 @@ from src.config import get_api_key, get_api_model, get_skill_dir, resolve_skill_
16
16
  from src.ai_providers import call_ai_model
17
17
  from src.i18n import __, CURRENT_LANG
18
18
  from src.updater import __lang_version__
19
+ # Metrics are imported lazily inside generate_pr_content() to avoid circular imports
19
20
 
20
21
  # Smart Diff filter: files that consume AI tokens without adding semantic value.
21
22
  # The pattern list is managed remotely (templates/gitpr.smart-excludes.json) and
@@ -261,6 +262,8 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
261
262
  click.secho(__("⚠️ No diff found. Make some changes before running the command."), fg="yellow")
262
263
  return None
263
264
 
265
+ t_start = time.perf_counter()
266
+
264
267
  # Cache folder configuration
265
268
  action_folder_map = {
266
269
  "pr": "pr_desc",
@@ -298,6 +301,15 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
298
301
  cached_data = get_cached_response(action_folder, prompt)
299
302
  if cached_data:
300
303
  click.secho(__("⚡ Response retrieved from local cache."), fg="green", dim=True)
304
+ from src.metrics import log_command_metric
305
+ duration_ms = int((time.perf_counter() - t_start) * 1000)
306
+ log_command_metric(
307
+ command=action_type,
308
+ status="success",
309
+ provider=provider,
310
+ duration_ms=duration_ms,
311
+ cache_hit=True,
312
+ )
301
313
  return cached_data
302
314
 
303
315
  # Key Preparation (Now dynamic per Provider)
@@ -313,15 +325,27 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
313
325
  click.secho(__("❌ Error: Could not determine model for provider '{provider}'.", provider=provider), fg="red")
314
326
  return None
315
327
 
316
- # API CALL
317
328
  # API CALL
318
329
  click.secho(__("🤖 GitPR is analyzing your code using {provider} ({model})...\n", provider=provider.capitalize(), model=api_model), fg="cyan")
319
330
 
320
331
  chunks = split_diff_into_chunks(diff_text, max_tokens=90000)
321
-
332
+ total_meta = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "duration_ms": 0}
333
+
334
+ def _aggregate_meta(new_meta):
335
+ if new_meta:
336
+ total_meta["prompt_tokens"] += new_meta.get("prompt_tokens", 0)
337
+ total_meta["completion_tokens"] += new_meta.get("completion_tokens", 0)
338
+ total_meta["total_tokens"] += new_meta.get("total_tokens", 0)
339
+ total_meta["duration_ms"] += new_meta.get("duration_ms", 0)
340
+
322
341
  if len(chunks) == 1:
323
342
  result_json = call_ai_model(provider, api_key, api_model, prompt, instrucao_sistema, action=action_folder)
343
+ if result_json:
344
+ _aggregate_meta(result_json.pop("_telemetry_meta", None))
324
345
  else:
346
+ from src.metrics import log_local_metric
347
+ log_local_metric(command="map_reduce", status="triggered", map_reduce_triggered=True, chunks_count=len(chunks))
348
+
325
349
  click.secho(__("📦 Huge diff detected! Processing in {count} batches (Map-Reduce)...", count=len(chunks)), fg="yellow", bold=True)
326
350
  click.secho(f"📚 {__('Understand why:')} {get_doc_url('map-reduce-diff.md')}\n", fg="blue", underline=True)
327
351
  resumos_parciais = []
@@ -332,9 +356,11 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
332
356
  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
357
 
334
358
  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']}")
359
+
360
+ if resposta_parcial:
361
+ _aggregate_meta(resposta_parcial.pop("_telemetry_meta", None))
362
+ if "resumo" in resposta_parcial:
363
+ resumos_parciais.append(f"### Batch {i}\n{resposta_parcial['resumo']}")
338
364
 
339
365
  time.sleep(1)
340
366
 
@@ -353,12 +379,29 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
353
379
  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
380
 
355
381
  result_json = call_ai_model(provider, api_key, api_model, prompt, instrucao_sistema, action=action_folder)
382
+ if result_json:
383
+ _aggregate_meta(result_json.pop("_telemetry_meta", None))
356
384
 
357
385
  # SAVE TO CACHE AND RETURN
358
386
  if result_json:
359
- save_cached_response(action_folder, action_type, prompt, result_json)
387
+ save_cached_response(action_folder, action_type, prompt, result_json, meta_raw=total_meta)
388
+ # Fire-and-forget metric for successful AI-powered command
389
+ from src.metrics import log_command_metric
390
+ duration_ms = int((time.perf_counter() - t_start) * 1000)
391
+ log_command_metric(
392
+ command=action_type,
393
+ status="success",
394
+ provider=provider,
395
+ tokens_estimated=total_meta.get("total_tokens", 0),
396
+ duration_ms=duration_ms,
397
+ map_reduce_triggered=(len(chunks) > 1),
398
+ )
360
399
  return result_json
361
-
400
+
401
+ # Command failed (AI returned None)
402
+ from src.metrics import log_command_metric
403
+ duration_ms = int((time.perf_counter() - t_start) * 1000)
404
+ log_command_metric(command=action_type, status="error", provider=provider, duration_ms=duration_ms)
362
405
  return None
363
406
 
364
407
 
@@ -487,7 +530,10 @@ def install_git_hooks():
487
530
  # Mapping: Hook Name in Git -> Template Name on your GitHub
488
531
  hooks_to_install = {
489
532
  "pre-commit": "pre-commit-template.sh",
490
- "prepare-commit-msg": "prepare-commit-msg-template.sh"
533
+ "prepare-commit-msg": "prepare-commit-msg-template.sh",
534
+ "post-checkout": "post-checkout-template.sh",
535
+ "pre-push": "pre-push-template.sh",
536
+ "post-merge": "post-merge-template.sh",
491
537
  }
492
538
 
493
539
  base_url = "https://raw.githubusercontent.com/natanfiuza/gitpr/main/scripts/"
@@ -1,6 +1,7 @@
1
1
  import subprocess
2
2
  import re
3
3
  import os
4
+ import time
4
5
  import click
5
6
  from src.ai_providers import call_ai_model
6
7
  from src.cache import get_cached_response, save_cached_response
@@ -32,14 +33,20 @@ def get_github_repo_info():
32
33
 
33
34
  def generate_issue_content(context_text, context_type="diff"):
34
35
  """Sends the context (diff, blame, or history) to the AI and returns an issue dictionary."""
36
+ from src.metrics import log_command_metric
37
+
35
38
  if not context_text or not str(context_text).strip():
36
39
  return None
37
40
 
41
+ t_start = time.perf_counter()
42
+
38
43
  provider = get_ai_provider()
39
44
  api_key = get_api_key(provider)
40
45
 
41
46
  if not api_key:
42
47
  click.secho(__("❌ Error: API Key not found."), fg="red")
48
+ duration_ms = int((time.perf_counter() - t_start) * 1000)
49
+ log_command_metric(command="issue", status="error", provider=provider, duration_ms=duration_ms)
43
50
  return None
44
51
 
45
52
  # Use the advanced model to ensure Issue structure quality
@@ -69,11 +76,13 @@ def generate_issue_content(context_text, context_type="diff"):
69
76
  __("Generate the requested JSON object following the system instructions to {target_action}\n\n", target_action=target_action) +
70
77
  f"{data_label}\n{context_text}"
71
78
  )
72
-
79
+
73
80
  # Try to retrieve from Cache
74
81
  cached_data = get_cached_response("issue", prompt)
75
82
  if cached_data:
76
83
  click.secho(__("⚡ Issue response retrieved from local cache."), fg="green", dim=True)
84
+ duration_ms = int((time.perf_counter() - t_start) * 1000)
85
+ log_command_metric(command="issue", status="success", provider=provider, duration_ms=duration_ms, cache_hit=True)
77
86
  return cached_data
78
87
 
79
88
  click.secho(__("🤖 Structuring Issue using {provider} ({api_model})...", provider=provider.capitalize(), api_model=api_model), fg="cyan", dim=True)
@@ -81,8 +90,19 @@ def generate_issue_content(context_text, context_type="diff"):
81
90
  result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst, action="issue")
82
91
 
83
92
  if result_json and "titulo" in result_json and "corpo" in result_json:
84
- # Save to Cache
85
- save_cached_response("issue", "issue", prompt, result_json)
93
+ # Extract telemetry metadata and save to cache with real token counts
94
+ meta = result_json.pop("_telemetry_meta", None)
95
+ save_cached_response("issue", "issue", prompt, result_json, meta_raw=meta)
96
+ duration_ms = int((time.perf_counter() - t_start) * 1000)
97
+ log_command_metric(
98
+ command="issue",
99
+ status="success",
100
+ provider=provider,
101
+ tokens_estimated=(meta or {}).get("total_tokens", 0),
102
+ duration_ms=duration_ms,
103
+ )
86
104
  return result_json
87
-
105
+
106
+ duration_ms = int((time.perf_counter() - t_start) * 1000)
107
+ log_command_metric(command="issue", status="error", provider=provider, duration_ms=duration_ms)
88
108
  return {"titulo": __("Error generating title"), "corpo": __("Could not generate issue body by AI.")}