gac 0.17.2__py3-none-any.whl → 3.6.0__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 (53) hide show
  1. gac/__version__.py +1 -1
  2. gac/ai.py +69 -123
  3. gac/ai_utils.py +227 -0
  4. gac/auth_cli.py +69 -0
  5. gac/cli.py +87 -19
  6. gac/config.py +13 -7
  7. gac/config_cli.py +26 -5
  8. gac/constants.py +176 -5
  9. gac/errors.py +14 -0
  10. gac/git.py +207 -11
  11. gac/init_cli.py +52 -29
  12. gac/language_cli.py +378 -0
  13. gac/main.py +922 -189
  14. gac/model_cli.py +374 -0
  15. gac/oauth/__init__.py +1 -0
  16. gac/oauth/claude_code.py +397 -0
  17. gac/preprocess.py +5 -5
  18. gac/prompt.py +656 -219
  19. gac/providers/__init__.py +88 -0
  20. gac/providers/anthropic.py +51 -0
  21. gac/providers/azure_openai.py +97 -0
  22. gac/providers/cerebras.py +38 -0
  23. gac/providers/chutes.py +71 -0
  24. gac/providers/claude_code.py +102 -0
  25. gac/providers/custom_anthropic.py +133 -0
  26. gac/providers/custom_openai.py +98 -0
  27. gac/providers/deepseek.py +38 -0
  28. gac/providers/fireworks.py +38 -0
  29. gac/providers/gemini.py +87 -0
  30. gac/providers/groq.py +63 -0
  31. gac/providers/kimi_coding.py +63 -0
  32. gac/providers/lmstudio.py +59 -0
  33. gac/providers/minimax.py +38 -0
  34. gac/providers/mistral.py +38 -0
  35. gac/providers/moonshot.py +38 -0
  36. gac/providers/ollama.py +50 -0
  37. gac/providers/openai.py +38 -0
  38. gac/providers/openrouter.py +58 -0
  39. gac/providers/replicate.py +98 -0
  40. gac/providers/streamlake.py +51 -0
  41. gac/providers/synthetic.py +42 -0
  42. gac/providers/together.py +38 -0
  43. gac/providers/zai.py +59 -0
  44. gac/security.py +293 -0
  45. gac/utils.py +243 -4
  46. gac/workflow_utils.py +222 -0
  47. gac-3.6.0.dist-info/METADATA +281 -0
  48. gac-3.6.0.dist-info/RECORD +53 -0
  49. {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/WHEEL +1 -1
  50. {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/licenses/LICENSE +1 -1
  51. gac-0.17.2.dist-info/METADATA +0 -221
  52. gac-0.17.2.dist-info/RECORD +0 -20
  53. {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/entry_points.txt +0 -0
gac/cli.py CHANGED
@@ -9,26 +9,36 @@ import logging
9
9
  import sys
10
10
 
11
11
  import click
12
+ from rich.console import Console
12
13
 
13
14
  from gac import __version__
15
+ from gac.auth_cli import auth as auth_cli
14
16
  from gac.config import load_config
15
17
  from gac.config_cli import config as config_cli
16
- from gac.constants import Logging
18
+ from gac.constants import Languages, Logging
17
19
  from gac.diff_cli import diff as diff_cli
18
20
  from gac.errors import handle_error
19
21
  from gac.init_cli import init as init_cli
22
+ from gac.language_cli import language as language_cli
20
23
  from gac.main import main
24
+ from gac.model_cli import model as model_cli
21
25
  from gac.utils import setup_logging
22
26
 
23
27
  config = load_config()
24
28
  logger = logging.getLogger(__name__)
29
+ console = Console()
25
30
 
26
31
 
27
32
  @click.group(invoke_without_command=True, context_settings={"ignore_unknown_options": True})
28
33
  # Git workflow options
29
34
  @click.option("--add-all", "-a", is_flag=True, help="Stage all changes before committing")
35
+ @click.option("--group", "-g", is_flag=True, help="Group changes into multiple logical commits")
36
+ @click.option(
37
+ "--interactive", "-i", is_flag=True, help="Ask interactive questions to gather more context for the commit message"
38
+ )
30
39
  @click.option("--push", "-p", is_flag=True, help="Push changes to remote after committing")
31
40
  @click.option("--dry-run", is_flag=True, help="Dry run the commit workflow")
41
+ @click.option("--message-only", is_flag=True, help="Output only the generated commit message without committing")
32
42
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
33
43
  # Commit message options
34
44
  @click.option("--one-liner", "-o", is_flag=True, help="Generate a single-line commit message")
@@ -36,17 +46,24 @@ logger = logging.getLogger(__name__)
36
46
  @click.option(
37
47
  "--scope",
38
48
  "-s",
39
- is_flag=False,
40
- flag_value="",
41
- default=None,
42
- help="Add a scope to the commit message. If used without a value, the LLM will determine an appropriate scope.",
49
+ is_flag=True,
50
+ default=False,
51
+ help="Infer an appropriate scope for the commit message",
43
52
  )
44
53
  @click.option("--hint", "-h", default="", help="Additional context to include in the prompt")
45
54
  # Model options
46
55
  @click.option("--model", "-m", help="Override the default model (format: 'provider:model_name')")
56
+ @click.option(
57
+ "--language", "-l", help="Override the language for commit messages (e.g., 'Spanish', 'es', 'zh-CN', 'ja')"
58
+ )
47
59
  # Output options
48
60
  @click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
49
- @click.option("--verbose", "-v", is_flag=True, help="Increase output verbosity to INFO")
61
+ @click.option(
62
+ "--verbose",
63
+ "-v",
64
+ is_flag=True,
65
+ help="Generate detailed commit messages with motivation, architecture, and impact sections",
66
+ )
50
67
  @click.option(
51
68
  "--log-level",
52
69
  default=config["log_level"],
@@ -54,26 +71,39 @@ logger = logging.getLogger(__name__)
54
71
  help=f"Set log level (default: {config['log_level']})",
55
72
  )
56
73
  # Advanced options
57
- @click.option("--no-verify", is_flag=True, help="Skip pre-commit hooks when committing")
74
+ @click.option("--no-verify", is_flag=True, help="Skip pre-commit and lefthook hooks when committing")
75
+ @click.option("--skip-secret-scan", is_flag=True, help="Skip security scan for secrets in staged changes")
76
+ @click.option(
77
+ "--hook-timeout",
78
+ type=int,
79
+ default=0,
80
+ help="Timeout for pre-commit and lefthook hooks in seconds (0 to use configuration)",
81
+ )
58
82
  # Other options
59
83
  @click.option("--version", is_flag=True, help="Show the version of the Git Auto Commit (gac) tool")
60
84
  @click.pass_context
61
85
  def cli(
62
86
  ctx: click.Context,
63
87
  add_all: bool = False,
64
- log_level: str = config["log_level"],
88
+ group: bool = False,
89
+ interactive: bool = False,
90
+ log_level: str = str(config["log_level"]),
65
91
  one_liner: bool = False,
66
92
  push: bool = False,
67
93
  show_prompt: bool = False,
68
- scope: str = None,
94
+ scope: bool = False,
69
95
  quiet: bool = False,
70
96
  yes: bool = False,
71
97
  hint: str = "",
72
- model: str = None,
98
+ model: str | None = None,
99
+ language: str | None = None,
73
100
  version: bool = False,
74
101
  dry_run: bool = False,
102
+ message_only: bool = False,
75
103
  verbose: bool = False,
76
104
  no_verify: bool = False,
105
+ skip_secret_scan: bool = False,
106
+ hook_timeout: int = 0,
77
107
  ) -> None:
78
108
  """Git Auto Commit - Generate commit messages with AI."""
79
109
  if ctx.invoked_subcommand is None:
@@ -81,56 +111,94 @@ def cli(
81
111
  print(f"Git Auto Commit (gac) version: {__version__}")
82
112
  sys.exit(0)
83
113
  effective_log_level = log_level
84
- if verbose and log_level not in ("DEBUG", "INFO"):
85
- effective_log_level = "INFO"
86
114
  if quiet:
87
115
  effective_log_level = "ERROR"
88
116
  setup_logging(effective_log_level)
89
117
  logger.info("Starting gac")
90
118
 
91
- # Apply always_include_scope setting if no explicit scope provided
92
- effective_scope = scope
93
- if scope is None and config.get("always_include_scope", False):
94
- effective_scope = "" # Empty string triggers scope inference
119
+ # Validate incompatible flag combinations
120
+ if message_only and group:
121
+ console.print("[red]Error: --message-only and --group options are mutually exclusive[/red]")
122
+ console.print("[yellow]--message-only is for generating a single commit message for external use[/yellow]")
123
+ console.print("[yellow]--group is for organizing multiple commits within the current workflow[/yellow]")
124
+ sys.exit(1)
125
+
126
+ # Determine if we should infer scope based on -s flag or always_include_scope setting
127
+ infer_scope = bool(scope or config.get("always_include_scope", False))
128
+
129
+ # Determine if verbose mode should be enabled based on -v flag or verbose config setting
130
+ use_verbose = bool(verbose or config.get("verbose", False))
131
+
132
+ # Resolve language code to full name if provided
133
+ resolved_language = Languages.resolve_code(language) if language else None
95
134
 
96
135
  try:
97
136
  main(
98
137
  stage_all=add_all,
138
+ group=group,
139
+ interactive=interactive,
99
140
  model=model,
100
141
  hint=hint,
101
142
  one_liner=one_liner,
102
143
  show_prompt=show_prompt,
103
- scope=effective_scope,
144
+ infer_scope=bool(infer_scope),
104
145
  require_confirmation=not yes,
105
146
  push=push,
106
147
  quiet=quiet,
107
148
  dry_run=dry_run,
149
+ message_only=message_only,
150
+ verbose=use_verbose,
108
151
  no_verify=no_verify,
152
+ skip_secret_scan=skip_secret_scan or bool(config.get("skip_secret_scan", False)),
153
+ language=resolved_language,
154
+ hook_timeout=hook_timeout if hook_timeout > 0 else int(config.get("hook_timeout", 120) or 120),
109
155
  )
110
156
  except Exception as e:
111
157
  handle_error(e, exit_program=True)
112
158
  else:
159
+ # Determine if we should infer scope based on -s flag or always_include_scope setting
160
+ infer_scope = bool(scope or config.get("always_include_scope", False))
161
+
113
162
  ctx.obj = {
114
163
  "add_all": add_all,
164
+ "group": group,
165
+ "interactive": interactive,
115
166
  "log_level": log_level,
116
167
  "one_liner": one_liner,
117
168
  "push": push,
118
169
  "show_prompt": show_prompt,
119
- "scope": scope,
170
+ "scope": infer_scope,
120
171
  "quiet": quiet,
121
172
  "yes": yes,
122
173
  "hint": hint,
123
174
  "model": model,
175
+ "language": language,
124
176
  "version": version,
125
177
  "dry_run": dry_run,
178
+ "message_only": message_only,
126
179
  "verbose": verbose,
127
180
  "no_verify": no_verify,
181
+ "skip_secret_scan": skip_secret_scan,
182
+ "hook_timeout": hook_timeout,
128
183
  }
129
184
 
130
185
 
186
+ cli.add_command(auth_cli)
131
187
  cli.add_command(config_cli)
132
- cli.add_command(init_cli)
133
188
  cli.add_command(diff_cli)
189
+ cli.add_command(init_cli)
190
+ cli.add_command(language_cli)
191
+ cli.add_command(model_cli)
192
+
193
+
194
+ @click.command(context_settings=language_cli.context_settings)
195
+ @click.pass_context
196
+ def lang(ctx):
197
+ """Set the language for commit messages interactively. (Alias for 'language')"""
198
+ ctx.forward(language_cli)
199
+
200
+
201
+ cli.add_command(lang) # Add the lang alias
134
202
 
135
203
  if __name__ == "__main__":
136
204
  cli()
gac/config.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Configuration loading for gac.
2
2
 
3
- Handles environment variable and .env file precedence for application settings.
3
+ Handles environment variable and .gac.env file precedence for application settings.
4
4
  """
5
5
 
6
6
  import os
@@ -11,20 +11,17 @@ from dotenv import load_dotenv
11
11
  from gac.constants import EnvDefaults, Logging
12
12
 
13
13
 
14
- def load_config() -> dict[str, str | int | float | bool]:
15
- """Load configuration from $HOME/.gac.env, then ./.gac.env or ./.env, then environment variables."""
14
+ def load_config() -> dict[str, str | int | float | bool | None]:
15
+ """Load configuration from $HOME/.gac.env, then ./.gac.env, then environment variables."""
16
16
  user_config = Path.home() / ".gac.env"
17
17
  if user_config.exists():
18
18
  load_dotenv(user_config)
19
19
 
20
- # Check for both .gac.env and .env in project directory
20
+ # Check for .gac.env in project directory
21
21
  project_gac_env = Path(".gac.env")
22
- project_env = Path(".env")
23
22
 
24
23
  if project_gac_env.exists():
25
24
  load_dotenv(project_gac_env, override=True)
26
- elif project_env.exists():
27
- load_dotenv(project_env, override=True)
28
25
 
29
26
  config = {
30
27
  "model": os.getenv("GAC_MODEL"),
@@ -35,6 +32,15 @@ def load_config() -> dict[str, str | int | float | bool]:
35
32
  "warning_limit_tokens": int(os.getenv("GAC_WARNING_LIMIT_TOKENS", EnvDefaults.WARNING_LIMIT_TOKENS)),
36
33
  "always_include_scope": os.getenv("GAC_ALWAYS_INCLUDE_SCOPE", str(EnvDefaults.ALWAYS_INCLUDE_SCOPE)).lower()
37
34
  in ("true", "1", "yes", "on"),
35
+ "skip_secret_scan": os.getenv("GAC_SKIP_SECRET_SCAN", str(EnvDefaults.SKIP_SECRET_SCAN)).lower()
36
+ in ("true", "1", "yes", "on"),
37
+ "no_tiktoken": os.getenv("GAC_NO_TIKTOKEN", str(EnvDefaults.NO_TIKTOKEN)).lower() in ("true", "1", "yes", "on"),
38
+ "verbose": os.getenv("GAC_VERBOSE", str(EnvDefaults.VERBOSE)).lower() in ("true", "1", "yes", "on"),
39
+ "system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
40
+ "language": os.getenv("GAC_LANGUAGE"),
41
+ "translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
42
+ "rtl_confirmed": os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on"),
43
+ "hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
38
44
  }
39
45
 
40
46
  return config
gac/config_cli.py CHANGED
@@ -18,13 +18,34 @@ def config():
18
18
  @config.command()
19
19
  def show() -> None:
20
20
  """Show all current config values."""
21
- if not GAC_ENV_PATH.exists():
21
+ project_env_path = Path(".gac.env")
22
+ user_exists = GAC_ENV_PATH.exists()
23
+ project_exists = project_env_path.exists()
24
+
25
+ if not user_exists and not project_exists:
22
26
  click.echo("No $HOME/.gac.env found.")
27
+ click.echo("No project-level .gac.env found.")
23
28
  return
24
- load_dotenv(GAC_ENV_PATH, override=True)
25
- with open(GAC_ENV_PATH) as f:
26
- for line in f:
27
- click.echo(line.rstrip())
29
+
30
+ if user_exists:
31
+ 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())
35
+ else:
36
+ click.echo("No $HOME/.gac.env found.")
37
+
38
+ if project_exists:
39
+ if user_exists:
40
+ click.echo("")
41
+ 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())
45
+ click.echo("")
46
+ click.echo("Note: Project-level .gac.env overrides $HOME/.gac.env values for any duplicated variables.")
47
+ else:
48
+ click.echo("No project-level .gac.env found.")
28
49
 
29
50
 
30
51
  @config.command()
gac/constants.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import os
4
4
  from enum import Enum
5
- from re import Pattern
6
5
 
7
6
 
8
7
  class FileStatus(Enum):
@@ -21,9 +20,13 @@ class EnvDefaults:
21
20
 
22
21
  MAX_RETRIES: int = 3
23
22
  TEMPERATURE: float = 1
24
- MAX_OUTPUT_TOKENS: int = 512
25
- WARNING_LIMIT_TOKENS: int = 16384
23
+ MAX_OUTPUT_TOKENS: int = 4096 # includes reasoning tokens
24
+ WARNING_LIMIT_TOKENS: int = 32768
26
25
  ALWAYS_INCLUDE_SCOPE: bool = False
26
+ SKIP_SECRET_SCAN: bool = False
27
+ VERBOSE: bool = False
28
+ NO_TIKTOKEN: bool = False
29
+ HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
27
30
 
28
31
 
29
32
  class Logging:
@@ -39,13 +42,14 @@ class Utility:
39
42
  DEFAULT_ENCODING: str = "cl100k_base" # llm encoding
40
43
  DEFAULT_DIFF_TOKEN_LIMIT: int = 15000 # Maximum tokens for diff processing
41
44
  MAX_WORKERS: int = os.cpu_count() or 4 # Maximum number of parallel workers
45
+ MAX_DISPLAYED_SECRET_LENGTH: int = 50 # Maximum length for displaying secrets
42
46
 
43
47
 
44
48
  class FilePatterns:
45
49
  """Patterns for identifying special file types."""
46
50
 
47
51
  # Regex patterns to detect binary file changes in git diffs (e.g., images or other non-text files)
48
- BINARY: list[Pattern[str]] = [
52
+ BINARY: list[str] = [
49
53
  r"Binary files .* differ",
50
54
  r"GIT binary patch",
51
55
  ]
@@ -126,7 +130,7 @@ class CodePatternImportance:
126
130
 
127
131
  # Regex patterns to detect code structure changes in git diffs (e.g., class, function, import)
128
132
  # Note: The patterns are prefixed with "+" to match only added and modified lines
129
- PATTERNS: dict[Pattern[str], float] = {
133
+ PATTERNS: dict[str, float] = {
130
134
  # Structure changes
131
135
  r"\+\s*(class|interface|enum)\s+\w+": 1.8, # Class/interface/enum definitions
132
136
  r"\+\s*(def|function|func)\s+\w+\s*\(": 1.5, # Function definitions
@@ -148,3 +152,170 @@ class CodePatternImportance:
148
152
  r"\+\s*(test|describe|it|should)\s*\(": 1.1, # Test definitions
149
153
  r"\+\s*(assert|expect)": 1.0, # Assertions
150
154
  }
155
+
156
+
157
+ class Languages:
158
+ """Language code mappings and utilities."""
159
+
160
+ # Language code to full name mapping
161
+ # Supports ISO 639-1 codes and common variants
162
+ CODE_MAP: dict[str, str] = {
163
+ # English
164
+ "en": "English",
165
+ # Chinese
166
+ "zh": "Simplified Chinese",
167
+ "zh-cn": "Simplified Chinese",
168
+ "zh-hans": "Simplified Chinese",
169
+ "zh-tw": "Traditional Chinese",
170
+ "zh-hant": "Traditional Chinese",
171
+ # Japanese
172
+ "ja": "Japanese",
173
+ # Korean
174
+ "ko": "Korean",
175
+ # Spanish
176
+ "es": "Spanish",
177
+ # Portuguese
178
+ "pt": "Portuguese",
179
+ # French
180
+ "fr": "French",
181
+ # German
182
+ "de": "German",
183
+ # Russian
184
+ "ru": "Russian",
185
+ # Hindi
186
+ "hi": "Hindi",
187
+ # Italian
188
+ "it": "Italian",
189
+ # Polish
190
+ "pl": "Polish",
191
+ # Turkish
192
+ "tr": "Turkish",
193
+ # Dutch
194
+ "nl": "Dutch",
195
+ # Vietnamese
196
+ "vi": "Vietnamese",
197
+ # Thai
198
+ "th": "Thai",
199
+ # Indonesian
200
+ "id": "Indonesian",
201
+ # Swedish
202
+ "sv": "Swedish",
203
+ # Arabic
204
+ "ar": "Arabic",
205
+ # Hebrew
206
+ "he": "Hebrew",
207
+ # Greek
208
+ "el": "Greek",
209
+ # Danish
210
+ "da": "Danish",
211
+ # Norwegian
212
+ "no": "Norwegian",
213
+ "nb": "Norwegian",
214
+ "nn": "Norwegian",
215
+ # Finnish
216
+ "fi": "Finnish",
217
+ }
218
+
219
+ # List of languages with display names and English names for CLI selection
220
+ # Format: (display_name, english_name)
221
+ LANGUAGES: list[tuple[str, str]] = [
222
+ ("English", "English"),
223
+ ("简体中文", "Simplified Chinese"),
224
+ ("繁體中文", "Traditional Chinese"),
225
+ ("日本語", "Japanese"),
226
+ ("한국어", "Korean"),
227
+ ("Español", "Spanish"),
228
+ ("Português", "Portuguese"),
229
+ ("Français", "French"),
230
+ ("Deutsch", "German"),
231
+ ("Русский", "Russian"),
232
+ ("हिन्दी", "Hindi"),
233
+ ("Italiano", "Italian"),
234
+ ("Polski", "Polish"),
235
+ ("Türkçe", "Turkish"),
236
+ ("Nederlands", "Dutch"),
237
+ ("Tiếng Việt", "Vietnamese"),
238
+ ("ไทย", "Thai"),
239
+ ("Bahasa Indonesia", "Indonesian"),
240
+ ("Svenska", "Swedish"),
241
+ ("العربية", "Arabic"),
242
+ ("עברית", "Hebrew"),
243
+ ("Ελληνικά", "Greek"),
244
+ ("Dansk", "Danish"),
245
+ ("Norsk", "Norwegian"),
246
+ ("Suomi", "Finnish"),
247
+ ("Custom", "Custom"),
248
+ ]
249
+
250
+ @staticmethod
251
+ def resolve_code(language: str) -> str:
252
+ """Resolve a language code to its full name.
253
+
254
+ Args:
255
+ language: Language name or code (e.g., 'Spanish', 'es', 'zh-CN')
256
+
257
+ Returns:
258
+ Full language name (e.g., 'Spanish', 'Simplified Chinese')
259
+
260
+ If the input is already a full language name, it's returned as-is.
261
+ If it's a recognized code, it's converted to the full name.
262
+ Otherwise, the input is returned unchanged (for custom languages).
263
+ """
264
+ # Normalize the code to lowercase for lookup
265
+ code_lower = language.lower().strip()
266
+
267
+ # Check if it's a recognized code
268
+ if code_lower in Languages.CODE_MAP:
269
+ return Languages.CODE_MAP[code_lower]
270
+
271
+ # Return as-is (could be a full name or custom language)
272
+ return language
273
+
274
+
275
+ class CommitMessageConstants:
276
+ """Constants for commit message generation and cleaning."""
277
+
278
+ # Conventional commit type prefixes
279
+ CONVENTIONAL_PREFIXES: list[str] = [
280
+ "feat",
281
+ "fix",
282
+ "docs",
283
+ "style",
284
+ "refactor",
285
+ "perf",
286
+ "test",
287
+ "build",
288
+ "ci",
289
+ "chore",
290
+ ]
291
+
292
+ # XML tags that may leak from prompt templates into AI responses
293
+ XML_TAGS_TO_REMOVE: list[str] = [
294
+ "<git-status>",
295
+ "</git-status>",
296
+ "<git_status>",
297
+ "</git_status>",
298
+ "<git-diff>",
299
+ "</git-diff>",
300
+ "<git_diff>",
301
+ "</git_diff>",
302
+ "<repository_context>",
303
+ "</repository_context>",
304
+ "<instructions>",
305
+ "</instructions>",
306
+ "<format>",
307
+ "</format>",
308
+ "<conventions>",
309
+ "</conventions>",
310
+ ]
311
+
312
+ # Indicators that mark the start of the actual commit message in AI responses
313
+ COMMIT_INDICATORS: list[str] = [
314
+ "# Your commit message:",
315
+ "Your commit message:",
316
+ "The commit message is:",
317
+ "Here's the commit message:",
318
+ "Commit message:",
319
+ "Final commit message:",
320
+ "# Commit Message",
321
+ ]
gac/errors.py CHANGED
@@ -95,6 +95,11 @@ class AIError(GacError):
95
95
  """Create a model error."""
96
96
  return cls(message, error_type="model")
97
97
 
98
+ @classmethod
99
+ def unknown_error(cls, message: str) -> "AIError":
100
+ """Create an unknown error."""
101
+ return cls(message, error_type="unknown")
102
+
98
103
 
99
104
  class FormattingError(GacError):
100
105
  """Error related to code formatting."""
@@ -102,6 +107,12 @@ class FormattingError(GacError):
102
107
  exit_code = 5
103
108
 
104
109
 
110
+ class SecurityError(GacError):
111
+ """Error related to security issues (e.g., detected secrets)."""
112
+
113
+ exit_code = 6
114
+
115
+
105
116
  # Simplified error hierarchy - we use a single AIError class with error codes
106
117
  # instead of multiple subclasses for better maintainability
107
118
 
@@ -130,6 +141,8 @@ def handle_error(error: Exception, exit_program: bool = False, quiet: bool = Fal
130
141
  logger.error("Git operation failed. Please check your repository status.")
131
142
  elif isinstance(error, AIError):
132
143
  logger.error("AI operation failed. Please check your configuration and API keys.")
144
+ elif isinstance(error, SecurityError):
145
+ logger.error("Security scan detected potential secrets in staged changes.")
133
146
  else:
134
147
  logger.error("An unexpected error occurred.")
135
148
 
@@ -170,6 +183,7 @@ def format_error_for_user(error: Exception) -> str:
170
183
  ConfigError: "Please check your configuration settings.",
171
184
  GitError: "Please ensure Git is installed and you're in a valid Git repository.",
172
185
  FormattingError: "Please check that required formatters are installed.",
186
+ SecurityError: "Please remove or secure any detected secrets before committing.",
173
187
  }
174
188
 
175
189
  # Generic remediation for unexpected errors