gitpr-cli 0.0.30__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 (46) hide show
  1. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/PKG-INFO +24 -1
  2. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/README.md +23 -0
  3. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/PKG-INFO +24 -1
  4. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/SOURCES.txt +1 -0
  5. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/pyproject.toml +1 -1
  6. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/ai_providers.py +5 -0
  7. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/config.py +30 -2
  8. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/core.py +18 -3
  9. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/issue_engine.py +24 -4
  10. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/main.py +41 -13
  11. gitpr_cli-0.0.31/src/metrics.py +588 -0
  12. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/spinner.py +6 -6
  13. gitpr_cli-0.0.31/src/tui_issue.py +122 -0
  14. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/ui/issue_app.py +16 -4
  15. gitpr_cli-0.0.31/src/ui/metrics_app.py +434 -0
  16. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/updater.py +2 -2
  17. gitpr_cli-0.0.31/tests/test_metrics.py +833 -0
  18. gitpr_cli-0.0.30/src/metrics.py +0 -266
  19. gitpr_cli-0.0.30/src/tui_issue.py +0 -37
  20. gitpr_cli-0.0.30/src/ui/metrics_app.py +0 -168
  21. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/LICENSE +0 -0
  22. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/dependency_links.txt +0 -0
  23. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/entry_points.txt +0 -0
  24. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/requires.txt +0 -0
  25. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/top_level.txt +0 -0
  26. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/setup.cfg +0 -0
  27. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/__init__.py +0 -0
  28. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/blame_engine.py +0 -0
  29. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/cache.py +0 -0
  30. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/chat_memory.py +0 -0
  31. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/i18n.py +0 -0
  32. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/linter_engine.py +0 -0
  33. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/mcp_server.py +0 -0
  34. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/security.py +0 -0
  35. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/ui/__init__.py +0 -0
  36. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/ui/chat_app.py +0 -0
  37. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/src/ui/help_screen.py +0 -0
  38. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_chat_backend.py +0 -0
  39. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_core.py +0 -0
  40. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_install_wizard.py +0 -0
  41. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_mcp_prompts.py +0 -0
  42. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_mcp_server.py +0 -0
  43. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_pre_save.py +0 -0
  44. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_skill_command.py +0 -0
  45. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_smart_excludes.py +0 -0
  46. {gitpr_cli-0.0.30 → gitpr_cli-0.0.31}/tests/test_thinking_words.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.30
3
+ Version: 0.0.31
4
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
@@ -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:
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.30
3
+ Version: 0.0.31
4
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
@@ -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:
@@ -34,6 +34,7 @@ tests/test_core.py
34
34
  tests/test_install_wizard.py
35
35
  tests/test_mcp_prompts.py
36
36
  tests/test_mcp_server.py
37
+ tests/test_metrics.py
37
38
  tests/test_pre_save.py
38
39
  tests/test_skill_command.py
39
40
  tests/test_smart_excludes.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitpr-cli"
7
- version = "0.0.30"
7
+ version = "0.0.31"
8
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"
@@ -73,6 +73,8 @@ 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:
@@ -135,6 +137,9 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
135
137
  result_json = result_json[0] if result_json else {}
136
138
 
137
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
138
143
  result_json["_telemetry_meta"] = meta_raw
139
144
 
140
145
  spinner.stop()
@@ -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))
@@ -262,6 +262,8 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
262
262
  click.secho(__("⚠️ No diff found. Make some changes before running the command."), fg="yellow")
263
263
  return None
264
264
 
265
+ t_start = time.perf_counter()
266
+
265
267
  # Cache folder configuration
266
268
  action_folder_map = {
267
269
  "pr": "pr_desc",
@@ -299,6 +301,15 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
299
301
  cached_data = get_cached_response(action_folder, prompt)
300
302
  if cached_data:
301
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
+ )
302
313
  return cached_data
303
314
 
304
315
  # Key Preparation (Now dynamic per Provider)
@@ -318,13 +329,14 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
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
- total_meta = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
322
-
332
+ total_meta = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "duration_ms": 0}
333
+
323
334
  def _aggregate_meta(new_meta):
324
335
  if new_meta:
325
336
  total_meta["prompt_tokens"] += new_meta.get("prompt_tokens", 0)
326
337
  total_meta["completion_tokens"] += new_meta.get("completion_tokens", 0)
327
338
  total_meta["total_tokens"] += new_meta.get("total_tokens", 0)
339
+ total_meta["duration_ms"] += new_meta.get("duration_ms", 0)
328
340
 
329
341
  if len(chunks) == 1:
330
342
  result_json = call_ai_model(provider, api_key, api_model, prompt, instrucao_sistema, action=action_folder)
@@ -375,18 +387,21 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
375
387
  save_cached_response(action_folder, action_type, prompt, result_json, meta_raw=total_meta)
376
388
  # Fire-and-forget metric for successful AI-powered command
377
389
  from src.metrics import log_command_metric
390
+ duration_ms = int((time.perf_counter() - t_start) * 1000)
378
391
  log_command_metric(
379
392
  command=action_type,
380
393
  status="success",
381
394
  provider=provider,
382
395
  tokens_estimated=total_meta.get("total_tokens", 0),
396
+ duration_ms=duration_ms,
383
397
  map_reduce_triggered=(len(chunks) > 1),
384
398
  )
385
399
  return result_json
386
400
 
387
401
  # Command failed (AI returned None)
388
402
  from src.metrics import log_command_metric
389
- log_command_metric(command=action_type, status="error", provider=provider)
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)
390
405
  return None
391
406
 
392
407
 
@@ -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.")}
@@ -250,7 +250,8 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, install
250
250
 
251
251
  if metrics and export:
252
252
  from src.metrics import export_metrics
253
- csv_path, json_path, count = export_metrics()
253
+ from src.core import get_repo_name
254
+ csv_path, json_path, count = export_metrics(repo_filter=get_repo_name())
254
255
  if count > 0:
255
256
  click.secho(__("✅ Metrics exported: {count} events.", count=count), fg="green", bold=True)
256
257
  if csv_path:
@@ -270,9 +271,10 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, install
270
271
  click.secho(__("Purge cancelled."), fg="yellow")
271
272
  return
272
273
 
273
- if metrics and show_dashboard:
274
+ if show_dashboard:
274
275
  from src.ui.metrics_app import launch_metrics_dashboard
275
- launch_metrics_dashboard()
276
+ from src.core import get_repo_name
277
+ launch_metrics_dashboard(repo_filter=get_repo_name())
276
278
  return
277
279
 
278
280
  if metrics:
@@ -364,20 +366,27 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, install
364
366
  if update:
365
367
  click.secho(__("🔍 Checking for updates..."), fg="cyan")
366
368
  check_and_update()
369
+ from src.metrics import log_command_metric
370
+ log_command_metric(command="update", status="success", provider="git")
367
371
  return
368
372
 
369
373
  # --install option: Interactive setup wizard
370
374
  if install:
371
375
  run_install_wizard()
376
+ from src.metrics import log_command_metric
377
+ log_command_metric(command="install", status="success", provider="git")
372
378
  return
373
379
 
374
380
  # --skill option: Generate template and exit
375
381
  if skill:
376
382
  generate_skill_template()
383
+ from src.metrics import log_command_metric
384
+ log_command_metric(command="skill", status="success", provider="git")
377
385
  return
378
386
 
379
387
  if installhooks:
380
- if install_git_hooks():
388
+ hooks_ok = install_git_hooks()
389
+ if hooks_ok:
381
390
  click.secho("\n" + __("✅ Git Hooks successfully installed!"), fg="green", bold=True)
382
391
  click.echo(__("The Linter will now run automatically before each commit."))
383
392
 
@@ -392,6 +401,8 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, install
392
401
  click.echo(__("• How to create new Linter rules (.gitpr.linter.yml):"))
393
402
  click.secho(f" {get_doc_url("linter-regras-customizadas.md")}", fg="blue")
394
403
  click.echo("---\n")
404
+ from src.metrics import log_command_metric
405
+ log_command_metric(command="installhooks", status="success" if hooks_ok else "error", provider="git")
395
406
  return
396
407
 
397
408
  # Archaeologist Module (--blame)
@@ -431,6 +442,8 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, install
431
442
  # Trigger the engine
432
443
  from src.blame_engine import run_blame_analysis
433
444
  run_blame_analysis(file_path.strip(), start_line.strip(), end_line.strip())
445
+ from src.metrics import log_command_metric
446
+ log_command_metric(command="blame", status="success", provider="git")
434
447
  return
435
448
 
436
449
  # Issue Module (Hybrid)
@@ -526,19 +539,32 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, install
526
539
 
527
540
  # Validate or request PAT Token
528
541
  github_token = validate_or_request_github_token(repo_info)
529
-
542
+
530
543
  if not github_token:
531
544
  click.secho(__("❌ Access canceled. GitHub Token is mandatory for this action."), fg="red")
532
545
  return
533
-
534
- # Run the Terminal Graphical Interface
535
- app = IssueApp(issue_data=issue_data, repo_info=repo_info, github_token=github_token)
536
- app.run()
537
546
 
538
- # Display return message after closing the TUI
539
- if app.final_message:
540
- cor = "green" if app.final_action in ["saved", "created"] else "red"
541
- click.secho(f"\n{app.final_message}\n", fg=cor, bold=True)
547
+ # Run the Terminal Graphical Interface (with reauth loop for expired tokens)
548
+ while True:
549
+ app = IssueApp(issue_data=issue_data, repo_info=repo_info, github_token=github_token)
550
+ app.run()
551
+
552
+ # If token expired during the TUI session, re-prompt and relaunch
553
+ if app.final_action == "reauth":
554
+ click.secho(f"\n{app.final_message}\n", fg="yellow")
555
+ github_token = validate_or_request_github_token(repo_info)
556
+ if not github_token:
557
+ click.secho(__("❌ Access canceled. GitHub Token is mandatory for this action."), fg="red")
558
+ return
559
+ # Restart TUI with the same issue data and new token
560
+ continue
561
+
562
+ # Display return message after closing the TUI
563
+ if app.final_message:
564
+ cor = "green" if app.final_action in ["saved", "created"] else "red"
565
+ click.secho(f"\n{app.final_message}\n", fg=cor, bold=True)
566
+
567
+ break
542
568
 
543
569
  return
544
570
 
@@ -590,6 +616,8 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, install
590
616
  system_instruction=system_instruction
591
617
  )
592
618
  app.run()
619
+ from src.metrics import log_command_metric
620
+ log_command_metric(command="chat", status="success", provider=active_provider)
593
621
  click.secho(f"📚 {__('Chat documentation:')} {get_doc_url('understanding_chat_functionality.md')}", fg="cyan")
594
622
  return
595
623