gac 2.0.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of gac might be problematic. Click here for more details.

Files changed (44) hide show
  1. {gac-2.0.0 → gac-2.2.0}/PKG-INFO +29 -10
  2. {gac-2.0.0 → gac-2.2.0}/README.md +27 -9
  3. {gac-2.0.0 → gac-2.2.0}/pyproject.toml +1 -0
  4. {gac-2.0.0 → gac-2.2.0}/src/gac/__version__.py +1 -1
  5. {gac-2.0.0 → gac-2.2.0}/src/gac/ai.py +2 -0
  6. {gac-2.0.0 → gac-2.2.0}/src/gac/ai_utils.py +1 -0
  7. {gac-2.0.0 → gac-2.2.0}/src/gac/cli.py +10 -0
  8. {gac-2.0.0 → gac-2.2.0}/src/gac/constants.py +32 -1
  9. {gac-2.0.0 → gac-2.2.0}/src/gac/init_cli.py +6 -32
  10. {gac-2.0.0 → gac-2.2.0}/src/gac/language_cli.py +4 -33
  11. {gac-2.0.0 → gac-2.2.0}/src/gac/main.py +14 -1
  12. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/__init__.py +2 -0
  13. gac-2.2.0/src/gac/providers/mistral.py +38 -0
  14. gac-2.2.0/src/gac/utils.py +270 -0
  15. gac-2.0.0/src/gac/utils.py +0 -132
  16. {gac-2.0.0 → gac-2.2.0}/.gitignore +0 -0
  17. {gac-2.0.0 → gac-2.2.0}/LICENSE +0 -0
  18. {gac-2.0.0 → gac-2.2.0}/src/gac/__init__.py +0 -0
  19. {gac-2.0.0 → gac-2.2.0}/src/gac/config.py +0 -0
  20. {gac-2.0.0 → gac-2.2.0}/src/gac/config_cli.py +0 -0
  21. {gac-2.0.0 → gac-2.2.0}/src/gac/diff_cli.py +0 -0
  22. {gac-2.0.0 → gac-2.2.0}/src/gac/errors.py +0 -0
  23. {gac-2.0.0 → gac-2.2.0}/src/gac/git.py +0 -0
  24. {gac-2.0.0 → gac-2.2.0}/src/gac/preprocess.py +0 -0
  25. {gac-2.0.0 → gac-2.2.0}/src/gac/prompt.py +0 -0
  26. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/anthropic.py +0 -0
  27. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/cerebras.py +0 -0
  28. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/chutes.py +0 -0
  29. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/custom_anthropic.py +0 -0
  30. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/custom_openai.py +0 -0
  31. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/deepseek.py +0 -0
  32. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/fireworks.py +0 -0
  33. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/gemini.py +0 -0
  34. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/groq.py +0 -0
  35. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/lmstudio.py +0 -0
  36. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/minimax.py +0 -0
  37. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/ollama.py +0 -0
  38. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/openai.py +0 -0
  39. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/openrouter.py +0 -0
  40. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/streamlake.py +0 -0
  41. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/synthetic.py +0 -0
  42. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/together.py +0 -0
  43. {gac-2.0.0 → gac-2.2.0}/src/gac/providers/zai.py +0 -0
  44. {gac-2.0.0 → gac-2.2.0}/src/gac/security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gac
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: LLM-powered Git commit message generator with multi-provider support
5
5
  Project-URL: Homepage, https://github.com/cellwebb/gac
6
6
  Project-URL: Documentation, https://github.com/cellwebb/gac#readme
@@ -25,6 +25,7 @@ Requires-Dist: click>=8.3.0
25
25
  Requires-Dist: halo
26
26
  Requires-Dist: httpcore>=1.0.9
27
27
  Requires-Dist: httpx>=0.28.0
28
+ Requires-Dist: prompt-toolkit>=3.0.36
28
29
  Requires-Dist: pydantic>=2.12.0
29
30
  Requires-Dist: python-dotenv>=1.1.1
30
31
  Requires-Dist: questionary
@@ -52,9 +53,9 @@ Description-Content-Type: text/markdown
52
53
  [![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](docs/CONTRIBUTING.md)
53
54
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
54
55
 
55
- **LLM-powered commit messages that understand your code.**
56
+ **LLM-powered commit messages that understand your code!**
56
57
 
57
- **Tired of writing commit messages?** Replace `git commit -m "..."` with `gac` for contextual, well-formatted commit messages generated by large language models.
58
+ **Automate your commits!** Replace `git commit -m "..."` with `gac` for contextual, well-formatted commit messages generated by large language models!
58
59
 
59
60
  ---
60
61
 
@@ -68,7 +69,7 @@ Intelligent, contextual messages that explain the **why** behind your changes:
68
69
 
69
70
  ## Quick Start
70
71
 
71
- ### Use without installing
72
+ ### Use gac without installing
72
73
 
73
74
  ```bash
74
75
  uvx gac init # Configure your LLM provider
@@ -77,7 +78,7 @@ uvx gac # Generate and commit with LLM
77
78
 
78
79
  That's it! Review the generated message and confirm with `y`.
79
80
 
80
- ### Install gac globally
81
+ ### Install and use gac
81
82
 
82
83
  ```bash
83
84
  uv tool install gac
@@ -85,6 +86,12 @@ gac init
85
86
  gac
86
87
  ```
87
88
 
89
+ ### Upgrade installed gac
90
+
91
+ ```bash
92
+ uv tool upgrade gac
93
+ ```
94
+
88
95
  ---
89
96
 
90
97
  ## Key Features
@@ -92,7 +99,7 @@ gac
92
99
  ### 🌐 **Supported Providers**
93
100
 
94
101
  - **Anthropic** • **Cerebras** • **Chutes.ai** • **DeepSeek** • **Fireworks**
95
- - **Gemini** • **Groq** • **LM Studio** • **MiniMax** • **Ollama** • **OpenAI**
102
+ - **Gemini** • **Groq** • **LM Studio** • **MiniMax** • **Mistral** • **Ollama** • **OpenAI**
96
103
  - **OpenRouter** • **Streamlake** • **Synthetic.new** • **Together AI**
97
104
  - **Z.AI** • **Z.AI Coding** • **Custom Endpoints (Anthropic/OpenAI)**
98
105
 
@@ -117,7 +124,7 @@ gac
117
124
 
118
125
  ### 💻 **Developer Experience**
119
126
 
120
- - **Interactive feedback**: Type `r` to reroll, or directly type your feedback like `make it shorter` or `focus on the bug fix`
127
+ - **Interactive feedback**: Type `r` to reroll, `e` to edit in-place with vi/emacs keybindings, or directly type your feedback like `make it shorter` or `focus on the bug fix`
121
128
  - **One-command workflows**: Complete workflows with flags like `gac -ayp` (stage all, auto-confirm, push)
122
129
  - **Git integration**: Respects pre-commit and lefthook hooks, running them before expensive LLM operations
123
130
 
@@ -140,7 +147,7 @@ git add .
140
147
  # Generate and commit with LLM
141
148
  gac
142
149
 
143
- # Review → y (commit) | n (cancel) | r (reroll) | or type feedback
150
+ # Review → y (commit) | n (cancel) | r (reroll) | e (edit) | or type feedback
144
151
  ```
145
152
 
146
153
  ### Common Commands
@@ -177,13 +184,18 @@ gac --skip-secret-scan
177
184
 
178
185
  ### Interactive Feedback System
179
186
 
180
- Not happy with the result? You have two options:
187
+ Not happy with the result? You have several options:
181
188
 
182
189
  ```bash
183
190
  # Simple reroll (no feedback)
184
191
  r
185
192
 
186
- # Or just type your feedback directly - no prefix needed!
193
+ # Edit in-place with rich terminal editing
194
+ e
195
+ # Uses prompt_toolkit for multi-line editing with vi/emacs keybindings
196
+ # Press Esc+Enter or Ctrl+S to submit, Ctrl+C to cancel
197
+
198
+ # Or just type your feedback directly!
187
199
  make it shorter and focus on the performance improvement
188
200
  use conventional commit format with scope
189
201
  explain the security implications
@@ -191,6 +203,13 @@ explain the security implications
191
203
  # Press Enter on empty input to see the prompt again
192
204
  ```
193
205
 
206
+ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
207
+
208
+ - **Edit naturally**: Multi-line editing with familiar vi/emacs key bindings
209
+ - **Make quick fixes**: Correct typos, adjust wording, or refine formatting
210
+ - **Add details**: Include information the LLM might have missed
211
+ - **Restructure**: Reorganize bullet points or change the message structure
212
+
194
213
  ---
195
214
 
196
215
  ## Configuration
@@ -11,9 +11,9 @@
11
11
  [![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](docs/CONTRIBUTING.md)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
13
 
14
- **LLM-powered commit messages that understand your code.**
14
+ **LLM-powered commit messages that understand your code!**
15
15
 
16
- **Tired of writing commit messages?** Replace `git commit -m "..."` with `gac` for contextual, well-formatted commit messages generated by large language models.
16
+ **Automate your commits!** Replace `git commit -m "..."` with `gac` for contextual, well-formatted commit messages generated by large language models!
17
17
 
18
18
  ---
19
19
 
@@ -27,7 +27,7 @@ Intelligent, contextual messages that explain the **why** behind your changes:
27
27
 
28
28
  ## Quick Start
29
29
 
30
- ### Use without installing
30
+ ### Use gac without installing
31
31
 
32
32
  ```bash
33
33
  uvx gac init # Configure your LLM provider
@@ -36,7 +36,7 @@ uvx gac # Generate and commit with LLM
36
36
 
37
37
  That's it! Review the generated message and confirm with `y`.
38
38
 
39
- ### Install gac globally
39
+ ### Install and use gac
40
40
 
41
41
  ```bash
42
42
  uv tool install gac
@@ -44,6 +44,12 @@ gac init
44
44
  gac
45
45
  ```
46
46
 
47
+ ### Upgrade installed gac
48
+
49
+ ```bash
50
+ uv tool upgrade gac
51
+ ```
52
+
47
53
  ---
48
54
 
49
55
  ## Key Features
@@ -51,7 +57,7 @@ gac
51
57
  ### 🌐 **Supported Providers**
52
58
 
53
59
  - **Anthropic** • **Cerebras** • **Chutes.ai** • **DeepSeek** • **Fireworks**
54
- - **Gemini** • **Groq** • **LM Studio** • **MiniMax** • **Ollama** • **OpenAI**
60
+ - **Gemini** • **Groq** • **LM Studio** • **MiniMax** • **Mistral** • **Ollama** • **OpenAI**
55
61
  - **OpenRouter** • **Streamlake** • **Synthetic.new** • **Together AI**
56
62
  - **Z.AI** • **Z.AI Coding** • **Custom Endpoints (Anthropic/OpenAI)**
57
63
 
@@ -76,7 +82,7 @@ gac
76
82
 
77
83
  ### 💻 **Developer Experience**
78
84
 
79
- - **Interactive feedback**: Type `r` to reroll, or directly type your feedback like `make it shorter` or `focus on the bug fix`
85
+ - **Interactive feedback**: Type `r` to reroll, `e` to edit in-place with vi/emacs keybindings, or directly type your feedback like `make it shorter` or `focus on the bug fix`
80
86
  - **One-command workflows**: Complete workflows with flags like `gac -ayp` (stage all, auto-confirm, push)
81
87
  - **Git integration**: Respects pre-commit and lefthook hooks, running them before expensive LLM operations
82
88
 
@@ -99,7 +105,7 @@ git add .
99
105
  # Generate and commit with LLM
100
106
  gac
101
107
 
102
- # Review → y (commit) | n (cancel) | r (reroll) | or type feedback
108
+ # Review → y (commit) | n (cancel) | r (reroll) | e (edit) | or type feedback
103
109
  ```
104
110
 
105
111
  ### Common Commands
@@ -136,13 +142,18 @@ gac --skip-secret-scan
136
142
 
137
143
  ### Interactive Feedback System
138
144
 
139
- Not happy with the result? You have two options:
145
+ Not happy with the result? You have several options:
140
146
 
141
147
  ```bash
142
148
  # Simple reroll (no feedback)
143
149
  r
144
150
 
145
- # Or just type your feedback directly - no prefix needed!
151
+ # Edit in-place with rich terminal editing
152
+ e
153
+ # Uses prompt_toolkit for multi-line editing with vi/emacs keybindings
154
+ # Press Esc+Enter or Ctrl+S to submit, Ctrl+C to cancel
155
+
156
+ # Or just type your feedback directly!
146
157
  make it shorter and focus on the performance improvement
147
158
  use conventional commit format with scope
148
159
  explain the security implications
@@ -150,6 +161,13 @@ explain the security implications
150
161
  # Press Enter on empty input to see the prompt again
151
162
  ```
152
163
 
164
+ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
165
+
166
+ - **Edit naturally**: Multi-line editing with familiar vi/emacs key bindings
167
+ - **Make quick fixes**: Correct typos, adjust wording, or refine formatting
168
+ - **Add details**: Include information the LLM might have missed
169
+ - **Restructure**: Reorganize bullet points or change the message structure
170
+
153
171
  ---
154
172
 
155
173
  ## Configuration
@@ -41,6 +41,7 @@ dependencies = [
41
41
  "halo",
42
42
  "questionary",
43
43
  "rich>=14.1.0",
44
+ "prompt_toolkit>=3.0.36",
44
45
 
45
46
  ]
46
47
 
@@ -1,3 +1,3 @@
1
1
  """Version information for gac package."""
2
2
 
3
- __version__ = "2.0.0"
3
+ __version__ = "2.2.0"
@@ -21,6 +21,7 @@ from gac.providers import (
21
21
  call_groq_api,
22
22
  call_lmstudio_api,
23
23
  call_minimax_api,
24
+ call_mistral_api,
24
25
  call_ollama_api,
25
26
  call_openai_api,
26
27
  call_openrouter_api,
@@ -94,6 +95,7 @@ def generate_commit_message(
94
95
  "groq": call_groq_api,
95
96
  "lm-studio": call_lmstudio_api,
96
97
  "minimax": call_minimax_api,
98
+ "mistral": call_mistral_api,
97
99
  "ollama": call_ollama_api,
98
100
  "openai": call_openai_api,
99
101
  "openrouter": call_openrouter_api,
@@ -102,6 +102,7 @@ def generate_with_retries(
102
102
  "groq",
103
103
  "lm-studio",
104
104
  "minimax",
105
+ "mistral",
105
106
  "ollama",
106
107
  "openai",
107
108
  "openrouter",
@@ -154,5 +154,15 @@ cli.add_command(init_cli)
154
154
  cli.add_command(language_cli)
155
155
  cli.add_command(diff_cli)
156
156
 
157
+
158
+ @click.command(context_settings=language_cli.context_settings)
159
+ @click.pass_context
160
+ def lang(ctx):
161
+ """Set the language for commit messages interactively. (Alias for 'language')"""
162
+ ctx.forward(language_cli)
163
+
164
+
165
+ cli.add_command(lang) # Add the lang alias
166
+
157
167
  if __name__ == "__main__":
158
168
  cli()
@@ -21,7 +21,7 @@ class EnvDefaults:
21
21
  MAX_RETRIES: int = 3
22
22
  TEMPERATURE: float = 1
23
23
  MAX_OUTPUT_TOKENS: int = 1024 # includes reasoning tokens
24
- WARNING_LIMIT_TOKENS: int = 16384
24
+ WARNING_LIMIT_TOKENS: int = 32768
25
25
  ALWAYS_INCLUDE_SCOPE: bool = False
26
26
  SKIP_SECRET_SCAN: bool = False
27
27
  VERBOSE: bool = False
@@ -214,6 +214,37 @@ class Languages:
214
214
  "fi": "Finnish",
215
215
  }
216
216
 
217
+ # List of languages with display names and English names for CLI selection
218
+ # Format: (display_name, english_name)
219
+ LANGUAGES: list[tuple[str, str]] = [
220
+ ("English", "English"),
221
+ ("简体中文", "Simplified Chinese"),
222
+ ("繁體中文", "Traditional Chinese"),
223
+ ("日本語", "Japanese"),
224
+ ("한국어", "Korean"),
225
+ ("Español", "Spanish"),
226
+ ("Português", "Portuguese"),
227
+ ("Français", "French"),
228
+ ("Deutsch", "German"),
229
+ ("Русский", "Russian"),
230
+ ("हिन्दी", "Hindi"),
231
+ ("Italiano", "Italian"),
232
+ ("Polski", "Polish"),
233
+ ("Türkçe", "Turkish"),
234
+ ("Nederlands", "Dutch"),
235
+ ("Tiếng Việt", "Vietnamese"),
236
+ ("ไทย", "Thai"),
237
+ ("Bahasa Indonesia", "Indonesian"),
238
+ ("Svenska", "Swedish"),
239
+ ("العربية", "Arabic"),
240
+ ("עברית", "Hebrew"),
241
+ ("Ελληνικά", "Greek"),
242
+ ("Dansk", "Danish"),
243
+ ("Norsk", "Norwegian"),
244
+ ("Suomi", "Finnish"),
245
+ ("Custom", "Custom"),
246
+ ]
247
+
217
248
  @staticmethod
218
249
  def resolve_code(language: str) -> str:
219
250
  """Resolve a language code to its full name.
@@ -6,6 +6,8 @@ import click
6
6
  import questionary
7
7
  from dotenv import set_key
8
8
 
9
+ from gac.constants import Languages
10
+
9
11
  GAC_ENV_PATH = Path.home() / ".gac.env"
10
12
 
11
13
 
@@ -33,7 +35,7 @@ def init() -> None:
33
35
 
34
36
  providers = [
35
37
  ("Anthropic", "claude-haiku-4-5"),
36
- ("Cerebras", "qwen-3-coder-480b"),
38
+ ("Cerebras", "zai-glm-4.6"),
37
39
  ("Chutes", "zai-org/GLM-4.6-FP8"),
38
40
  ("Custom (Anthropic)", ""),
39
41
  ("Custom (OpenAI)", ""),
@@ -43,6 +45,7 @@ def init() -> None:
43
45
  ("Groq", "meta-llama/llama-4-maverick-17b-128e-instruct"),
44
46
  ("LM Studio", "gemma3"),
45
47
  ("MiniMax", "MiniMax-M2"),
48
+ ("Mistral", "mistral-small-latest"),
46
49
  ("Ollama", "gemma3"),
47
50
  ("OpenAI", "gpt-4.1-mini"),
48
51
  ("OpenRouter", "openrouter/auto"),
@@ -153,36 +156,7 @@ def init() -> None:
153
156
 
154
157
  # Language selection
155
158
  click.echo("\n")
156
- languages = [
157
- ("English", "English"),
158
- ("简体中文", "Simplified Chinese"),
159
- ("繁體中文", "Traditional Chinese"),
160
- ("日本語", "Japanese"),
161
- ("한국어", "Korean"),
162
- ("Español", "Spanish"),
163
- ("Português", "Portuguese"),
164
- ("Français", "French"),
165
- ("Deutsch", "German"),
166
- ("Русский", "Russian"),
167
- ("हिन्दी", "Hindi"),
168
- ("Italiano", "Italian"),
169
- ("Polski", "Polish"),
170
- ("Türkçe", "Turkish"),
171
- ("Nederlands", "Dutch"),
172
- ("Tiếng Việt", "Vietnamese"),
173
- ("ไทย", "Thai"),
174
- ("Bahasa Indonesia", "Indonesian"),
175
- ("Svenska", "Swedish"),
176
- ("العربية", "Arabic"),
177
- ("עברית", "Hebrew"),
178
- ("Ελληνικά", "Greek"),
179
- ("Dansk", "Danish"),
180
- ("Norsk", "Norwegian"),
181
- ("Suomi", "Finnish"),
182
- ("Custom", "Custom"),
183
- ]
184
-
185
- display_names = [lang[0] for lang in languages]
159
+ display_names = [lang[0] for lang in Languages.LANGUAGES]
186
160
  language_selection = questionary.select(
187
161
  "Select a language for commit messages:", choices=display_names, use_shortcuts=True, use_arrow_keys=True
188
162
  ).ask()
@@ -202,7 +176,7 @@ def init() -> None:
202
176
  language_value = custom_language.strip()
203
177
  else:
204
178
  # Find the English name for the selected language
205
- language_value = next(lang[1] for lang in languages if lang[0] == language_selection)
179
+ language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == language_selection)
206
180
 
207
181
  if language_value:
208
182
  # Ask about prefix translation
@@ -6,6 +6,8 @@ import click
6
6
  import questionary
7
7
  from dotenv import set_key, unset_key
8
8
 
9
+ from gac.constants import Languages
10
+
9
11
  GAC_ENV_PATH = Path.home() / ".gac.env"
10
12
 
11
13
 
@@ -14,38 +16,7 @@ def language() -> None:
14
16
  """Set the language for commit messages interactively."""
15
17
  click.echo("Select a language for your commit messages:\n")
16
18
 
17
- # Languages sorted by programmer population likelihood
18
- # Based on GitHub statistics and global developer demographics
19
- languages = [
20
- ("English", "English"),
21
- ("简体中文", "Simplified Chinese"),
22
- ("繁體中文", "Traditional Chinese"),
23
- ("日本語", "Japanese"),
24
- ("한국어", "Korean"),
25
- ("Español", "Spanish"),
26
- ("Português", "Portuguese"),
27
- ("Français", "French"),
28
- ("Deutsch", "German"),
29
- ("Русский", "Russian"),
30
- ("हिन्दी", "Hindi"),
31
- ("Italiano", "Italian"),
32
- ("Polski", "Polish"),
33
- ("Türkçe", "Turkish"),
34
- ("Nederlands", "Dutch"),
35
- ("Tiếng Việt", "Vietnamese"),
36
- ("ไทย", "Thai"),
37
- ("Bahasa Indonesia", "Indonesian"),
38
- ("Svenska", "Swedish"),
39
- ("العربية", "Arabic"),
40
- ("עברית", "Hebrew"),
41
- ("Ελληνικά", "Greek"),
42
- ("Dansk", "Danish"),
43
- ("Norsk", "Norwegian"),
44
- ("Suomi", "Finnish"),
45
- ("Custom", "Custom"),
46
- ]
47
-
48
- display_names = [lang[0] for lang in languages]
19
+ display_names = [lang[0] for lang in Languages.LANGUAGES]
49
20
  selection = questionary.select(
50
21
  "Choose your language:", choices=display_names, use_shortcuts=True, use_arrow_keys=True
51
22
  ).ask()
@@ -78,7 +49,7 @@ def language() -> None:
78
49
  language_value = custom_language.strip()
79
50
  else:
80
51
  # Find the English name for the selected language
81
- language_value = next(lang[1] for lang in languages if lang[0] == selection)
52
+ language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == selection)
82
53
 
83
54
  # Ask about prefix translation
84
55
  click.echo() # Blank line for spacing
@@ -25,6 +25,7 @@ from gac.git import (
25
25
  from gac.preprocess import preprocess_diff
26
26
  from gac.prompt import build_prompt, clean_commit_message
27
27
  from gac.security import get_affected_files, scan_staged_diff
28
+ from gac.utils import edit_commit_message_inplace
28
29
 
29
30
  logger = logging.getLogger(__name__)
30
31
 
@@ -277,7 +278,7 @@ def main(
277
278
  if require_confirmation:
278
279
  while True:
279
280
  response = click.prompt(
280
- "Proceed with commit above? [y/n/r/<feedback>]",
281
+ "Proceed with commit above? [y/n/r/e/<feedback>]",
281
282
  type=str,
282
283
  show_default=False,
283
284
  ).strip()
@@ -290,6 +291,18 @@ def main(
290
291
  sys.exit(0)
291
292
  if response == "":
292
293
  continue
294
+ if response_lower in ["e", "edit"]:
295
+ edited_message = edit_commit_message_inplace(commit_message)
296
+ if edited_message:
297
+ commit_message = edited_message
298
+ conversation_messages[-1] = {"role": "assistant", "content": commit_message}
299
+ logger.info("Commit message edited by user")
300
+ console.print("\n[bold green]Edited commit message:[/bold green]")
301
+ console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
302
+ else:
303
+ console.print("[yellow]Using previous message.[/yellow]")
304
+ console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
305
+ continue
293
306
  if response_lower in ["r", "reroll"]:
294
307
  feedback_message = (
295
308
  "Please provide an alternative commit message using the same repository context."
@@ -11,6 +11,7 @@ from .gemini import call_gemini_api
11
11
  from .groq import call_groq_api
12
12
  from .lmstudio import call_lmstudio_api
13
13
  from .minimax import call_minimax_api
14
+ from .mistral import call_mistral_api
14
15
  from .ollama import call_ollama_api
15
16
  from .openai import call_openai_api
16
17
  from .openrouter import call_openrouter_api
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "call_groq_api",
32
33
  "call_lmstudio_api",
33
34
  "call_minimax_api",
35
+ "call_mistral_api",
34
36
  "call_ollama_api",
35
37
  "call_openai_api",
36
38
  "call_openrouter_api",
@@ -0,0 +1,38 @@
1
+ """Mistral API provider for gac."""
2
+
3
+ import os
4
+
5
+ import httpx
6
+
7
+ from gac.errors import AIError
8
+
9
+
10
+ def call_mistral_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
11
+ """Call Mistral API directly."""
12
+ api_key = os.getenv("MISTRAL_API_KEY")
13
+ if not api_key:
14
+ raise AIError.authentication_error("MISTRAL_API_KEY not found in environment variables")
15
+
16
+ url = "https://api.mistral.ai/v1/chat/completions"
17
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
18
+
19
+ data = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens}
20
+
21
+ try:
22
+ response = httpx.post(url, headers=headers, json=data, timeout=120)
23
+ response.raise_for_status()
24
+ response_data = response.json()
25
+ content = response_data["choices"][0]["message"]["content"]
26
+ if content is None:
27
+ raise AIError.model_error("Mistral API returned null content")
28
+ if content == "":
29
+ raise AIError.model_error("Mistral API returned empty content")
30
+ return content
31
+ except httpx.HTTPStatusError as e:
32
+ if e.response.status_code == 429:
33
+ raise AIError.rate_limit_error(f"Mistral API rate limit exceeded: {e.response.text}") from e
34
+ raise AIError.model_error(f"Mistral API error: {e.response.status_code} - {e.response.text}") from e
35
+ except httpx.TimeoutException as e:
36
+ raise AIError.timeout_error(f"Mistral API request timed out: {str(e)}") from e
37
+ except Exception as e:
38
+ raise AIError.model_error(f"Error calling Mistral API: {str(e)}") from e
@@ -0,0 +1,270 @@
1
+ """Utility functions for gac."""
2
+
3
+ import logging
4
+ import subprocess
5
+
6
+ from rich.console import Console
7
+ from rich.theme import Theme
8
+
9
+ from gac.constants import Logging
10
+ from gac.errors import GacError
11
+
12
+
13
+ def setup_logging(
14
+ log_level: int | str = Logging.DEFAULT_LEVEL,
15
+ quiet: bool = False,
16
+ force: bool = False,
17
+ suppress_noisy: bool = False,
18
+ ) -> None:
19
+ """Configure logging for the application.
20
+
21
+ Args:
22
+ log_level: Log level to use (DEBUG, INFO, WARNING, ERROR)
23
+ quiet: If True, suppress all output except errors
24
+ force: If True, force reconfiguration of logging
25
+ suppress_noisy: If True, suppress noisy third-party loggers
26
+ """
27
+ if isinstance(log_level, str):
28
+ log_level = getattr(logging, log_level.upper(), logging.WARNING)
29
+
30
+ if quiet:
31
+ log_level = logging.ERROR
32
+
33
+ kwargs = {"force": force} if force else {}
34
+
35
+ logging.basicConfig(
36
+ level=log_level,
37
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
38
+ datefmt="%Y-%m-%d %H:%M:%S",
39
+ **kwargs, # type: ignore[arg-type]
40
+ )
41
+
42
+ if suppress_noisy:
43
+ for noisy_logger in ["requests", "urllib3"]:
44
+ logging.getLogger(noisy_logger).setLevel(logging.WARNING)
45
+
46
+ logger.info(f"Logging initialized with level: {logging.getLevelName(log_level)}")
47
+
48
+
49
+ theme = Theme(
50
+ {
51
+ "success": "green bold",
52
+ "info": "blue",
53
+ "warning": "yellow",
54
+ "error": "red bold",
55
+ "header": "magenta",
56
+ "notification": "bright_cyan bold",
57
+ }
58
+ )
59
+ console = Console(theme=theme)
60
+ logger = logging.getLogger(__name__)
61
+
62
+
63
+ def print_message(message: str, level: str = "info") -> None:
64
+ """Print a styled message with the specified level."""
65
+ console.print(message, style=level)
66
+
67
+
68
+ def run_subprocess(
69
+ command: list[str],
70
+ silent: bool = False,
71
+ timeout: int = 60,
72
+ check: bool = True,
73
+ strip_output: bool = True,
74
+ raise_on_error: bool = True,
75
+ ) -> str:
76
+ """Run a subprocess command safely and return the output.
77
+
78
+ Args:
79
+ command: List of command arguments
80
+ silent: If True, suppress debug logging
81
+ timeout: Command timeout in seconds
82
+ check: Whether to check return code (for compatibility)
83
+ strip_output: Whether to strip whitespace from output
84
+ raise_on_error: Whether to raise an exception on error
85
+
86
+ Returns:
87
+ Command output as string
88
+
89
+ Raises:
90
+ GacError: If the command times out
91
+ subprocess.CalledProcessError: If the command fails and raise_on_error is True
92
+ """
93
+ if not silent:
94
+ logger.debug(f"Running command: {' '.join(command)}")
95
+
96
+ try:
97
+ result = subprocess.run(
98
+ command,
99
+ capture_output=True,
100
+ text=True,
101
+ check=False,
102
+ timeout=timeout,
103
+ )
104
+
105
+ should_raise = result.returncode != 0 and (check or raise_on_error)
106
+
107
+ if should_raise:
108
+ if not silent:
109
+ logger.debug(f"Command stderr: {result.stderr}")
110
+ raise subprocess.CalledProcessError(result.returncode, command, result.stdout, result.stderr)
111
+
112
+ output = result.stdout
113
+ if strip_output:
114
+ output = output.strip()
115
+
116
+ return output
117
+ except subprocess.TimeoutExpired as e:
118
+ logger.error(f"Command timed out after {timeout} seconds: {' '.join(command)}")
119
+ raise GacError(f"Command timed out: {' '.join(command)}") from e
120
+ except subprocess.CalledProcessError as e:
121
+ if not silent:
122
+ logger.error(f"Command failed: {e.stderr.strip() if e.stderr else str(e)}")
123
+ if raise_on_error:
124
+ raise
125
+ return ""
126
+ except Exception as e:
127
+ if not silent:
128
+ logger.debug(f"Command error: {e}")
129
+ if raise_on_error:
130
+ # Convert generic exceptions to CalledProcessError for consistency
131
+ raise subprocess.CalledProcessError(1, command, "", str(e)) from e
132
+ return ""
133
+
134
+
135
+ def edit_commit_message_inplace(message: str) -> str | None:
136
+ """Edit commit message in-place using rich terminal editing.
137
+
138
+ Uses prompt_toolkit to provide a rich editing experience with:
139
+ - Multi-line editing
140
+ - Vi/Emacs key bindings
141
+ - Line editing capabilities
142
+ - Esc+Enter or Ctrl+S to submit
143
+ - Ctrl+C to cancel
144
+
145
+ Args:
146
+ message: The initial commit message
147
+
148
+ Returns:
149
+ The edited commit message, or None if editing was cancelled
150
+
151
+ Example:
152
+ >>> edited = edit_commit_message_inplace("feat: add feature")
153
+ >>> # User can edit the message using vi/emacs key bindings
154
+ >>> # Press Esc+Enter or Ctrl+S to submit
155
+ """
156
+ from prompt_toolkit import Application
157
+ from prompt_toolkit.buffer import Buffer
158
+ from prompt_toolkit.document import Document
159
+ from prompt_toolkit.enums import EditingMode
160
+ from prompt_toolkit.key_binding import KeyBindings
161
+ from prompt_toolkit.layout import HSplit, Layout, Window
162
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
163
+ from prompt_toolkit.layout.margins import ScrollbarMargin
164
+ from prompt_toolkit.styles import Style
165
+
166
+ try:
167
+ console.print("\n[info]Edit commit message:[/info]")
168
+ console.print()
169
+
170
+ # Create buffer for text editing
171
+ text_buffer = Buffer(
172
+ document=Document(text=message, cursor_position=0),
173
+ multiline=True,
174
+ enable_history_search=False,
175
+ )
176
+
177
+ # Track submission state
178
+ cancelled = {"value": False}
179
+ submitted = {"value": False}
180
+
181
+ # Create text editor window
182
+ text_window = Window(
183
+ content=BufferControl(
184
+ buffer=text_buffer,
185
+ focus_on_click=True,
186
+ ),
187
+ height=lambda: max(5, message.count("\n") + 3),
188
+ wrap_lines=True,
189
+ right_margins=[ScrollbarMargin()],
190
+ )
191
+
192
+ # Create hint window
193
+ hint_window = Window(
194
+ content=FormattedTextControl(
195
+ text=[("class:hint", " Esc+Enter or Ctrl+S to submit | Ctrl+C to cancel ")],
196
+ ),
197
+ height=1,
198
+ dont_extend_height=True,
199
+ )
200
+
201
+ # Create layout
202
+ root_container = HSplit(
203
+ [
204
+ text_window,
205
+ hint_window,
206
+ ]
207
+ )
208
+
209
+ layout = Layout(root_container, focused_element=text_window)
210
+
211
+ # Create key bindings
212
+ kb = KeyBindings()
213
+
214
+ @kb.add("c-s")
215
+ def _(event):
216
+ """Submit with Ctrl+S."""
217
+ submitted["value"] = True
218
+ event.app.exit()
219
+
220
+ @kb.add("c-c")
221
+ def _(event):
222
+ """Cancel editing."""
223
+ cancelled["value"] = True
224
+ event.app.exit()
225
+
226
+ @kb.add("escape", "enter")
227
+ def _(event):
228
+ """Submit with Esc+Enter."""
229
+ submitted["value"] = True
230
+ event.app.exit()
231
+
232
+ # Create and run application
233
+ custom_style = Style.from_dict(
234
+ {
235
+ "hint": "#888888",
236
+ }
237
+ )
238
+
239
+ app: Application[None] = Application(
240
+ layout=layout,
241
+ key_bindings=kb,
242
+ full_screen=False,
243
+ mouse_support=False,
244
+ editing_mode=EditingMode.VI, # Enable vi key bindings
245
+ style=custom_style,
246
+ )
247
+
248
+ app.run()
249
+
250
+ # Handle result
251
+ if cancelled["value"]:
252
+ console.print("\n[yellow]Edit cancelled.[/yellow]")
253
+ return None
254
+
255
+ if submitted["value"]:
256
+ edited_message = text_buffer.text.strip()
257
+ if not edited_message:
258
+ console.print("[yellow]Commit message cannot be empty. Edit cancelled.[/yellow]")
259
+ return None
260
+ return edited_message
261
+
262
+ return None
263
+
264
+ except (EOFError, KeyboardInterrupt):
265
+ console.print("\n[yellow]Edit cancelled.[/yellow]")
266
+ return None
267
+ except Exception as e:
268
+ logger.error(f"Error during in-place editing: {e}")
269
+ console.print(f"[error]Failed to edit commit message: {e}[/error]")
270
+ return None
@@ -1,132 +0,0 @@
1
- """Utility functions for gac."""
2
-
3
- import logging
4
- import subprocess
5
-
6
- from rich.console import Console
7
- from rich.theme import Theme
8
-
9
- from gac.constants import Logging
10
- from gac.errors import GacError
11
-
12
-
13
- def setup_logging(
14
- log_level: int | str = Logging.DEFAULT_LEVEL,
15
- quiet: bool = False,
16
- force: bool = False,
17
- suppress_noisy: bool = False,
18
- ) -> None:
19
- """Configure logging for the application.
20
-
21
- Args:
22
- log_level: Log level to use (DEBUG, INFO, WARNING, ERROR)
23
- quiet: If True, suppress all output except errors
24
- force: If True, force reconfiguration of logging
25
- suppress_noisy: If True, suppress noisy third-party loggers
26
- """
27
- if isinstance(log_level, str):
28
- log_level = getattr(logging, log_level.upper(), logging.WARNING)
29
-
30
- if quiet:
31
- log_level = logging.ERROR
32
-
33
- kwargs = {"force": force} if force else {}
34
-
35
- logging.basicConfig(
36
- level=log_level,
37
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
38
- datefmt="%Y-%m-%d %H:%M:%S",
39
- **kwargs, # type: ignore[arg-type]
40
- )
41
-
42
- if suppress_noisy:
43
- for noisy_logger in ["requests", "urllib3"]:
44
- logging.getLogger(noisy_logger).setLevel(logging.WARNING)
45
-
46
- logger.info(f"Logging initialized with level: {logging.getLevelName(log_level)}")
47
-
48
-
49
- theme = Theme(
50
- {
51
- "success": "green bold",
52
- "info": "blue",
53
- "warning": "yellow",
54
- "error": "red bold",
55
- "header": "magenta",
56
- "notification": "bright_cyan bold",
57
- }
58
- )
59
- console = Console(theme=theme)
60
- logger = logging.getLogger(__name__)
61
-
62
-
63
- def print_message(message: str, level: str = "info") -> None:
64
- """Print a styled message with the specified level."""
65
- console.print(message, style=level)
66
-
67
-
68
- def run_subprocess(
69
- command: list[str],
70
- silent: bool = False,
71
- timeout: int = 60,
72
- check: bool = True,
73
- strip_output: bool = True,
74
- raise_on_error: bool = True,
75
- ) -> str:
76
- """Run a subprocess command safely and return the output.
77
-
78
- Args:
79
- command: List of command arguments
80
- silent: If True, suppress debug logging
81
- timeout: Command timeout in seconds
82
- check: Whether to check return code (for compatibility)
83
- strip_output: Whether to strip whitespace from output
84
- raise_on_error: Whether to raise an exception on error
85
-
86
- Returns:
87
- Command output as string
88
-
89
- Raises:
90
- GacError: If the command times out
91
- subprocess.CalledProcessError: If the command fails and raise_on_error is True
92
- """
93
- if not silent:
94
- logger.debug(f"Running command: {' '.join(command)}")
95
-
96
- try:
97
- result = subprocess.run(
98
- command,
99
- capture_output=True,
100
- text=True,
101
- check=False,
102
- timeout=timeout,
103
- )
104
-
105
- should_raise = result.returncode != 0 and (check or raise_on_error)
106
-
107
- if should_raise:
108
- if not silent:
109
- logger.debug(f"Command stderr: {result.stderr}")
110
- raise subprocess.CalledProcessError(result.returncode, command, result.stdout, result.stderr)
111
-
112
- output = result.stdout
113
- if strip_output:
114
- output = output.strip()
115
-
116
- return output
117
- except subprocess.TimeoutExpired as e:
118
- logger.error(f"Command timed out after {timeout} seconds: {' '.join(command)}")
119
- raise GacError(f"Command timed out: {' '.join(command)}") from e
120
- except subprocess.CalledProcessError as e:
121
- if not silent:
122
- logger.error(f"Command failed: {e.stderr.strip() if e.stderr else str(e)}")
123
- if raise_on_error:
124
- raise
125
- return ""
126
- except Exception as e:
127
- if not silent:
128
- logger.debug(f"Command error: {e}")
129
- if raise_on_error:
130
- # Convert generic exceptions to CalledProcessError for consistency
131
- raise subprocess.CalledProcessError(1, command, "", str(e)) from e
132
- return ""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes