gac 2.1.0__tar.gz → 2.3.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 (47) hide show
  1. {gac-2.1.0 → gac-2.3.0}/PKG-INFO +21 -4
  2. {gac-2.1.0 → gac-2.3.0}/README.md +19 -3
  3. {gac-2.1.0 → gac-2.3.0}/pyproject.toml +1 -0
  4. {gac-2.1.0 → gac-2.3.0}/src/gac/__version__.py +1 -1
  5. {gac-2.1.0 → gac-2.3.0}/src/gac/ai.py +26 -0
  6. {gac-2.1.0 → gac-2.3.0}/src/gac/ai_utils.py +28 -13
  7. {gac-2.1.0 → gac-2.3.0}/src/gac/cli.py +4 -0
  8. {gac-2.1.0 → gac-2.3.0}/src/gac/constants.py +2 -2
  9. {gac-2.1.0 → gac-2.3.0}/src/gac/git.py +42 -0
  10. gac-2.3.0/src/gac/init_cli.py +316 -0
  11. {gac-2.1.0 → gac-2.3.0}/src/gac/language_cli.py +8 -8
  12. gac-2.3.0/src/gac/main.py +718 -0
  13. {gac-2.1.0 → gac-2.3.0}/src/gac/prompt.py +101 -15
  14. {gac-2.1.0 → gac-2.3.0}/src/gac/security.py +1 -1
  15. gac-2.3.0/src/gac/utils.py +270 -0
  16. gac-2.3.0/src/gac/workflow_utils.py +131 -0
  17. gac-2.1.0/src/gac/init_cli.py +0 -203
  18. gac-2.1.0/src/gac/main.py +0 -369
  19. gac-2.1.0/src/gac/utils.py +0 -132
  20. {gac-2.1.0 → gac-2.3.0}/.gitignore +0 -0
  21. {gac-2.1.0 → gac-2.3.0}/LICENSE +0 -0
  22. {gac-2.1.0 → gac-2.3.0}/src/gac/__init__.py +0 -0
  23. {gac-2.1.0 → gac-2.3.0}/src/gac/config.py +0 -0
  24. {gac-2.1.0 → gac-2.3.0}/src/gac/config_cli.py +0 -0
  25. {gac-2.1.0 → gac-2.3.0}/src/gac/diff_cli.py +0 -0
  26. {gac-2.1.0 → gac-2.3.0}/src/gac/errors.py +0 -0
  27. {gac-2.1.0 → gac-2.3.0}/src/gac/preprocess.py +0 -0
  28. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/__init__.py +0 -0
  29. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/anthropic.py +0 -0
  30. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/cerebras.py +0 -0
  31. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/chutes.py +0 -0
  32. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/custom_anthropic.py +0 -0
  33. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/custom_openai.py +0 -0
  34. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/deepseek.py +0 -0
  35. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/fireworks.py +0 -0
  36. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/gemini.py +0 -0
  37. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/groq.py +0 -0
  38. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/lmstudio.py +0 -0
  39. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/minimax.py +0 -0
  40. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/mistral.py +0 -0
  41. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/ollama.py +0 -0
  42. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/openai.py +0 -0
  43. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/openrouter.py +0 -0
  44. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/streamlake.py +0 -0
  45. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/synthetic.py +0 -0
  46. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/together.py +0 -0
  47. {gac-2.1.0 → gac-2.3.0}/src/gac/providers/zai.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gac
3
- Version: 2.1.0
3
+ Version: 2.3.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
@@ -107,6 +108,7 @@ uv tool upgrade gac
107
108
  - **Understands intent**: Analyzes code structure, logic, and patterns to understand the "why" behind your changes, not just what changed
108
109
  - **Semantic awareness**: Recognizes refactoring, bug fixes, features, and breaking changes to generate contextually appropriate messages
109
110
  - **Intelligent filtering**: Prioritizes meaningful changes while ignoring generated files, dependencies, and artifacts
111
+ - **Intelligent commit grouping** - Automatically group related changes into multiple logical commits with `--group`
110
112
 
111
113
  ### 📝 **Multiple Message Formats**
112
114
 
@@ -123,7 +125,7 @@ uv tool upgrade gac
123
125
 
124
126
  ### 💻 **Developer Experience**
125
127
 
126
- - **Interactive feedback**: Type `r` to reroll, or directly type your feedback like `make it shorter` or `focus on the bug fix`
128
+ - **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`
127
129
  - **One-command workflows**: Complete workflows with flags like `gac -ayp` (stage all, auto-confirm, push)
128
130
  - **Git integration**: Respects pre-commit and lefthook hooks, running them before expensive LLM operations
129
131
 
@@ -146,7 +148,7 @@ git add .
146
148
  # Generate and commit with LLM
147
149
  gac
148
150
 
149
- # Review → y (commit) | n (cancel) | r (reroll) | or type feedback
151
+ # Review → y (commit) | n (cancel) | r (reroll) | e (edit) | or type feedback
150
152
  ```
151
153
 
152
154
  ### Common Commands
@@ -174,6 +176,9 @@ gac -v -s
174
176
  # Quick one-liner for small changes
175
177
  gac -o
176
178
 
179
+ # Group changes into logically related commits
180
+ gac -ag
181
+
177
182
  # Debug what the LLM sees
178
183
  gac --show-prompt
179
184
 
@@ -183,12 +188,17 @@ gac --skip-secret-scan
183
188
 
184
189
  ### Interactive Feedback System
185
190
 
186
- Not happy with the result? You have two options:
191
+ Not happy with the result? You have several options:
187
192
 
188
193
  ```bash
189
194
  # Simple reroll (no feedback)
190
195
  r
191
196
 
197
+ # Edit in-place with rich terminal editing
198
+ e
199
+ # Uses prompt_toolkit for multi-line editing with vi/emacs keybindings
200
+ # Press Esc+Enter or Ctrl+S to submit, Ctrl+C to cancel
201
+
192
202
  # Or just type your feedback directly!
193
203
  make it shorter and focus on the performance improvement
194
204
  use conventional commit format with scope
@@ -197,6 +207,13 @@ explain the security implications
197
207
  # Press Enter on empty input to see the prompt again
198
208
  ```
199
209
 
210
+ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
211
+
212
+ - **Edit naturally**: Multi-line editing with familiar vi/emacs key bindings
213
+ - **Make quick fixes**: Correct typos, adjust wording, or refine formatting
214
+ - **Add details**: Include information the LLM might have missed
215
+ - **Restructure**: Reorganize bullet points or change the message structure
216
+
200
217
  ---
201
218
 
202
219
  ## Configuration
@@ -66,6 +66,7 @@ uv tool upgrade gac
66
66
  - **Understands intent**: Analyzes code structure, logic, and patterns to understand the "why" behind your changes, not just what changed
67
67
  - **Semantic awareness**: Recognizes refactoring, bug fixes, features, and breaking changes to generate contextually appropriate messages
68
68
  - **Intelligent filtering**: Prioritizes meaningful changes while ignoring generated files, dependencies, and artifacts
69
+ - **Intelligent commit grouping** - Automatically group related changes into multiple logical commits with `--group`
69
70
 
70
71
  ### 📝 **Multiple Message Formats**
71
72
 
@@ -82,7 +83,7 @@ uv tool upgrade gac
82
83
 
83
84
  ### 💻 **Developer Experience**
84
85
 
85
- - **Interactive feedback**: Type `r` to reroll, or directly type your feedback like `make it shorter` or `focus on the bug fix`
86
+ - **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`
86
87
  - **One-command workflows**: Complete workflows with flags like `gac -ayp` (stage all, auto-confirm, push)
87
88
  - **Git integration**: Respects pre-commit and lefthook hooks, running them before expensive LLM operations
88
89
 
@@ -105,7 +106,7 @@ git add .
105
106
  # Generate and commit with LLM
106
107
  gac
107
108
 
108
- # Review → y (commit) | n (cancel) | r (reroll) | or type feedback
109
+ # Review → y (commit) | n (cancel) | r (reroll) | e (edit) | or type feedback
109
110
  ```
110
111
 
111
112
  ### Common Commands
@@ -133,6 +134,9 @@ gac -v -s
133
134
  # Quick one-liner for small changes
134
135
  gac -o
135
136
 
137
+ # Group changes into logically related commits
138
+ gac -ag
139
+
136
140
  # Debug what the LLM sees
137
141
  gac --show-prompt
138
142
 
@@ -142,12 +146,17 @@ gac --skip-secret-scan
142
146
 
143
147
  ### Interactive Feedback System
144
148
 
145
- Not happy with the result? You have two options:
149
+ Not happy with the result? You have several options:
146
150
 
147
151
  ```bash
148
152
  # Simple reroll (no feedback)
149
153
  r
150
154
 
155
+ # Edit in-place with rich terminal editing
156
+ e
157
+ # Uses prompt_toolkit for multi-line editing with vi/emacs keybindings
158
+ # Press Esc+Enter or Ctrl+S to submit, Ctrl+C to cancel
159
+
151
160
  # Or just type your feedback directly!
152
161
  make it shorter and focus on the performance improvement
153
162
  use conventional commit format with scope
@@ -156,6 +165,13 @@ explain the security implications
156
165
  # Press Enter on empty input to see the prompt again
157
166
  ```
158
167
 
168
+ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
169
+
170
+ - **Edit naturally**: Multi-line editing with familiar vi/emacs key bindings
171
+ - **Make quick fixes**: Correct typos, adjust wording, or refine formatting
172
+ - **Add details**: Include information the LLM might have missed
173
+ - **Restructure**: Reorganize bullet points or change the message structure
174
+
159
175
  ---
160
176
 
161
177
  ## 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.1.0"
3
+ __version__ = "2.3.0"
@@ -42,6 +42,8 @@ def generate_commit_message(
42
42
  max_tokens: int = EnvDefaults.MAX_OUTPUT_TOKENS,
43
43
  max_retries: int = EnvDefaults.MAX_RETRIES,
44
44
  quiet: bool = False,
45
+ is_group: bool = False,
46
+ skip_success_message: bool = False,
45
47
  ) -> str:
46
48
  """Generate a commit message using direct API calls to AI providers.
47
49
 
@@ -116,6 +118,8 @@ def generate_commit_message(
116
118
  max_tokens=max_tokens,
117
119
  max_retries=max_retries,
118
120
  quiet=quiet,
121
+ is_group=is_group,
122
+ skip_success_message=skip_success_message,
119
123
  )
120
124
  except AIError:
121
125
  # Re-raise AIError exceptions as-is to preserve error classification
@@ -123,3 +127,25 @@ def generate_commit_message(
123
127
  except Exception as e:
124
128
  logger.error(f"Failed to generate commit message: {e}")
125
129
  raise AIError.model_error(f"Failed to generate commit message: {e}") from e
130
+
131
+
132
+ def generate_grouped_commits(
133
+ model: str,
134
+ prompt: list[dict[str, str]],
135
+ temperature: float,
136
+ max_tokens: int,
137
+ max_retries: int,
138
+ quiet: bool = False,
139
+ skip_success_message: bool = False,
140
+ ) -> str:
141
+ """Generate grouped commits JSON response."""
142
+ return generate_commit_message(
143
+ model=model,
144
+ prompt=prompt,
145
+ temperature=temperature,
146
+ max_tokens=max_tokens,
147
+ max_retries=max_retries,
148
+ quiet=quiet,
149
+ is_group=True,
150
+ skip_success_message=skip_success_message,
151
+ )
@@ -83,6 +83,8 @@ def generate_with_retries(
83
83
  max_tokens: int,
84
84
  max_retries: int,
85
85
  quiet: bool = False,
86
+ is_group: bool = False,
87
+ skip_success_message: bool = False,
86
88
  ) -> str:
87
89
  """Generate content with retry logic using direct API calls."""
88
90
  # Parse model string to determine provider and actual model
@@ -121,10 +123,11 @@ def generate_with_retries(
121
123
  raise AIError.model_error("No messages provided for AI generation")
122
124
 
123
125
  # Set up spinner
126
+ message_type = "commit messages" if is_group else "commit message"
124
127
  if quiet:
125
128
  spinner = None
126
129
  else:
127
- spinner = Halo(text=f"Generating commit message with {provider} {model_name}...", spinner="dots")
130
+ spinner = Halo(text=f"Generating {message_type} with {provider} {model_name}...", spinner="dots")
128
131
  spinner.start()
129
132
 
130
133
  last_exception = None
@@ -132,7 +135,7 @@ def generate_with_retries(
132
135
 
133
136
  for attempt in range(max_retries):
134
137
  try:
135
- if not quiet and attempt > 0:
138
+ if not quiet and not skip_success_message and attempt > 0:
136
139
  if spinner:
137
140
  spinner.text = f"Retry {attempt + 1}/{max_retries} with {provider} {model_name}..."
138
141
  logger.info(f"Retry attempt {attempt + 1}/{max_retries}")
@@ -145,7 +148,10 @@ def generate_with_retries(
145
148
  content = provider_func(model=model_name, messages=messages, temperature=temperature, max_tokens=max_tokens)
146
149
 
147
150
  if spinner:
148
- spinner.succeed(f"Generated commit message with {provider} {model_name}")
151
+ if skip_success_message:
152
+ spinner.stop() # Stop spinner without showing success/failure
153
+ else:
154
+ spinner.succeed(f"Generated {message_type} with {provider} {model_name}")
149
155
 
150
156
  if content is not None and content.strip():
151
157
  return content.strip() # type: ignore[no-any-return]
@@ -160,8 +166,8 @@ def generate_with_retries(
160
166
 
161
167
  # For authentication and model errors, don't retry
162
168
  if error_type in ["authentication", "model"]:
163
- if spinner:
164
- spinner.fail(f"Failed to generate commit message with {provider} {model_name}")
169
+ if spinner and not skip_success_message:
170
+ spinner.fail(f"Failed to generate {message_type} with {provider} {model_name}")
165
171
 
166
172
  # Create the appropriate error type based on classification
167
173
  if error_type == "authentication":
@@ -172,23 +178,32 @@ def generate_with_retries(
172
178
  if attempt < max_retries - 1:
173
179
  # Exponential backoff
174
180
  wait_time = 2**attempt
175
- if not quiet:
176
- logger.warning(f"AI generation failed (attempt {attempt + 1}), retrying in {wait_time}s: {str(e)}")
177
-
178
- if spinner:
181
+ if not quiet and not skip_success_message:
182
+ if attempt == 0:
183
+ logger.warning(f"AI generation failed, retrying in {wait_time}s: {str(e)}")
184
+ else:
185
+ logger.warning(
186
+ f"AI generation failed (attempt {attempt + 1}), retrying in {wait_time}s: {str(e)}"
187
+ )
188
+
189
+ if spinner and not skip_success_message:
179
190
  for i in range(wait_time, 0, -1):
180
191
  spinner.text = f"Retry {attempt + 1}/{max_retries} in {i}s..."
181
192
  time.sleep(1)
182
193
  else:
183
194
  time.sleep(wait_time)
184
195
  else:
185
- logger.error(f"AI generation failed after {max_retries} attempts: {str(e)}")
196
+ num_retries = max_retries
197
+ retry_word = "retry" if num_retries == 1 else "retries"
198
+ logger.error(f"AI generation failed after {num_retries} {retry_word}: {str(e)}")
186
199
 
187
- if spinner:
188
- spinner.fail(f"Failed to generate commit message with {provider} {model_name}")
200
+ if spinner and not skip_success_message:
201
+ spinner.fail(f"Failed to generate {message_type} with {provider} {model_name}")
189
202
 
190
203
  # If we get here, all retries failed - use the last classified error type
191
- error_message = f"Failed to generate commit message after {max_retries} attempts"
204
+ num_retries = max_retries
205
+ retry_word = "retry" if num_retries == 1 else "retries"
206
+ error_message = f"Failed to generate {message_type} after {num_retries} {retry_word}"
192
207
  if last_error_type == "authentication":
193
208
  raise AIError.authentication_error(error_message) from last_exception
194
209
  elif last_error_type == "rate_limit":
@@ -28,6 +28,7 @@ logger = logging.getLogger(__name__)
28
28
  @click.group(invoke_without_command=True, context_settings={"ignore_unknown_options": True})
29
29
  # Git workflow options
30
30
  @click.option("--add-all", "-a", is_flag=True, help="Stage all changes before committing")
31
+ @click.option("--group", "-g", is_flag=True, help="Group changes into multiple logical commits")
31
32
  @click.option("--push", "-p", is_flag=True, help="Push changes to remote after committing")
32
33
  @click.option("--dry-run", is_flag=True, help="Dry run the commit workflow")
33
34
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
@@ -70,6 +71,7 @@ logger = logging.getLogger(__name__)
70
71
  def cli(
71
72
  ctx: click.Context,
72
73
  add_all: bool = False,
74
+ group: bool = False,
73
75
  log_level: str = str(config["log_level"]),
74
76
  one_liner: bool = False,
75
77
  push: bool = False,
@@ -109,6 +111,7 @@ def cli(
109
111
  try:
110
112
  main(
111
113
  stage_all=add_all,
114
+ group=group,
112
115
  model=model,
113
116
  hint=hint,
114
117
  one_liner=one_liner,
@@ -131,6 +134,7 @@ def cli(
131
134
 
132
135
  ctx.obj = {
133
136
  "add_all": add_all,
137
+ "group": group,
134
138
  "log_level": log_level,
135
139
  "one_liner": one_liner,
136
140
  "push": push,
@@ -20,8 +20,8 @@ class EnvDefaults:
20
20
 
21
21
  MAX_RETRIES: int = 3
22
22
  TEMPERATURE: float = 1
23
- MAX_OUTPUT_TOKENS: int = 1024 # includes reasoning tokens
24
- WARNING_LIMIT_TOKENS: int = 16384
23
+ MAX_OUTPUT_TOKENS: int = 4096 # includes reasoning tokens
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
@@ -50,6 +50,48 @@ def get_staged_files(file_type: str | None = None, existing_only: bool = False)
50
50
  return []
51
51
 
52
52
 
53
+ def get_staged_status() -> str:
54
+ """Get formatted status of staged files only, excluding unstaged/untracked files.
55
+
56
+ Returns:
57
+ Formatted status string with M/A/D/R indicators
58
+ """
59
+ try:
60
+ output = run_git_command(["diff", "--name-status", "--staged"])
61
+ if not output:
62
+ return "No changes staged for commit."
63
+
64
+ status_map = {
65
+ "M": "modified",
66
+ "A": "new file",
67
+ "D": "deleted",
68
+ "R": "renamed",
69
+ "C": "copied",
70
+ "T": "typechange",
71
+ }
72
+
73
+ status_lines = ["Changes to be committed:"]
74
+ for line in output.splitlines():
75
+ line = line.strip()
76
+ if not line:
77
+ continue
78
+
79
+ # Parse status line (e.g., "M\tfile.py" or "R100\told.py\tnew.py")
80
+ parts = line.split("\t")
81
+ if len(parts) < 2:
82
+ continue
83
+
84
+ change_type = parts[0][0] # First char is the status (M, A, D, R, etc.)
85
+ file_path = parts[-1] # Last part is the new/current file path
86
+
87
+ status_label = status_map.get(change_type, "modified")
88
+ status_lines.append(f"\t{status_label}: {file_path}")
89
+
90
+ return "\n".join(status_lines)
91
+ except GitError:
92
+ return "No changes staged for commit."
93
+
94
+
53
95
  def get_diff(staged: bool = True, color: bool = True, commit1: str | None = None, commit2: str | None = None) -> str:
54
96
  """Get the diff between commits or working tree.
55
97