gitpr-cli 0.0.28__py3-none-any.whl → 0.0.29__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.28
3
+ Version: 0.0.29
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
@@ -139,7 +139,7 @@ You can pass the following *flags* for specific actions:
139
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.
140
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).
141
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.
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 10 annotated tools, 15 resources, and 7 pre-built prompts directly inside your IDE. Also available as the standalone `gitpr-mcp` command.
143
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.
144
144
  * `-ih` or `--installhooks`: Automatically installs **local Git Hooks** (`pre-commit` and `prepare-commit-msg`) in your repository.
145
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.
@@ -272,6 +272,8 @@ Once configured, use natural language in your editor's AI chat:
272
272
 
273
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).
274
274
 
275
+ > 💬 **MCP Prompts** — GitPR also exposes 7 pre-defined message templates (prompts) for common flows like "Review PR", "Generate Commit Message", and "Create Issue from Diff". See the [MCP Prompts guide](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-prompts.md) for the full list.
276
+
275
277
  ## 📚 Technical Documentation and Advanced Guides
276
278
 
277
279
  To keep this README concise, we detail the most advanced **DevOps** and **Continuous Integration** focused implementations in separate documents.
@@ -307,6 +309,8 @@ If you want to implement GitPR as an automated quality barrier in your team, che
307
309
  * [**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.
308
310
  * [**Internationalization (i18n)**](https://github.com/natanfiuza/gitpr/blob/main/docs/i18n_explanation.md) — Architecture, usage patterns, and how to add new languages.
309
311
  * [**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
+ * [**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
+ * [**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.
310
314
 
311
315
  ## ⚡ Local Cache System (Quota Savings)
312
316
 
@@ -1,4 +1,4 @@
1
- gitpr_cli-0.0.28.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
1
+ gitpr_cli-0.0.29.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
2
2
  src/__init__.py,sha256=U6ksarernNcryHO20aj4rNG7w0thT7xdMs5KzBJyPvE,28
3
3
  src/ai_providers.py,sha256=cxPuytZ9LfvhQbWFPBJ4dud3lHLpAQpa3cbY8bKESk8,10954
4
4
  src/blame_engine.py,sha256=z-uh8mHOF--JxY_nnbp5r_9y_a_IO4Zn42P0AeLBpto,10436
@@ -10,17 +10,17 @@ src/i18n.py,sha256=QED7hTSdWCC1lSNJ-l6VuOba18OrWgFTMQcZ7cQmXbY,3599
10
10
  src/issue_engine.py,sha256=XJ7WNLoeg3wct079wS7OQqU62f_avn3A8LmMuPWchzE,3642
11
11
  src/linter_engine.py,sha256=D04wvOgRa-vZEYknm-z-Cg7V_3gnoQz_64PMorWnISo,4414
12
12
  src/main.py,sha256=BoI2aN3l1fS4UzoWL67Yw2QtkeRxh9ITuVIQEc9JOPA,32724
13
- src/mcp_server.py,sha256=14BRJX7-etSPztBdUfzocbL9KML99nogQfjkZFRFFIU,31406
13
+ src/mcp_server.py,sha256=rT5ACAb0MUpvt8qfO0jIt3g4y6BKWW5YY2qRrs6hKsQ,40227
14
14
  src/security.py,sha256=Li0zM8ZURTK2nKRHIt49TWLO4DIddBEHzXPrVycv0Rs,1343
15
- src/spinner.py,sha256=16ngB2oT1t7O-c3D2DWCJbFb9nMyD-HWv6KcvhKTelQ,7651
15
+ src/spinner.py,sha256=fnl_jVOPjlC1DmINbnJ_IKW-uujpyJNTv51jwhAMvlc,8466
16
16
  src/tui_issue.py,sha256=gcAJCIfMcdeMjrFJUWQQvdgBv7fnOBtxQxwdBk2Sk40,1789
17
- src/updater.py,sha256=M30nc_eAO7CZl6628TkSwuTwXbzZ9sdIgaoilLL1z2A,5886
17
+ src/updater.py,sha256=kHzP-SyprrNQSLeOhiplFjPuswkh4enVc9TM_7LQnpU,5886
18
18
  src/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  src/ui/chat_app.py,sha256=y-py5UpisQbPLqqDON8AIkyA7X1IM5LAn1GbeYr2p_U,25598
20
20
  src/ui/help_screen.py,sha256=tjbokXhwyzLnkOC0S56NbpX-78hcFQnvsQMpNU1wt0c,1862
21
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,,
22
+ gitpr_cli-0.0.29.dist-info/METADATA,sha256=Q77hoxR0vAluySUVwFCFuJPb3-7zozYTPFNm-y2Y1jY,23425
23
+ gitpr_cli-0.0.29.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
24
+ gitpr_cli-0.0.29.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
25
+ gitpr_cli-0.0.29.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
26
+ gitpr_cli-0.0.29.dist-info/RECORD,,
src/mcp_server.py CHANGED
@@ -37,8 +37,9 @@ from pathlib import Path
37
37
  import click
38
38
  from dotenv import load_dotenv
39
39
  from mcp.server.fastmcp import FastMCP
40
+ from mcp.types import ToolAnnotations
40
41
 
41
- from src.i18n import __
42
+ from src.i18n import __, CURRENT_LANG
42
43
 
43
44
  # =============================================================================
44
45
  # Output Patching System
@@ -243,7 +244,8 @@ mcp = FastMCP(
243
244
  # =============================================================================
244
245
 
245
246
  @mcp.tool(
246
- description=__("Get the current git branch, repository name, and remote origin URL.")
247
+ description=__("Get the current git branch, repository name, and remote origin URL."),
248
+ annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True),
247
249
  )
248
250
  def get_git_context() -> str:
249
251
  """Return JSON with branch name and repository info."""
@@ -264,7 +266,8 @@ def get_git_context() -> str:
264
266
 
265
267
  @mcp.tool(
266
268
  description=__("Get the current unstaged git diff (git diff HEAD). ")
267
- + __("Lists all changed files and their line-level modifications.")
269
+ + __("Lists all changed files and their line-level modifications."),
270
+ annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True),
268
271
  )
269
272
  def analyze_diff() -> str:
270
273
  """Return the raw git diff for uncommitted local changes."""
@@ -284,7 +287,8 @@ def analyze_diff() -> str:
284
287
 
285
288
  @mcp.tool(
286
289
  description=__("Get the full diff of the current branch against the remote "
287
- "base branch (origin/main or origin/master). Runs git fetch first.")
290
+ "base branch (origin/main or origin/master). Runs git fetch first."),
291
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False),
288
292
  )
289
293
  def get_full_diff() -> str:
290
294
  """Return the full diff between the current branch and origin/main."""
@@ -309,7 +313,8 @@ def get_full_diff() -> str:
309
313
  @mcp.tool(
310
314
  description=__("Generate a Conventional Commits commit message from the "
311
315
  "current git diff using AI. "
312
- "Returns a message like 'feat: add user authentication'.")
316
+ "Returns a message like 'feat: add user authentication'."),
317
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False),
313
318
  )
314
319
  def generate_commit_message(
315
320
  provider: str = "",
@@ -351,7 +356,8 @@ def generate_commit_message(
351
356
  @mcp.tool(
352
357
  description=__("Perform an AI code review on uncommitted local changes "
353
358
  "(git diff HEAD). Returns structured feedback with issues "
354
- "and improvement suggestions.")
359
+ "and improvement suggestions."),
360
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False),
355
361
  )
356
362
  def review_code(
357
363
  provider: str = "",
@@ -391,7 +397,8 @@ def review_code(
391
397
 
392
398
  @mcp.tool(
393
399
  description=__("Perform a full AI code review comparing the entire current "
394
- "branch against origin/main. Runs git fetch automatically.")
400
+ "branch against origin/main. Runs git fetch automatically."),
401
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False),
395
402
  )
396
403
  def full_review(provider: str = "") -> str:
397
404
  """AI code review of all changes since origin/main.
@@ -426,7 +433,8 @@ def full_review(provider: str = "") -> str:
426
433
  @mcp.tool(
427
434
  description=__("Generate a complete Pull Request description (title + body) "
428
435
  "from the full diff against origin/main. Uses AI to create a "
429
- "structured, professional PR document.")
436
+ "structured, professional PR document."),
437
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False),
430
438
  )
431
439
  def generate_pr_description(provider: str = "") -> str:
432
440
  """Generate a full PR description from the branch diff.
@@ -466,7 +474,8 @@ def generate_pr_description(provider: str = "") -> str:
466
474
  @mcp.tool(
467
475
  description=__("Run the static local linter (regex-based rules from "
468
476
  ".gitpr.linter.yml) on the current git diff. "
469
- "Returns error and warning counts with detailed messages.")
477
+ "Returns error and warning counts with detailed messages."),
478
+ annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True),
470
479
  )
471
480
  def run_linter() -> str:
472
481
  """Analyze the current diff against .gitpr.linter.yml rules."""
@@ -499,7 +508,8 @@ def run_linter() -> str:
499
508
  @mcp.tool(
500
509
  description=__("Run AI-powered git blame analysis on a file region to trace "
501
510
  "the origin of business rules. Classifies each commit as "
502
- "ORIGIN (first introduction) or REFACTORING (later change).")
511
+ "ORIGIN (first introduction) or REFACTORING (later change)."),
512
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False),
503
513
  )
504
514
  def analyze_blame(
505
515
  file_path: str,
@@ -542,7 +552,8 @@ def analyze_blame(
542
552
  @mcp.tool(
543
553
  description=__("Generate a structured Issue (What / Why / Where / How) from "
544
554
  "code context using AI. Supports three modes: diff (current "
545
- "changes), history (branch history), or blame (file region).")
555
+ "changes), history (branch history), or blame (file region)."),
556
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False),
546
557
  )
547
558
  def generate_issue(context_type: str = "diff") -> str:
548
559
  """Generate an issue from code context.
@@ -592,6 +603,19 @@ SKILL_FILES = {
592
603
  "blame": ".gitpr.blame.md",
593
604
  }
594
605
 
606
+ # Prompt template files (message templates for common MCP flows).
607
+ # Each prompt has a base English file and optional language variants
608
+ # (e.g. gitpr.prompt.review.pt_br.md).
609
+ PROMPT_FILES = {
610
+ "review": "gitpr.prompt.review.md",
611
+ "commit": "gitpr.prompt.commit.md",
612
+ "pr": "gitpr.prompt.pr.md",
613
+ "linter": "gitpr.prompt.linter.md",
614
+ "issue": "gitpr.prompt.issue.md",
615
+ "blame": "gitpr.prompt.blame.md",
616
+ "explore": "gitpr.prompt.explore.md",
617
+ }
618
+
595
619
 
596
620
  def _read_resource_file(filename: str) -> str:
597
621
  """Read a skill or config file from the project's .gitpr/skill/ directory.
@@ -613,6 +637,62 @@ def _read_resource_file(filename: str) -> str:
613
637
  })
614
638
 
615
639
 
640
+ def _read_prompt_file(prompt_name: str) -> str:
641
+ """Load prompt content from a template file with language fallback.
642
+
643
+ Tries the language-specific variant first (e.g.
644
+ gitpr.prompt.review.pt_br.md), falling back to the English base
645
+ file (gitpr.prompt.review.md). Returns an empty string when
646
+ neither file can be found.
647
+
648
+ Search order (first match wins):
649
+ 1. <cwd>/templates/ — development / project-local
650
+ 2. <cwd>/.gitpr/skill/ — downloaded via --skill
651
+ 3. <package>/templates/ — bundled with the installation
652
+ """
653
+ base_filename = PROMPT_FILES.get(prompt_name)
654
+ if not base_filename:
655
+ return ""
656
+
657
+ # Build language-specific filename
658
+ if not CURRENT_LANG.startswith("en"):
659
+ name_part, ext = base_filename.rsplit(".", 1)
660
+ lang_filename = f"{name_part}.{CURRENT_LANG}.{ext}"
661
+ else:
662
+ lang_filename = base_filename
663
+
664
+ # Search directories in priority order
665
+ search_dirs = [
666
+ os.path.join(os.getcwd(), "templates"), # dev / project-local
667
+ os.path.join(os.getcwd(), ".gitpr", "skill"), # downloaded
668
+ os.path.join(os.path.dirname(__file__), "..", "templates"), # bundled
669
+ ]
670
+
671
+ def _try_read(search_dir, filename):
672
+ path = os.path.join(search_dir, filename)
673
+ if os.path.exists(path):
674
+ try:
675
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
676
+ return f.read().strip()
677
+ except Exception:
678
+ pass
679
+ return None
680
+
681
+ for search_dir in search_dirs:
682
+ # Try language variant first
683
+ result = _try_read(search_dir, lang_filename)
684
+ if result:
685
+ return result
686
+
687
+ # Fallback to English base in all directories
688
+ for search_dir in search_dirs:
689
+ result = _try_read(search_dir, base_filename)
690
+ if result:
691
+ return result
692
+
693
+ return ""
694
+
695
+
616
696
  @mcp.resource(
617
697
  uri="skill://list",
618
698
  name=__("Available Skill Templates"),
@@ -697,6 +777,174 @@ def get_linter_config() -> str:
697
777
  return _read_resource_file(".gitpr.linter.yml")
698
778
 
699
779
 
780
+ # =============================================================================
781
+ # Resources — Prompt Templates
782
+ # =============================================================================
783
+
784
+ @mcp.resource(
785
+ uri="prompt://list",
786
+ name=__("Available Prompt Templates"),
787
+ description=__("Lists all available MCP prompt template URIs."),
788
+ mime_type="application/json",
789
+ )
790
+ def list_prompts() -> str:
791
+ """Return a JSON list of available prompt resource URIs."""
792
+ return json.dumps({
793
+ "prompts": [f"prompt://{name}" for name in PROMPT_FILES],
794
+ })
795
+
796
+
797
+ @mcp.resource(
798
+ uri="prompt://review",
799
+ name=__("Review PR Prompt"),
800
+ description=__("Prompt template: full code review of the current branch."),
801
+ mime_type="text/markdown",
802
+ )
803
+ def get_prompt_review() -> str:
804
+ return _read_prompt_file("review")
805
+
806
+
807
+ @mcp.resource(
808
+ uri="prompt://commit",
809
+ name=__("Commit Message Prompt"),
810
+ description=__("Prompt template: generate a Conventional Commits message."),
811
+ mime_type="text/markdown",
812
+ )
813
+ def get_prompt_commit() -> str:
814
+ return _read_prompt_file("commit")
815
+
816
+
817
+ @mcp.resource(
818
+ uri="prompt://pr",
819
+ name=__("PR Description Prompt"),
820
+ description=__("Prompt template: generate a Pull Request description."),
821
+ mime_type="text/markdown",
822
+ )
823
+ def get_prompt_pr() -> str:
824
+ return _read_prompt_file("pr")
825
+
826
+
827
+ @mcp.resource(
828
+ uri="prompt://linter",
829
+ name=__("Linter Prompt"),
830
+ description=__("Prompt template: run the static linter on changes."),
831
+ mime_type="text/markdown",
832
+ )
833
+ def get_prompt_linter() -> str:
834
+ return _read_prompt_file("linter")
835
+
836
+
837
+ @mcp.resource(
838
+ uri="prompt://issue",
839
+ name=__("Issue Prompt"),
840
+ description=__("Prompt template: generate a structured issue from changes."),
841
+ mime_type="text/markdown",
842
+ )
843
+ def get_prompt_issue() -> str:
844
+ return _read_prompt_file("issue")
845
+
846
+
847
+ @mcp.resource(
848
+ uri="prompt://blame",
849
+ name=__("Blame Prompt"),
850
+ description=__("Prompt template: trace code origin with git blame + AI."),
851
+ mime_type="text/markdown",
852
+ )
853
+ def get_prompt_blame() -> str:
854
+ return _read_prompt_file("blame")
855
+
856
+
857
+ @mcp.resource(
858
+ uri="prompt://explore",
859
+ name=__("Explore Prompt"),
860
+ description=__("Prompt template: explore project context and available skills."),
861
+ mime_type="text/markdown",
862
+ )
863
+ def get_prompt_explore() -> str:
864
+ return _read_prompt_file("explore")
865
+
866
+
867
+ # =============================================================================
868
+ # Prompts — Message Templates for Common Flows
869
+ # =============================================================================
870
+ # Prompts are pre-defined message templates that users can select in their
871
+ # editor's AI chat. Unlike tools (which execute automatically), prompts are
872
+ # starter messages that guide the AI to invoke the right GitPR tools.
873
+ #
874
+ # Prompt content is loaded from template files in templates/ (with language
875
+ # variants), so translations can be updated independently of the Python code.
876
+ # =============================================================================
877
+
878
+ @mcp.prompt(
879
+ name=__("Review PR"),
880
+ description=__("Full code review of all changes in the current branch against origin/main. "
881
+ "Runs the full review tool and linter, then composes a comprehensive report."),
882
+ )
883
+ def review_pr_prompt() -> str:
884
+ """Prompt: full code review of the current branch."""
885
+ return _read_prompt_file("review")
886
+
887
+
888
+ @mcp.prompt(
889
+ name=__("Generate Commit Message"),
890
+ description=__("Generate a Conventional Commits message (e.g., 'feat: add user auth') "
891
+ "from the current uncommitted changes."),
892
+ )
893
+ def generate_commit_message_prompt() -> str:
894
+ """Prompt: generate a commit message from uncommitted changes."""
895
+ return _read_prompt_file("commit")
896
+
897
+
898
+ @mcp.prompt(
899
+ name=__("Create PR Description"),
900
+ description=__("Generate a complete Pull Request description (title + body) "
901
+ "from all changes in the current branch."),
902
+ )
903
+ def create_pr_description_prompt() -> str:
904
+ """Prompt: generate a full PR description."""
905
+ return _read_prompt_file("pr")
906
+
907
+
908
+ @mcp.prompt(
909
+ name=__("Run Code Linter"),
910
+ description=__("Run the static linter (.gitpr.linter.yml rules) on current "
911
+ "uncommitted changes and report violations."),
912
+ )
913
+ def run_linter_prompt() -> str:
914
+ """Prompt: run the static linter on current changes."""
915
+ return _read_prompt_file("linter")
916
+
917
+
918
+ @mcp.prompt(
919
+ name=__("Create Issue from Diff"),
920
+ description=__("Generate a structured issue (What / Why / Where / How) from "
921
+ "the current uncommitted changes."),
922
+ )
923
+ def create_issue_prompt() -> str:
924
+ """Prompt: generate an issue from the current diff."""
925
+ return _read_prompt_file("issue")
926
+
927
+
928
+ @mcp.prompt(
929
+ name=__("Trace Code Origin"),
930
+ description=__("Investigate the history of a specific file region using git "
931
+ "blame + AI to trace where business rules came from."),
932
+ )
933
+ def trace_code_origin_prompt() -> str:
934
+ """Prompt: trace the origin of code in a file region."""
935
+ return _read_prompt_file("blame")
936
+
937
+
938
+ @mcp.prompt(
939
+ name=__("Explore Project Context"),
940
+ description=__("Get current branch info, repository name, and list available "
941
+ "skill templates for the project."),
942
+ )
943
+ def explore_project_prompt() -> str:
944
+ """Prompt: explore the current git context and available skills."""
945
+ return _read_prompt_file("explore")
946
+
947
+
700
948
  # =============================================================================
701
949
  # MCP Config Installer (gitpr-mcp --install <editor>)
702
950
  # =============================================================================
src/spinner.py CHANGED
@@ -135,6 +135,29 @@ class Spinner:
135
135
  self._running = False
136
136
  self._quiet = quiet
137
137
 
138
+ @staticmethod
139
+ def _adaptive_speed(word):
140
+ """Calculate chars_per_letter and sleep_time based on word length.
141
+
142
+ Short words (<=15 chars) keep the original leisurely reveal.
143
+ Longer phrases get progressively faster so the full text is
144
+ displayed well before the spinner switches to the next word.
145
+ """
146
+ length = len(word)
147
+ if length <= 15:
148
+ # Single word: original speed
149
+ return 4, 0.08
150
+ elif length <= 35:
151
+ # Medium phrase: moderate speed
152
+ return 2, 0.06
153
+ else:
154
+ # Long phrase: fast reveal
155
+ return 1, 0.04
156
+
157
+ def _next_word(self, current_idx):
158
+ """Pick the next word index (sequential, wrapping around)."""
159
+ return (current_idx + 1) % len(THINKING_WORDS)
160
+
138
161
  def _spin(self):
139
162
  """Main animation loop, runs in a separate thread."""
140
163
  braille_idx = 0
@@ -144,7 +167,9 @@ class Spinner:
144
167
  discovered = "" # Letters already "discovered" of the word
145
168
  dots_cycle = 0 # 0 = ".", 1 = "..", 2 = "..."
146
169
  char_step = 0 # Frame counter for revealing letters
147
- chars_per_letter = 4 # Frames with random chars before revealing a letter
170
+
171
+ # Adaptive speed based on word length
172
+ chars_per_letter, sleep_time = self._adaptive_speed(word)
148
173
 
149
174
  while self._running:
150
175
  braille_char = BRAILLE_FRAMES[braille_idx]
@@ -177,22 +202,22 @@ class Spinner:
177
202
 
178
203
  # Change word and color after a few dot cycles
179
204
  if dots_cycle == 0 and braille_idx == 0:
180
- word_idx = (word_idx + 1) % len(THINKING_WORDS)
205
+ word_idx = self._next_word(word_idx)
181
206
  word = THINKING_WORDS[word_idx]
182
207
  word_color = random.choice(WORD_COLORS)
183
208
  discovered = ""
184
209
  char_step = 0
210
+ # Recalculate adaptive speed for the new word
211
+ chars_per_letter, sleep_time = self._adaptive_speed(word)
185
212
 
186
213
  # Build and display the line: magenta braille + colored word
187
- line = f"\r {MAGENTA}{braille_char}{RESET} {word_color}{display_word}{RESET}"
188
- # Pad the rest of the line with spaces
189
- line = line.ljust(70 + len(MAGENTA) + len(RESET) * 2 + len(word_color))
214
+ line = f"\r {MAGENTA}{braille_char}{RESET} {word_color}{display_word}{RESET}\033[K"
190
215
 
191
216
  if not self._quiet:
192
217
  sys.stdout.write(line)
193
218
  sys.stdout.flush()
194
219
 
195
- time.sleep(0.08) # ~12 fps, suave
220
+ time.sleep(sleep_time)
196
221
 
197
222
  def start(self):
198
223
  """Starts the animation in the background."""
@@ -208,5 +233,5 @@ class Spinner:
208
233
  if self._thread:
209
234
  self._thread.join(timeout=0.5)
210
235
  if not self._quiet:
211
- sys.stdout.write("\r" + " " * 70 + "\r")
236
+ sys.stdout.write("\r\033[K")
212
237
  sys.stdout.flush()
src/updater.py CHANGED
@@ -7,8 +7,8 @@ import click
7
7
  from datetime import datetime
8
8
 
9
9
  # Current version of your local executable (Update this on every new build!)
10
- __version__ = "0.0.28" # GitPR current version
11
- __lang_version__ = "v0.0.6" # Language dictionary version control
10
+ __version__ = "0.0.29" # GitPR current version
11
+ __lang_version__ = "v0.0.7" # Language dictionary version control
12
12
  GITHUB_API_URL = "https://api.github.com/repos/natanfiuza/gitpr/releases/latest"
13
13
  PYPI_API_URL = "https://pypi.org/pypi/gitpr-cli/json"
14
14