gac 3.8.1__py3-none-any.whl → 3.10.10__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.
Files changed (76) hide show
  1. gac/__init__.py +4 -6
  2. gac/__version__.py +1 -1
  3. gac/ai_utils.py +18 -49
  4. gac/cli.py +14 -10
  5. gac/commit_executor.py +59 -0
  6. gac/config.py +28 -3
  7. gac/config_cli.py +19 -7
  8. gac/constants/__init__.py +34 -0
  9. gac/constants/commit.py +63 -0
  10. gac/constants/defaults.py +40 -0
  11. gac/constants/file_patterns.py +110 -0
  12. gac/constants/languages.py +119 -0
  13. gac/diff_cli.py +0 -22
  14. gac/errors.py +8 -2
  15. gac/git.py +6 -6
  16. gac/git_state_validator.py +193 -0
  17. gac/grouped_commit_workflow.py +458 -0
  18. gac/init_cli.py +2 -1
  19. gac/interactive_mode.py +179 -0
  20. gac/language_cli.py +0 -1
  21. gac/main.py +222 -959
  22. gac/model_cli.py +2 -1
  23. gac/model_identifier.py +70 -0
  24. gac/oauth/claude_code.py +2 -2
  25. gac/oauth/qwen_oauth.py +4 -0
  26. gac/oauth/token_store.py +2 -2
  27. gac/oauth_retry.py +161 -0
  28. gac/postprocess.py +155 -0
  29. gac/prompt.py +20 -490
  30. gac/prompt_builder.py +88 -0
  31. gac/providers/README.md +437 -0
  32. gac/providers/__init__.py +70 -81
  33. gac/providers/anthropic.py +12 -56
  34. gac/providers/azure_openai.py +48 -92
  35. gac/providers/base.py +329 -0
  36. gac/providers/cerebras.py +10 -43
  37. gac/providers/chutes.py +16 -72
  38. gac/providers/claude_code.py +64 -97
  39. gac/providers/custom_anthropic.py +51 -85
  40. gac/providers/custom_openai.py +29 -87
  41. gac/providers/deepseek.py +10 -43
  42. gac/providers/error_handler.py +139 -0
  43. gac/providers/fireworks.py +10 -43
  44. gac/providers/gemini.py +66 -73
  45. gac/providers/groq.py +10 -62
  46. gac/providers/kimi_coding.py +19 -59
  47. gac/providers/lmstudio.py +62 -52
  48. gac/providers/minimax.py +10 -43
  49. gac/providers/mistral.py +10 -43
  50. gac/providers/moonshot.py +10 -43
  51. gac/providers/ollama.py +54 -41
  52. gac/providers/openai.py +30 -46
  53. gac/providers/openrouter.py +15 -62
  54. gac/providers/protocol.py +71 -0
  55. gac/providers/qwen.py +55 -67
  56. gac/providers/registry.py +58 -0
  57. gac/providers/replicate.py +137 -91
  58. gac/providers/streamlake.py +26 -56
  59. gac/providers/synthetic.py +35 -47
  60. gac/providers/together.py +10 -43
  61. gac/providers/zai.py +21 -59
  62. gac/py.typed +0 -0
  63. gac/security.py +1 -1
  64. gac/templates/__init__.py +1 -0
  65. gac/templates/question_generation.txt +60 -0
  66. gac/templates/system_prompt.txt +224 -0
  67. gac/templates/user_prompt.txt +28 -0
  68. gac/utils.py +6 -5
  69. gac/workflow_context.py +162 -0
  70. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/METADATA +1 -1
  71. gac-3.10.10.dist-info/RECORD +79 -0
  72. gac/constants.py +0 -328
  73. gac-3.8.1.dist-info/RECORD +0 -56
  74. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
  75. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
  76. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
gac/__init__.py CHANGED
@@ -1,15 +1,13 @@
1
1
  """Git Auto Commit (gac) - Generate commit messages using AI."""
2
2
 
3
+ from gac import init_cli
3
4
  from gac.__version__ import __version__
4
5
  from gac.ai import generate_commit_message
5
- from gac.git import get_staged_files, push_changes
6
- from gac.prompt import build_prompt, clean_commit_message
6
+ from gac.prompt import build_prompt
7
7
 
8
8
  __all__ = [
9
9
  "__version__",
10
- "generate_commit_message",
11
10
  "build_prompt",
12
- "clean_commit_message",
13
- "get_staged_files",
14
- "push_changes",
11
+ "generate_commit_message",
12
+ "init_cli",
15
13
  ]
gac/__version__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information for gac package."""
2
2
 
3
- __version__ = "3.8.1"
3
+ __version__ = "3.10.10"
gac/ai_utils.py CHANGED
@@ -3,11 +3,13 @@
3
3
  This module provides utility functions that support the AI provider implementations.
4
4
  """
5
5
 
6
+ import json
6
7
  import logging
7
8
  import os
8
9
  import time
10
+ from collections.abc import Callable
9
11
  from functools import lru_cache
10
- from typing import Any
12
+ from typing import Any, cast
11
13
 
12
14
  import tiktoken
13
15
  from rich.console import Console
@@ -15,6 +17,8 @@ from rich.status import Status
15
17
 
16
18
  from gac.constants import EnvDefaults, Utility
17
19
  from gac.errors import AIError
20
+ from gac.oauth import QwenOAuthProvider, refresh_token_if_expired
21
+ from gac.oauth.token_store import TokenStore
18
22
  from gac.providers import SUPPORTED_PROVIDERS
19
23
 
20
24
  logger = logging.getLogger(__name__)
@@ -40,7 +44,7 @@ def count_tokens(content: str | list[dict[str, str]] | dict[str, Any], model: st
40
44
  try:
41
45
  encoding = get_encoding(model)
42
46
  return len(encoding.encode(text))
43
- except Exception as e:
47
+ except (KeyError, UnicodeError, ValueError) as e:
44
48
  logger.error(f"Error counting tokens: {e}")
45
49
  # Fallback to rough estimation (4 chars per token on average)
46
50
  return len(text) // 4
@@ -53,7 +57,7 @@ def extract_text_content(content: str | list[dict[str, str]] | dict[str, Any]) -
53
57
  elif isinstance(content, list):
54
58
  return "\n".join(msg["content"] for msg in content if isinstance(msg, dict) and "content" in msg)
55
59
  elif isinstance(content, dict) and "content" in content:
56
- return content["content"] # type: ignore[no-any-return]
60
+ return cast(str, content["content"])
57
61
  return ""
58
62
 
59
63
 
@@ -70,36 +74,13 @@ def get_encoding(model: str) -> tiktoken.Encoding:
70
74
  except KeyError:
71
75
  # Fall back to default encoding if model not found
72
76
  return tiktoken.get_encoding(Utility.DEFAULT_ENCODING)
73
- except Exception:
77
+ except (OSError, ConnectionError):
74
78
  # If there are any network/SSL issues, fall back to default encoding
75
79
  return tiktoken.get_encoding(Utility.DEFAULT_ENCODING)
76
80
 
77
81
 
78
- def _classify_error(error_str: str) -> str:
79
- """Classify error types based on error message content."""
80
- error_str = error_str.lower()
81
-
82
- if (
83
- "api key" in error_str
84
- or "unauthorized" in error_str
85
- or "authentication" in error_str
86
- or "invalid api key" in error_str
87
- ):
88
- return "authentication"
89
- elif "timeout" in error_str or "timed out" in error_str or "request timeout" in error_str:
90
- return "timeout"
91
- elif "rate limit" in error_str or "too many requests" in error_str or "rate limit exceeded" in error_str:
92
- return "rate_limit"
93
- elif "connect" in error_str or "network" in error_str or "network connection failed" in error_str:
94
- return "connection"
95
- elif "model" in error_str or "not found" in error_str or "model not found" in error_str:
96
- return "model"
97
- else:
98
- return "unknown"
99
-
100
-
101
82
  def generate_with_retries(
102
- provider_funcs: dict,
83
+ provider_funcs: dict[str, Callable[..., str]],
103
84
  model: str,
104
85
  messages: list[dict[str, str]],
105
86
  temperature: float,
@@ -126,9 +107,6 @@ def generate_with_retries(
126
107
 
127
108
  # Load Claude Code token from TokenStore if needed
128
109
  if provider == "claude-code":
129
- from gac.oauth import refresh_token_if_expired
130
- from gac.oauth.token_store import TokenStore
131
-
132
110
  # Check token expiry and refresh if needed
133
111
  if not refresh_token_if_expired(quiet=True):
134
112
  raise AIError.authentication_error(
@@ -147,8 +125,6 @@ def generate_with_retries(
147
125
 
148
126
  # Check Qwen OAuth token expiry and refresh if needed
149
127
  if provider == "qwen":
150
- from gac.oauth import QwenOAuthProvider, TokenStore
151
-
152
128
  oauth_provider = QwenOAuthProvider(TokenStore())
153
129
  token = oauth_provider.get_token()
154
130
  if not token:
@@ -166,7 +142,7 @@ def generate_with_retries(
166
142
  console.print("[green]✓ Authentication successful![/green]\n")
167
143
  except AIError:
168
144
  raise
169
- except Exception as e:
145
+ except (ValueError, KeyError, json.JSONDecodeError, ConnectionError, OSError) as e:
170
146
  raise AIError.authentication_error(
171
147
  f"Qwen authentication failed: {e}. Run 'gac auth qwen login' to authenticate manually."
172
148
  ) from e
@@ -183,7 +159,7 @@ def generate_with_retries(
183
159
  spinner = Status(f"Generating {message_type} with {provider} {model_name}...")
184
160
  spinner.start()
185
161
 
186
- last_exception = None
162
+ last_exception: Exception | None = None
187
163
  last_error_type = "unknown"
188
164
 
189
165
  for attempt in range(max_retries):
@@ -208,14 +184,14 @@ def generate_with_retries(
208
184
  console.print(f"✓ Generated {message_type} with {provider} {model_name}")
209
185
 
210
186
  if content is not None and content.strip():
211
- return content.strip() # type: ignore[no-any-return]
187
+ return content.strip()
212
188
  else:
213
189
  logger.warning(f"Empty or None content received from {provider} {model_name}: {repr(content)}")
214
190
  raise AIError.model_error("Empty response from AI model")
215
191
 
216
- except Exception as e:
192
+ except AIError as e:
217
193
  last_exception = e
218
- error_type = _classify_error(str(e))
194
+ error_type = e.error_type
219
195
  last_error_type = error_type
220
196
 
221
197
  # For authentication and model errors, don't retry
@@ -223,23 +199,16 @@ def generate_with_retries(
223
199
  if spinner and not skip_success_message:
224
200
  spinner.stop()
225
201
  console.print(f"✗ Failed to generate {message_type} with {provider} {model_name}")
226
-
227
- # Create the appropriate error type based on classification
228
- if error_type == "authentication":
229
- raise AIError.authentication_error(f"AI generation failed: {str(e)}") from e
230
- elif error_type == "model":
231
- raise AIError.model_error(f"AI generation failed: {str(e)}") from e
202
+ raise
232
203
 
233
204
  if attempt < max_retries - 1:
234
205
  # Exponential backoff
235
206
  wait_time = 2**attempt
236
207
  if not quiet and not skip_success_message:
237
208
  if attempt == 0:
238
- logger.warning(f"AI generation failed, retrying in {wait_time}s: {str(e)}")
209
+ logger.warning(f"AI generation failed, retrying in {wait_time}s: {e}")
239
210
  else:
240
- logger.warning(
241
- f"AI generation failed (attempt {attempt + 1}), retrying in {wait_time}s: {str(e)}"
242
- )
211
+ logger.warning(f"AI generation failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}")
243
212
 
244
213
  if spinner and not skip_success_message:
245
214
  for i in range(wait_time, 0, -1):
@@ -250,7 +219,7 @@ def generate_with_retries(
250
219
  else:
251
220
  num_retries = max_retries
252
221
  retry_word = "retry" if num_retries == 1 else "retries"
253
- logger.error(f"AI generation failed after {num_retries} {retry_word}: {str(e)}")
222
+ logger.error(f"AI generation failed after {num_retries} {retry_word}: {e}")
254
223
 
255
224
  if spinner and not skip_success_message:
256
225
  spinner.stop()
gac/cli.py CHANGED
@@ -8,13 +8,14 @@ Defines the Click-based command-line interface and delegates execution to the ma
8
8
  import logging
9
9
  import os
10
10
  import sys
11
+ from typing import Any
11
12
 
12
13
  import click
13
14
  from rich.console import Console
14
15
 
15
16
  from gac import __version__
16
17
  from gac.auth_cli import auth as auth_cli
17
- from gac.config import load_config
18
+ from gac.config import GACConfig, load_config
18
19
  from gac.config_cli import config as config_cli
19
20
  from gac.constants import Languages, Logging
20
21
  from gac.diff_cli import diff as diff_cli
@@ -24,8 +25,9 @@ from gac.language_cli import language as language_cli
24
25
  from gac.main import main
25
26
  from gac.model_cli import model as model_cli
26
27
  from gac.utils import setup_logging
28
+ from gac.workflow_context import CLIOptions
27
29
 
28
- config = load_config()
30
+ config: GACConfig = load_config()
29
31
  logger = logging.getLogger(__name__)
30
32
  console = Console()
31
33
 
@@ -124,7 +126,7 @@ def cli(
124
126
  logger.info("Starting gac")
125
127
 
126
128
  # Set SSL verification environment variable if flag is used or config is set
127
- if no_verify_ssl or config.get("no_verify_ssl", False):
129
+ if no_verify_ssl or config["no_verify_ssl"]:
128
130
  os.environ["GAC_NO_VERIFY_SSL"] = "true"
129
131
  logger.info("SSL certificate verification disabled")
130
132
 
@@ -136,16 +138,16 @@ def cli(
136
138
  sys.exit(1)
137
139
 
138
140
  # Determine if we should infer scope based on -s flag or always_include_scope setting
139
- infer_scope = bool(scope or config.get("always_include_scope", False))
141
+ infer_scope = bool(scope or config["always_include_scope"])
140
142
 
141
143
  # Determine if verbose mode should be enabled based on -v flag or verbose config setting
142
- use_verbose = bool(verbose or config.get("verbose", False))
144
+ use_verbose = bool(verbose or config["verbose"])
143
145
 
144
146
  # Resolve language code to full name if provided
145
147
  resolved_language = Languages.resolve_code(language) if language else None
146
148
 
147
149
  try:
148
- main(
150
+ opts = CLIOptions(
149
151
  stage_all=add_all,
150
152
  group=group,
151
153
  interactive=interactive,
@@ -161,15 +163,17 @@ def cli(
161
163
  message_only=message_only,
162
164
  verbose=use_verbose,
163
165
  no_verify=no_verify,
164
- skip_secret_scan=skip_secret_scan or bool(config.get("skip_secret_scan", False)),
166
+ skip_secret_scan=skip_secret_scan or config["skip_secret_scan"],
165
167
  language=resolved_language,
166
- hook_timeout=hook_timeout if hook_timeout > 0 else int(config.get("hook_timeout", 120) or 120),
168
+ hook_timeout=hook_timeout if hook_timeout > 0 else config["hook_timeout"],
167
169
  )
170
+ exit_code = main(opts)
171
+ sys.exit(exit_code)
168
172
  except Exception as e:
169
173
  handle_error(e, exit_program=True)
170
174
  else:
171
175
  # Determine if we should infer scope based on -s flag or always_include_scope setting
172
- infer_scope = bool(scope or config.get("always_include_scope", False))
176
+ infer_scope = bool(scope or config["always_include_scope"])
173
177
 
174
178
  ctx.obj = {
175
179
  "add_all": add_all,
@@ -206,7 +210,7 @@ cli.add_command(model_cli)
206
210
 
207
211
  @click.command(context_settings=language_cli.context_settings)
208
212
  @click.pass_context
209
- def lang(ctx):
213
+ def lang(ctx: Any) -> None:
210
214
  """Set the language for commit messages interactively. (Alias for 'language')"""
211
215
  ctx.forward(language_cli)
212
216
 
gac/commit_executor.py ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """Commit execution logic for gac."""
3
+
4
+ import logging
5
+
6
+ from rich.console import Console
7
+
8
+ from gac.errors import GitError
9
+ from gac.git import get_staged_files, push_changes
10
+ from gac.workflow_utils import execute_commit
11
+
12
+ logger = logging.getLogger(__name__)
13
+ console = Console()
14
+
15
+
16
+ class CommitExecutor:
17
+ """Handles commit creation and related operations."""
18
+
19
+ def __init__(self, dry_run: bool = False, quiet: bool = False, no_verify: bool = False, hook_timeout: int = 120):
20
+ self.dry_run = dry_run
21
+ self.quiet = quiet
22
+ self.no_verify = no_verify
23
+ self.hook_timeout = hook_timeout
24
+
25
+ def create_commit(self, commit_message: str) -> None:
26
+ """Create a single commit with the given message."""
27
+ if self.dry_run:
28
+ console.print("[yellow]Dry run: Commit message generated but not applied[/yellow]")
29
+ console.print("Would commit with message:")
30
+ from rich.panel import Panel
31
+
32
+ console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
33
+ staged_files = get_staged_files(existing_only=False)
34
+ console.print(f"Would commit {len(staged_files)} files")
35
+ logger.info(f"Would commit {len(staged_files)} files")
36
+ else:
37
+ execute_commit(commit_message, self.no_verify, self.hook_timeout)
38
+
39
+ def push_to_remote(self) -> None:
40
+ """Push changes to remote repository.
41
+
42
+ Raises:
43
+ GitError: If push fails or remote is not configured.
44
+ """
45
+ if self.dry_run:
46
+ staged_files = get_staged_files(existing_only=False)
47
+ logger.info("Dry run: Would push changes")
48
+ logger.info(f"Would push {len(staged_files)} files")
49
+ console.print("[yellow]Dry run: Would push changes[/yellow]")
50
+ console.print(f"Would push {len(staged_files)} files")
51
+ return
52
+
53
+ if push_changes():
54
+ logger.info("Changes pushed successfully")
55
+ if not self.quiet:
56
+ console.print("[green]Changes pushed successfully[/green]")
57
+ else:
58
+ console.print("[red]Failed to push changes. Check your remote configuration and network connection.[/red]")
59
+ raise GitError("Failed to push changes")
gac/config.py CHANGED
@@ -5,6 +5,7 @@ Handles environment variable and .gac.env file precedence for application settin
5
5
 
6
6
  import os
7
7
  from pathlib import Path
8
+ from typing import TypedDict
8
9
 
9
10
  from dotenv import load_dotenv
10
11
 
@@ -12,7 +13,31 @@ from gac.constants import EnvDefaults, Logging
12
13
  from gac.errors import ConfigError
13
14
 
14
15
 
15
- def validate_config(config: dict[str, str | int | float | bool | None]) -> None:
16
+ class GACConfig(TypedDict, total=False):
17
+ """TypedDict for GAC configuration values.
18
+
19
+ Fields that can be None or omitted are marked with total=False.
20
+ """
21
+
22
+ model: str | None
23
+ temperature: float
24
+ max_output_tokens: int
25
+ max_retries: int
26
+ log_level: str
27
+ warning_limit_tokens: int
28
+ always_include_scope: bool
29
+ skip_secret_scan: bool
30
+ no_tiktoken: bool
31
+ no_verify_ssl: bool
32
+ verbose: bool
33
+ system_prompt_path: str | None
34
+ language: str | None
35
+ translate_prefixes: bool
36
+ rtl_confirmed: bool
37
+ hook_timeout: int
38
+
39
+
40
+ def validate_config(config: GACConfig) -> None:
16
41
  """Validate configuration values at load time.
17
42
 
18
43
  Args:
@@ -62,7 +87,7 @@ def validate_config(config: dict[str, str | int | float | bool | None]) -> None:
62
87
  raise ConfigError(f"hook_timeout must be positive, got {hook_timeout}")
63
88
 
64
89
 
65
- def load_config() -> dict[str, str | int | float | bool | None]:
90
+ def load_config() -> GACConfig:
66
91
  """Load configuration from $HOME/.gac.env, then ./.gac.env, then environment variables."""
67
92
  user_config = Path.home() / ".gac.env"
68
93
  if user_config.exists():
@@ -74,7 +99,7 @@ def load_config() -> dict[str, str | int | float | bool | None]:
74
99
  if project_gac_env.exists():
75
100
  load_dotenv(project_gac_env, override=True)
76
101
 
77
- config = {
102
+ config: GACConfig = {
78
103
  "model": os.getenv("GAC_MODEL"),
79
104
  "temperature": float(os.getenv("GAC_TEMPERATURE", EnvDefaults.TEMPERATURE)),
80
105
  "max_output_tokens": int(os.getenv("GAC_MAX_OUTPUT_TOKENS", EnvDefaults.MAX_OUTPUT_TOKENS)),
gac/config_cli.py CHANGED
@@ -10,7 +10,7 @@ GAC_ENV_PATH = Path.home() / ".gac.env"
10
10
 
11
11
 
12
12
  @click.group()
13
- def config():
13
+ def config() -> None:
14
14
  """Manage gac configuration."""
15
15
  pass
16
16
 
@@ -18,6 +18,8 @@ def config():
18
18
  @config.command()
19
19
  def show() -> None:
20
20
  """Show all current config values."""
21
+ from dotenv import dotenv_values
22
+
21
23
  project_env_path = Path(".gac.env")
22
24
  user_exists = GAC_ENV_PATH.exists()
23
25
  project_exists = project_env_path.exists()
@@ -29,9 +31,14 @@ def show() -> None:
29
31
 
30
32
  if user_exists:
31
33
  click.echo(f"User config ({GAC_ENV_PATH}):")
32
- with open(GAC_ENV_PATH, encoding="utf-8") as f:
33
- for line in f:
34
- click.echo(line.rstrip())
34
+ user_config = dotenv_values(str(GAC_ENV_PATH))
35
+ for key, value in sorted(user_config.items()):
36
+ if value is not None:
37
+ if any(sensitive in key.lower() for sensitive in ["key", "token", "secret"]):
38
+ display_value = "***hidden***"
39
+ else:
40
+ display_value = value
41
+ click.echo(f" {key}={display_value}")
35
42
  else:
36
43
  click.echo("No $HOME/.gac.env found.")
37
44
 
@@ -39,9 +46,14 @@ def show() -> None:
39
46
  if user_exists:
40
47
  click.echo("")
41
48
  click.echo("Project config (./.gac.env):")
42
- with open(project_env_path, encoding="utf-8") as f:
43
- for line in f:
44
- click.echo(line.rstrip())
49
+ project_config = dotenv_values(str(project_env_path))
50
+ for key, value in sorted(project_config.items()):
51
+ if value is not None:
52
+ if any(sensitive in key.lower() for sensitive in ["key", "token", "secret"]):
53
+ display_value = "***hidden***"
54
+ else:
55
+ display_value = value
56
+ click.echo(f" {key}={display_value}")
45
57
  click.echo("")
46
58
  click.echo("Note: Project-level .gac.env overrides $HOME/.gac.env values for any duplicated variables.")
47
59
  else:
@@ -0,0 +1,34 @@
1
+ """Constants for the Git Auto Commit (gac) project.
2
+
3
+ This package provides all constants used throughout gac, organized into
4
+ logical modules:
5
+
6
+ - defaults: Environment defaults, provider defaults, logging, and utility constants
7
+ - file_patterns: File pattern matching and importance weighting
8
+ - languages: Language code mappings for internationalization
9
+ - commit: Git file status and commit message constants
10
+
11
+ All constants are re-exported from this package for backward compatibility.
12
+ """
13
+
14
+ from gac.constants.commit import CommitMessageConstants, FileStatus
15
+ from gac.constants.defaults import EnvDefaults, Logging, ProviderDefaults, Utility
16
+ from gac.constants.file_patterns import CodePatternImportance, FilePatterns, FileTypeImportance
17
+ from gac.constants.languages import Languages
18
+
19
+ __all__ = [
20
+ # From defaults
21
+ "EnvDefaults",
22
+ "ProviderDefaults",
23
+ "Logging",
24
+ "Utility",
25
+ # From file_patterns
26
+ "FilePatterns",
27
+ "FileTypeImportance",
28
+ "CodePatternImportance",
29
+ # From languages
30
+ "Languages",
31
+ # From commit
32
+ "FileStatus",
33
+ "CommitMessageConstants",
34
+ ]
@@ -0,0 +1,63 @@
1
+ """Constants for git operations and commit message generation."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class FileStatus(Enum):
7
+ """File status for Git operations."""
8
+
9
+ MODIFIED = "M"
10
+ ADDED = "A"
11
+ DELETED = "D"
12
+ RENAMED = "R"
13
+ COPIED = "C"
14
+ UNTRACKED = "?"
15
+
16
+
17
+ class CommitMessageConstants:
18
+ """Constants for commit message generation and cleaning."""
19
+
20
+ # Conventional commit type prefixes
21
+ CONVENTIONAL_PREFIXES: list[str] = [
22
+ "feat",
23
+ "fix",
24
+ "docs",
25
+ "style",
26
+ "refactor",
27
+ "perf",
28
+ "test",
29
+ "build",
30
+ "ci",
31
+ "chore",
32
+ ]
33
+
34
+ # XML tags that may leak from prompt templates into AI responses
35
+ XML_TAGS_TO_REMOVE: list[str] = [
36
+ "<git-status>",
37
+ "</git-status>",
38
+ "<git_status>",
39
+ "</git_status>",
40
+ "<git-diff>",
41
+ "</git-diff>",
42
+ "<git_diff>",
43
+ "</git_diff>",
44
+ "<repository_context>",
45
+ "</repository_context>",
46
+ "<instructions>",
47
+ "</instructions>",
48
+ "<format>",
49
+ "</format>",
50
+ "<conventions>",
51
+ "</conventions>",
52
+ ]
53
+
54
+ # Indicators that mark the start of the actual commit message in AI responses
55
+ COMMIT_INDICATORS: list[str] = [
56
+ "# Your commit message:",
57
+ "Your commit message:",
58
+ "The commit message is:",
59
+ "Here's the commit message:",
60
+ "Commit message:",
61
+ "Final commit message:",
62
+ "# Commit Message",
63
+ ]
@@ -0,0 +1,40 @@
1
+ """Default values for environment variables and provider configurations."""
2
+
3
+ import os
4
+
5
+
6
+ class EnvDefaults:
7
+ """Default values for environment variables."""
8
+
9
+ MAX_RETRIES: int = 3
10
+ TEMPERATURE: float = 1
11
+ MAX_OUTPUT_TOKENS: int = 4096 # includes reasoning tokens
12
+ WARNING_LIMIT_TOKENS: int = 32768
13
+ ALWAYS_INCLUDE_SCOPE: bool = False
14
+ SKIP_SECRET_SCAN: bool = False
15
+ VERBOSE: bool = False
16
+ NO_TIKTOKEN: bool = False
17
+ NO_VERIFY_SSL: bool = False # Skip SSL certificate verification (for corporate proxies)
18
+ HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
19
+
20
+
21
+ class ProviderDefaults:
22
+ """Default values for provider configurations."""
23
+
24
+ HTTP_TIMEOUT: int = 120 # seconds - timeout for HTTP requests to LLM providers
25
+
26
+
27
+ class Logging:
28
+ """Logging configuration constants."""
29
+
30
+ DEFAULT_LEVEL: str = "WARNING"
31
+ LEVELS: list[str] = ["DEBUG", "INFO", "WARNING", "ERROR"]
32
+
33
+
34
+ class Utility:
35
+ """General utility constants."""
36
+
37
+ DEFAULT_ENCODING: str = "cl100k_base" # llm encoding
38
+ DEFAULT_DIFF_TOKEN_LIMIT: int = 15000 # Maximum tokens for diff processing
39
+ MAX_WORKERS: int = os.cpu_count() or 4 # Maximum number of parallel workers
40
+ MAX_DISPLAYED_SECRET_LENGTH: int = 50 # Maximum length for displaying secrets