gac 3.10.3__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.

Potentially problematic release.


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

Files changed (67) hide show
  1. gac/__init__.py +15 -0
  2. gac/__version__.py +3 -0
  3. gac/ai.py +109 -0
  4. gac/ai_utils.py +246 -0
  5. gac/auth_cli.py +214 -0
  6. gac/cli.py +218 -0
  7. gac/commit_executor.py +62 -0
  8. gac/config.py +125 -0
  9. gac/config_cli.py +95 -0
  10. gac/constants.py +328 -0
  11. gac/diff_cli.py +159 -0
  12. gac/errors.py +231 -0
  13. gac/git.py +372 -0
  14. gac/git_state_validator.py +184 -0
  15. gac/grouped_commit_workflow.py +423 -0
  16. gac/init_cli.py +70 -0
  17. gac/interactive_mode.py +182 -0
  18. gac/language_cli.py +377 -0
  19. gac/main.py +476 -0
  20. gac/model_cli.py +430 -0
  21. gac/oauth/__init__.py +27 -0
  22. gac/oauth/claude_code.py +464 -0
  23. gac/oauth/qwen_oauth.py +327 -0
  24. gac/oauth/token_store.py +81 -0
  25. gac/preprocess.py +511 -0
  26. gac/prompt.py +878 -0
  27. gac/prompt_builder.py +88 -0
  28. gac/providers/README.md +437 -0
  29. gac/providers/__init__.py +80 -0
  30. gac/providers/anthropic.py +17 -0
  31. gac/providers/azure_openai.py +57 -0
  32. gac/providers/base.py +329 -0
  33. gac/providers/cerebras.py +15 -0
  34. gac/providers/chutes.py +25 -0
  35. gac/providers/claude_code.py +79 -0
  36. gac/providers/custom_anthropic.py +103 -0
  37. gac/providers/custom_openai.py +44 -0
  38. gac/providers/deepseek.py +15 -0
  39. gac/providers/error_handler.py +139 -0
  40. gac/providers/fireworks.py +15 -0
  41. gac/providers/gemini.py +90 -0
  42. gac/providers/groq.py +15 -0
  43. gac/providers/kimi_coding.py +27 -0
  44. gac/providers/lmstudio.py +80 -0
  45. gac/providers/minimax.py +15 -0
  46. gac/providers/mistral.py +15 -0
  47. gac/providers/moonshot.py +15 -0
  48. gac/providers/ollama.py +73 -0
  49. gac/providers/openai.py +32 -0
  50. gac/providers/openrouter.py +21 -0
  51. gac/providers/protocol.py +71 -0
  52. gac/providers/qwen.py +64 -0
  53. gac/providers/registry.py +58 -0
  54. gac/providers/replicate.py +156 -0
  55. gac/providers/streamlake.py +31 -0
  56. gac/providers/synthetic.py +40 -0
  57. gac/providers/together.py +15 -0
  58. gac/providers/zai.py +31 -0
  59. gac/py.typed +0 -0
  60. gac/security.py +293 -0
  61. gac/utils.py +401 -0
  62. gac/workflow_utils.py +217 -0
  63. gac-3.10.3.dist-info/METADATA +283 -0
  64. gac-3.10.3.dist-info/RECORD +67 -0
  65. gac-3.10.3.dist-info/WHEEL +4 -0
  66. gac-3.10.3.dist-info/entry_points.txt +2 -0
  67. gac-3.10.3.dist-info/licenses/LICENSE +16 -0
gac/main.py ADDED
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env python3
2
+ """Business logic for gac: orchestrates the commit workflow, including git state, formatting,
3
+ prompt building, AI generation, and commit/push operations. This module contains no CLI wiring.
4
+ """
5
+
6
+ import logging
7
+ import sys
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+
12
+ from gac.ai import generate_commit_message
13
+ from gac.ai_utils import count_tokens
14
+ from gac.commit_executor import CommitExecutor
15
+ from gac.config import GACConfig, load_config
16
+ from gac.constants import EnvDefaults
17
+ from gac.errors import AIError, ConfigError, handle_error
18
+ from gac.git import run_lefthook_hooks, run_pre_commit_hooks
19
+ from gac.git_state_validator import GitState, GitStateValidator
20
+ from gac.grouped_commit_workflow import GroupedCommitWorkflow
21
+ from gac.interactive_mode import InteractiveMode
22
+ from gac.prompt import clean_commit_message
23
+ from gac.prompt_builder import PromptBuilder
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ config: GACConfig = load_config()
28
+ console = Console() # Initialize console globally to prevent undefined access
29
+
30
+
31
+ def _parse_model_identifier(model: str) -> tuple[str, str]:
32
+ """Validate and split model identifier into provider and model name."""
33
+ normalized = model.strip()
34
+ if ":" not in normalized:
35
+ message = (
36
+ f"Invalid model format: '{model}'. Expected 'provider:model', e.g. 'openai:gpt-4o-mini'. "
37
+ "Use 'gac config set model <provider:model>' to update your configuration."
38
+ )
39
+ logger.error(message)
40
+ console.print(f"[red]{message}[/red]")
41
+ sys.exit(1)
42
+
43
+ provider, model_name = normalized.split(":", 1)
44
+ if not provider or not model_name:
45
+ message = (
46
+ f"Invalid model format: '{model}'. Both provider and model name are required "
47
+ "(example: 'anthropic:claude-haiku-4-5')."
48
+ )
49
+ logger.error(message)
50
+ console.print(f"[red]{message}[/red]")
51
+ sys.exit(1)
52
+
53
+ return provider, model_name
54
+
55
+
56
+ def _execute_single_commit_workflow(
57
+ *,
58
+ system_prompt: str,
59
+ user_prompt: str,
60
+ model: str,
61
+ temperature: float,
62
+ max_output_tokens: int,
63
+ max_retries: int,
64
+ require_confirmation: bool,
65
+ quiet: bool,
66
+ no_verify: bool,
67
+ dry_run: bool,
68
+ message_only: bool = False,
69
+ push: bool,
70
+ show_prompt: bool,
71
+ hook_timeout: int = 120,
72
+ interactive: bool = False,
73
+ commit_executor: CommitExecutor,
74
+ interactive_mode: InteractiveMode,
75
+ git_state: GitState,
76
+ hint: str,
77
+ ) -> None:
78
+ """Execute single commit workflow using extracted components."""
79
+ conversation_messages: list[dict[str, str]] = []
80
+ if system_prompt:
81
+ conversation_messages.append({"role": "system", "content": system_prompt})
82
+ conversation_messages.append({"role": "user", "content": user_prompt})
83
+
84
+ # Handle interactive questions if enabled
85
+ if interactive and not message_only:
86
+ interactive_mode.handle_interactive_flow(
87
+ model=model,
88
+ user_prompt=user_prompt,
89
+ git_state=git_state,
90
+ hint=hint,
91
+ conversation_messages=conversation_messages,
92
+ temperature=temperature,
93
+ max_tokens=max_output_tokens,
94
+ max_retries=max_retries,
95
+ quiet=quiet,
96
+ )
97
+
98
+ # Generate commit message
99
+ first_iteration = True
100
+ while True:
101
+ prompt_tokens = count_tokens(conversation_messages, model)
102
+ if first_iteration:
103
+ warning_limit_val = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
104
+ if warning_limit_val is None:
105
+ raise ConfigError("warning_limit_tokens configuration missing")
106
+ warning_limit = int(warning_limit_val)
107
+ from gac.workflow_utils import check_token_warning
108
+
109
+ if not check_token_warning(prompt_tokens, warning_limit, require_confirmation):
110
+ sys.exit(0)
111
+ first_iteration = False
112
+
113
+ raw_commit_message = generate_commit_message(
114
+ model=model,
115
+ prompt=conversation_messages,
116
+ temperature=temperature,
117
+ max_tokens=max_output_tokens,
118
+ max_retries=max_retries,
119
+ quiet=quiet or message_only,
120
+ )
121
+ commit_message = clean_commit_message(raw_commit_message)
122
+ logger.info("Generated commit message:")
123
+ logger.info(commit_message)
124
+ conversation_messages.append({"role": "assistant", "content": commit_message})
125
+
126
+ if message_only:
127
+ # Output only the commit message without any formatting
128
+ print(commit_message)
129
+ sys.exit(0)
130
+
131
+ # Display commit message panel (always show, regardless of confirmation mode)
132
+ if not quiet:
133
+ from gac.workflow_utils import display_commit_message
134
+
135
+ display_commit_message(commit_message, prompt_tokens, model, quiet)
136
+
137
+ # Handle confirmation
138
+ if require_confirmation:
139
+ final_message, decision = interactive_mode.handle_single_commit_confirmation(
140
+ model=model,
141
+ commit_message=commit_message,
142
+ conversation_messages=conversation_messages,
143
+ quiet=quiet,
144
+ )
145
+ if decision == "yes":
146
+ commit_message = final_message
147
+ break
148
+ elif decision == "no":
149
+ console.print("[yellow]Commit aborted.[/yellow]")
150
+ sys.exit(0)
151
+ # decision == "regenerate": continue the loop
152
+ else:
153
+ break
154
+
155
+ # Execute the commit
156
+ commit_executor.create_commit(commit_message)
157
+
158
+ # Push if requested
159
+ if push:
160
+ commit_executor.push_to_remote()
161
+
162
+ if not quiet:
163
+ logger.info("Successfully committed changes with message:")
164
+ logger.info(commit_message)
165
+ if push:
166
+ logger.info("Changes pushed to remote.")
167
+ sys.exit(0)
168
+
169
+
170
+ def _handle_oauth_retry(
171
+ e: AIError,
172
+ prompts: Any,
173
+ model: str,
174
+ temperature: float,
175
+ max_output_tokens: int,
176
+ max_retries: int,
177
+ require_confirmation: bool,
178
+ quiet: bool,
179
+ no_verify: bool,
180
+ dry_run: bool,
181
+ message_only: bool,
182
+ push: bool,
183
+ show_prompt: bool,
184
+ hook_timeout: int,
185
+ interactive: bool,
186
+ commit_executor: CommitExecutor,
187
+ interactive_mode: InteractiveMode,
188
+ git_state: GitState,
189
+ hint: str,
190
+ ) -> None:
191
+ """Handle OAuth retry logic for expired tokens."""
192
+ logger.error(str(e))
193
+
194
+ # Check if this is a Claude Code OAuth token expiration
195
+ if (
196
+ e.error_type == "authentication"
197
+ and model.startswith("claude-code:")
198
+ and ("expired" in str(e).lower() or "oauth" in str(e).lower())
199
+ ):
200
+ console.print("[yellow]⚠ Claude Code OAuth token has expired[/yellow]")
201
+ console.print("[cyan]🔐 Starting automatic re-authentication...[/cyan]")
202
+
203
+ try:
204
+ from gac.oauth.claude_code import authenticate_and_save
205
+
206
+ if authenticate_and_save(quiet=quiet):
207
+ console.print("[green]✓ Re-authentication successful![/green]")
208
+ console.print("[cyan]Retrying commit...[/cyan]\n")
209
+
210
+ # Retry the commit workflow
211
+ _execute_single_commit_workflow(
212
+ system_prompt=prompts.system_prompt,
213
+ user_prompt=prompts.user_prompt,
214
+ model=model,
215
+ temperature=temperature,
216
+ max_output_tokens=max_output_tokens,
217
+ max_retries=max_retries,
218
+ require_confirmation=require_confirmation,
219
+ quiet=quiet,
220
+ no_verify=no_verify,
221
+ dry_run=dry_run,
222
+ message_only=message_only,
223
+ push=push,
224
+ show_prompt=show_prompt,
225
+ hook_timeout=hook_timeout,
226
+ interactive=interactive,
227
+ commit_executor=commit_executor,
228
+ interactive_mode=interactive_mode,
229
+ git_state=git_state,
230
+ hint=hint,
231
+ )
232
+ else:
233
+ console.print("[red]Re-authentication failed.[/red]")
234
+ console.print("[yellow]Run 'gac model' to re-authenticate manually.[/yellow]")
235
+ sys.exit(1)
236
+ except (AIError, ConfigError, OSError) as auth_error:
237
+ console.print(f"[red]Re-authentication error: {auth_error}[/red]")
238
+ console.print("[yellow]Run 'gac model' to re-authenticate manually.[/yellow]")
239
+ sys.exit(1)
240
+ # Check if this is a Qwen OAuth token expiration
241
+ elif e.error_type == "authentication" and model.startswith("qwen:"):
242
+ console.print("[yellow]⚠ Qwen authentication failed[/yellow]")
243
+ console.print("[cyan]🔐 Starting automatic re-authentication...[/cyan]")
244
+
245
+ try:
246
+ from gac.oauth import QwenOAuthProvider, TokenStore
247
+
248
+ oauth_provider = QwenOAuthProvider(TokenStore())
249
+ oauth_provider.initiate_auth(open_browser=True)
250
+ console.print("[green]✓ Re-authentication successful![/green]")
251
+ console.print("[cyan]Retrying commit...[/cyan]\n")
252
+
253
+ # Retry the commit workflow
254
+ _execute_single_commit_workflow(
255
+ system_prompt=prompts.system_prompt,
256
+ user_prompt=prompts.user_prompt,
257
+ model=model,
258
+ temperature=temperature,
259
+ max_output_tokens=max_output_tokens,
260
+ max_retries=max_retries,
261
+ require_confirmation=require_confirmation,
262
+ quiet=quiet,
263
+ no_verify=no_verify,
264
+ dry_run=dry_run,
265
+ message_only=message_only,
266
+ push=push,
267
+ show_prompt=show_prompt,
268
+ hook_timeout=hook_timeout,
269
+ interactive=interactive,
270
+ commit_executor=commit_executor,
271
+ interactive_mode=interactive_mode,
272
+ git_state=git_state,
273
+ hint=hint,
274
+ )
275
+ except (AIError, ConfigError, OSError) as auth_error:
276
+ console.print(f"[red]Re-authentication error: {auth_error}[/red]")
277
+ console.print("[yellow]Run 'gac auth qwen login' to re-authenticate manually.[/yellow]")
278
+ sys.exit(1)
279
+ else:
280
+ # Non-Claude Code/Qwen error or non-auth error
281
+ console.print(f"[red]Failed to generate commit message: {str(e)}[/red]")
282
+ sys.exit(1)
283
+
284
+
285
+ def main(
286
+ stage_all: bool = False,
287
+ group: bool = False,
288
+ interactive: bool = False,
289
+ model: str | None = None,
290
+ hint: str = "",
291
+ one_liner: bool = False,
292
+ show_prompt: bool = False,
293
+ infer_scope: bool = False,
294
+ require_confirmation: bool = True,
295
+ push: bool = False,
296
+ quiet: bool = False,
297
+ dry_run: bool = False,
298
+ message_only: bool = False,
299
+ verbose: bool = False,
300
+ no_verify: bool = False,
301
+ skip_secret_scan: bool = False,
302
+ language: str | None = None,
303
+ hook_timeout: int = 120,
304
+ ) -> None:
305
+ """Main application logic for gac."""
306
+ # Initialize components
307
+ git_validator = GitStateValidator(config)
308
+ prompt_builder = PromptBuilder(config)
309
+ commit_executor = CommitExecutor(dry_run=dry_run, quiet=quiet, no_verify=no_verify, hook_timeout=hook_timeout)
310
+ interactive_mode = InteractiveMode(config)
311
+ grouped_workflow = GroupedCommitWorkflow(config)
312
+
313
+ # Validate and get model configuration
314
+ if model is None:
315
+ model_from_config = config["model"]
316
+ if model_from_config is None:
317
+ handle_error(
318
+ AIError.model_error(
319
+ "gac init hasn't been run yet. Please run 'gac init' to set up your configuration, then try again."
320
+ ),
321
+ exit_program=True,
322
+ )
323
+ model = str(model_from_config)
324
+
325
+ temperature_val = config["temperature"]
326
+ if temperature_val is None:
327
+ raise ConfigError("temperature configuration missing")
328
+ temperature = float(temperature_val)
329
+
330
+ max_tokens_val = config["max_output_tokens"]
331
+ if max_tokens_val is None:
332
+ raise ConfigError("max_output_tokens configuration missing")
333
+ max_output_tokens = int(max_tokens_val)
334
+
335
+ max_retries_val = config["max_retries"]
336
+ if max_retries_val is None:
337
+ raise ConfigError("max_retries configuration missing")
338
+ max_retries = int(max_retries_val)
339
+
340
+ # Get git state and handle hooks
341
+ git_state = git_validator.get_git_state(
342
+ stage_all=stage_all,
343
+ dry_run=dry_run,
344
+ skip_secret_scan=skip_secret_scan,
345
+ quiet=quiet,
346
+ model=model,
347
+ hint=hint,
348
+ one_liner=one_liner,
349
+ infer_scope=infer_scope,
350
+ verbose=verbose,
351
+ language=language,
352
+ )
353
+
354
+ # Run pre-commit hooks
355
+ if not no_verify and not dry_run:
356
+ if not run_lefthook_hooks(hook_timeout):
357
+ console.print("[red]Lefthook hooks failed. Please fix the issues and try again.[/red]")
358
+ console.print("[yellow]You can use --no-verify to skip pre-commit and lefthook hooks.[/yellow]")
359
+ sys.exit(1)
360
+
361
+ if not run_pre_commit_hooks(hook_timeout):
362
+ console.print("[red]Pre-commit hooks failed. Please fix the issues and try again.[/red]")
363
+ console.print("[yellow]You can use --no-verify to skip pre-commit and lefthook hooks.[/yellow]")
364
+ sys.exit(1)
365
+
366
+ # Handle secret detection
367
+ if git_state.has_secrets:
368
+ should_continue = git_validator.handle_secret_detection(git_state.secrets, quiet)
369
+ if not should_continue:
370
+ # If secrets were removed, we need to refresh the git state
371
+ git_state = git_validator.get_git_state(
372
+ stage_all=False,
373
+ dry_run=dry_run,
374
+ skip_secret_scan=True, # Skip secret scan this time
375
+ quiet=quiet,
376
+ model=model,
377
+ hint=hint,
378
+ one_liner=one_liner,
379
+ infer_scope=infer_scope,
380
+ verbose=verbose,
381
+ language=language,
382
+ )
383
+
384
+ # Adjust max_output_tokens for grouped mode
385
+ if group:
386
+ num_files = len(git_state.staged_files)
387
+ multiplier = min(5, 2 + (num_files // 10))
388
+ max_output_tokens *= multiplier
389
+ logger.debug(f"Grouped mode: scaling max_output_tokens by {multiplier}x for {num_files} files")
390
+
391
+ # Build prompts
392
+ prompts = prompt_builder.build_prompts(
393
+ git_state=git_state,
394
+ group=group,
395
+ one_liner=one_liner,
396
+ hint=hint,
397
+ infer_scope=infer_scope,
398
+ verbose=verbose,
399
+ language=language,
400
+ )
401
+
402
+ # Display prompts if requested
403
+ if show_prompt:
404
+ prompt_builder.display_prompts(prompts.system_prompt, prompts.user_prompt)
405
+
406
+ try:
407
+ if group:
408
+ # Execute grouped workflow
409
+ grouped_workflow.execute_workflow(
410
+ system_prompt=prompts.system_prompt,
411
+ user_prompt=prompts.user_prompt,
412
+ model=model,
413
+ temperature=temperature,
414
+ max_output_tokens=max_output_tokens,
415
+ max_retries=max_retries,
416
+ require_confirmation=require_confirmation,
417
+ quiet=quiet,
418
+ no_verify=no_verify,
419
+ dry_run=dry_run,
420
+ push=push,
421
+ show_prompt=show_prompt,
422
+ interactive=interactive,
423
+ message_only=message_only,
424
+ hook_timeout=hook_timeout,
425
+ git_state=git_state,
426
+ hint=hint,
427
+ )
428
+ else:
429
+ # Execute single commit workflow
430
+ _execute_single_commit_workflow(
431
+ system_prompt=prompts.system_prompt,
432
+ user_prompt=prompts.user_prompt,
433
+ model=model,
434
+ temperature=temperature,
435
+ max_output_tokens=max_output_tokens,
436
+ max_retries=max_retries,
437
+ require_confirmation=require_confirmation,
438
+ quiet=quiet,
439
+ no_verify=no_verify,
440
+ dry_run=dry_run,
441
+ message_only=message_only,
442
+ push=push,
443
+ show_prompt=show_prompt,
444
+ hook_timeout=hook_timeout,
445
+ interactive=interactive,
446
+ commit_executor=commit_executor,
447
+ interactive_mode=interactive_mode,
448
+ git_state=git_state,
449
+ hint=hint,
450
+ )
451
+ except AIError as e:
452
+ _handle_oauth_retry(
453
+ e,
454
+ prompts,
455
+ model,
456
+ temperature,
457
+ max_output_tokens,
458
+ max_retries,
459
+ require_confirmation,
460
+ quiet,
461
+ no_verify,
462
+ dry_run,
463
+ message_only,
464
+ push,
465
+ show_prompt,
466
+ hook_timeout,
467
+ interactive,
468
+ commit_executor,
469
+ interactive_mode,
470
+ git_state,
471
+ hint,
472
+ )
473
+
474
+
475
+ if __name__ == "__main__":
476
+ main()