gac 1.13.0__py3-none-any.whl → 3.8.1__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 (54) hide show
  1. gac/__version__.py +1 -1
  2. gac/ai.py +33 -47
  3. gac/ai_utils.py +113 -41
  4. gac/auth_cli.py +214 -0
  5. gac/cli.py +72 -2
  6. gac/config.py +63 -6
  7. gac/config_cli.py +26 -5
  8. gac/constants.py +178 -2
  9. gac/git.py +158 -12
  10. gac/init_cli.py +40 -125
  11. gac/language_cli.py +378 -0
  12. gac/main.py +868 -158
  13. gac/model_cli.py +429 -0
  14. gac/oauth/__init__.py +27 -0
  15. gac/oauth/claude_code.py +464 -0
  16. gac/oauth/qwen_oauth.py +323 -0
  17. gac/oauth/token_store.py +81 -0
  18. gac/preprocess.py +3 -3
  19. gac/prompt.py +573 -226
  20. gac/providers/__init__.py +49 -0
  21. gac/providers/anthropic.py +11 -1
  22. gac/providers/azure_openai.py +101 -0
  23. gac/providers/cerebras.py +11 -1
  24. gac/providers/chutes.py +11 -1
  25. gac/providers/claude_code.py +112 -0
  26. gac/providers/custom_anthropic.py +6 -2
  27. gac/providers/custom_openai.py +6 -3
  28. gac/providers/deepseek.py +11 -1
  29. gac/providers/fireworks.py +11 -1
  30. gac/providers/gemini.py +11 -1
  31. gac/providers/groq.py +5 -1
  32. gac/providers/kimi_coding.py +67 -0
  33. gac/providers/lmstudio.py +12 -1
  34. gac/providers/minimax.py +11 -1
  35. gac/providers/mistral.py +48 -0
  36. gac/providers/moonshot.py +48 -0
  37. gac/providers/ollama.py +11 -1
  38. gac/providers/openai.py +11 -1
  39. gac/providers/openrouter.py +11 -1
  40. gac/providers/qwen.py +76 -0
  41. gac/providers/replicate.py +110 -0
  42. gac/providers/streamlake.py +11 -1
  43. gac/providers/synthetic.py +11 -1
  44. gac/providers/together.py +11 -1
  45. gac/providers/zai.py +11 -1
  46. gac/security.py +1 -1
  47. gac/utils.py +272 -4
  48. gac/workflow_utils.py +217 -0
  49. {gac-1.13.0.dist-info → gac-3.8.1.dist-info}/METADATA +90 -27
  50. gac-3.8.1.dist-info/RECORD +56 -0
  51. {gac-1.13.0.dist-info → gac-3.8.1.dist-info}/WHEEL +1 -1
  52. gac-1.13.0.dist-info/RECORD +0 -41
  53. {gac-1.13.0.dist-info → gac-3.8.1.dist-info}/entry_points.txt +0 -0
  54. {gac-1.13.0.dist-info → gac-3.8.1.dist-info}/licenses/LICENSE +0 -0
gac/cli.py CHANGED
@@ -6,29 +6,40 @@ Defines the Click-based command-line interface and delegates execution to the ma
6
6
  """
7
7
 
8
8
  import logging
9
+ import os
9
10
  import sys
10
11
 
11
12
  import click
13
+ from rich.console import Console
12
14
 
13
15
  from gac import __version__
16
+ from gac.auth_cli import auth as auth_cli
14
17
  from gac.config import load_config
15
18
  from gac.config_cli import config as config_cli
16
- from gac.constants import Logging
19
+ from gac.constants import Languages, Logging
17
20
  from gac.diff_cli import diff as diff_cli
18
21
  from gac.errors import handle_error
19
22
  from gac.init_cli import init as init_cli
23
+ from gac.language_cli import language as language_cli
20
24
  from gac.main import main
25
+ from gac.model_cli import model as model_cli
21
26
  from gac.utils import setup_logging
22
27
 
23
28
  config = load_config()
24
29
  logger = logging.getLogger(__name__)
30
+ console = Console()
25
31
 
26
32
 
27
33
  @click.group(invoke_without_command=True, context_settings={"ignore_unknown_options": True})
28
34
  # Git workflow options
29
35
  @click.option("--add-all", "-a", is_flag=True, help="Stage all changes before committing")
36
+ @click.option("--group", "-g", is_flag=True, help="Group changes into multiple logical commits")
37
+ @click.option(
38
+ "--interactive", "-i", is_flag=True, help="Ask interactive questions to gather more context for the commit message"
39
+ )
30
40
  @click.option("--push", "-p", is_flag=True, help="Push changes to remote after committing")
31
41
  @click.option("--dry-run", is_flag=True, help="Dry run the commit workflow")
42
+ @click.option("--message-only", is_flag=True, help="Output only the generated commit message without committing")
32
43
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
33
44
  # Commit message options
34
45
  @click.option("--one-liner", "-o", is_flag=True, help="Generate a single-line commit message")
@@ -43,6 +54,9 @@ logger = logging.getLogger(__name__)
43
54
  @click.option("--hint", "-h", default="", help="Additional context to include in the prompt")
44
55
  # Model options
45
56
  @click.option("--model", "-m", help="Override the default model (format: 'provider:model_name')")
57
+ @click.option(
58
+ "--language", "-l", help="Override the language for commit messages (e.g., 'Spanish', 'es', 'zh-CN', 'ja')"
59
+ )
46
60
  # Output options
47
61
  @click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
48
62
  @click.option(
@@ -60,12 +74,25 @@ logger = logging.getLogger(__name__)
60
74
  # Advanced options
61
75
  @click.option("--no-verify", is_flag=True, help="Skip pre-commit and lefthook hooks when committing")
62
76
  @click.option("--skip-secret-scan", is_flag=True, help="Skip security scan for secrets in staged changes")
77
+ @click.option(
78
+ "--no-verify-ssl",
79
+ is_flag=True,
80
+ help="Skip SSL certificate verification (useful for corporate proxies)",
81
+ )
82
+ @click.option(
83
+ "--hook-timeout",
84
+ type=int,
85
+ default=0,
86
+ help="Timeout for pre-commit and lefthook hooks in seconds (0 to use configuration)",
87
+ )
63
88
  # Other options
64
89
  @click.option("--version", is_flag=True, help="Show the version of the Git Auto Commit (gac) tool")
65
90
  @click.pass_context
66
91
  def cli(
67
92
  ctx: click.Context,
68
93
  add_all: bool = False,
94
+ group: bool = False,
95
+ interactive: bool = False,
69
96
  log_level: str = str(config["log_level"]),
70
97
  one_liner: bool = False,
71
98
  push: bool = False,
@@ -75,11 +102,15 @@ def cli(
75
102
  yes: bool = False,
76
103
  hint: str = "",
77
104
  model: str | None = None,
105
+ language: str | None = None,
78
106
  version: bool = False,
79
107
  dry_run: bool = False,
108
+ message_only: bool = False,
80
109
  verbose: bool = False,
81
110
  no_verify: bool = False,
82
111
  skip_secret_scan: bool = False,
112
+ no_verify_ssl: bool = False,
113
+ hook_timeout: int = 0,
83
114
  ) -> None:
84
115
  """Git Auto Commit - Generate commit messages with AI."""
85
116
  if ctx.invoked_subcommand is None:
@@ -92,15 +123,32 @@ def cli(
92
123
  setup_logging(effective_log_level)
93
124
  logger.info("Starting gac")
94
125
 
126
+ # 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):
128
+ os.environ["GAC_NO_VERIFY_SSL"] = "true"
129
+ logger.info("SSL certificate verification disabled")
130
+
131
+ # Validate incompatible flag combinations
132
+ if message_only and group:
133
+ console.print("[red]Error: --message-only and --group options are mutually exclusive[/red]")
134
+ console.print("[yellow]--message-only is for generating a single commit message for external use[/yellow]")
135
+ console.print("[yellow]--group is for organizing multiple commits within the current workflow[/yellow]")
136
+ sys.exit(1)
137
+
95
138
  # Determine if we should infer scope based on -s flag or always_include_scope setting
96
139
  infer_scope = bool(scope or config.get("always_include_scope", False))
97
140
 
98
141
  # Determine if verbose mode should be enabled based on -v flag or verbose config setting
99
142
  use_verbose = bool(verbose or config.get("verbose", False))
100
143
 
144
+ # Resolve language code to full name if provided
145
+ resolved_language = Languages.resolve_code(language) if language else None
146
+
101
147
  try:
102
148
  main(
103
149
  stage_all=add_all,
150
+ group=group,
151
+ interactive=interactive,
104
152
  model=model,
105
153
  hint=hint,
106
154
  one_liner=one_liner,
@@ -110,9 +158,12 @@ def cli(
110
158
  push=push,
111
159
  quiet=quiet,
112
160
  dry_run=dry_run,
161
+ message_only=message_only,
113
162
  verbose=use_verbose,
114
163
  no_verify=no_verify,
115
164
  skip_secret_scan=skip_secret_scan or bool(config.get("skip_secret_scan", False)),
165
+ language=resolved_language,
166
+ hook_timeout=hook_timeout if hook_timeout > 0 else int(config.get("hook_timeout", 120) or 120),
116
167
  )
117
168
  except Exception as e:
118
169
  handle_error(e, exit_program=True)
@@ -122,6 +173,8 @@ def cli(
122
173
 
123
174
  ctx.obj = {
124
175
  "add_all": add_all,
176
+ "group": group,
177
+ "interactive": interactive,
125
178
  "log_level": log_level,
126
179
  "one_liner": one_liner,
127
180
  "push": push,
@@ -131,17 +184,34 @@ def cli(
131
184
  "yes": yes,
132
185
  "hint": hint,
133
186
  "model": model,
187
+ "language": language,
134
188
  "version": version,
135
189
  "dry_run": dry_run,
190
+ "message_only": message_only,
136
191
  "verbose": verbose,
137
192
  "no_verify": no_verify,
138
193
  "skip_secret_scan": skip_secret_scan,
194
+ "no_verify_ssl": no_verify_ssl,
195
+ "hook_timeout": hook_timeout,
139
196
  }
140
197
 
141
198
 
199
+ cli.add_command(auth_cli)
142
200
  cli.add_command(config_cli)
143
- cli.add_command(init_cli)
144
201
  cli.add_command(diff_cli)
202
+ cli.add_command(init_cli)
203
+ cli.add_command(language_cli)
204
+ cli.add_command(model_cli)
205
+
206
+
207
+ @click.command(context_settings=language_cli.context_settings)
208
+ @click.pass_context
209
+ def lang(ctx):
210
+ """Set the language for commit messages interactively. (Alias for 'language')"""
211
+ ctx.forward(language_cli)
212
+
213
+
214
+ cli.add_command(lang) # Add the lang alias
145
215
 
146
216
  if __name__ == "__main__":
147
217
  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
@@ -9,22 +9,70 @@ from pathlib import Path
9
9
  from dotenv import load_dotenv
10
10
 
11
11
  from gac.constants import EnvDefaults, Logging
12
+ from gac.errors import ConfigError
13
+
14
+
15
+ def validate_config(config: dict[str, str | int | float | bool | None]) -> None:
16
+ """Validate configuration values at load time.
17
+
18
+ Args:
19
+ config: Configuration dictionary to validate
20
+
21
+ Raises:
22
+ ConfigError: If any configuration value is invalid
23
+ """
24
+ # Validate temperature (0.0 to 2.0)
25
+ if config.get("temperature") is not None:
26
+ temp = config["temperature"]
27
+ if not isinstance(temp, (int, float)):
28
+ raise ConfigError(f"temperature must be a number, got {type(temp).__name__}")
29
+ if not 0.0 <= temp <= 2.0:
30
+ raise ConfigError(f"temperature must be between 0.0 and 2.0, got {temp}")
31
+
32
+ # Validate max_output_tokens (1 to 100000)
33
+ if config.get("max_output_tokens") is not None:
34
+ tokens = config["max_output_tokens"]
35
+ if not isinstance(tokens, int):
36
+ raise ConfigError(f"max_output_tokens must be an integer, got {type(tokens).__name__}")
37
+ if tokens < 1 or tokens > 100000:
38
+ raise ConfigError(f"max_output_tokens must be between 1 and 100000, got {tokens}")
39
+
40
+ # Validate max_retries (1 to 10)
41
+ if config.get("max_retries") is not None:
42
+ retries = config["max_retries"]
43
+ if not isinstance(retries, int):
44
+ raise ConfigError(f"max_retries must be an integer, got {type(retries).__name__}")
45
+ if retries < 1 or retries > 10:
46
+ raise ConfigError(f"max_retries must be between 1 and 10, got {retries}")
47
+
48
+ # Validate warning_limit_tokens (must be positive)
49
+ if config.get("warning_limit_tokens") is not None:
50
+ warning_limit = config["warning_limit_tokens"]
51
+ if not isinstance(warning_limit, int):
52
+ raise ConfigError(f"warning_limit_tokens must be an integer, got {type(warning_limit).__name__}")
53
+ if warning_limit < 1:
54
+ raise ConfigError(f"warning_limit_tokens must be positive, got {warning_limit}")
55
+
56
+ # Validate hook_timeout (must be positive)
57
+ if config.get("hook_timeout") is not None:
58
+ hook_timeout = config["hook_timeout"]
59
+ if not isinstance(hook_timeout, int):
60
+ raise ConfigError(f"hook_timeout must be an integer, got {type(hook_timeout).__name__}")
61
+ if hook_timeout < 1:
62
+ raise ConfigError(f"hook_timeout must be positive, got {hook_timeout}")
12
63
 
13
64
 
14
65
  def load_config() -> dict[str, str | int | float | bool | None]:
15
- """Load configuration from $HOME/.gac.env, then ./.gac.env or ./.env, then environment variables."""
66
+ """Load configuration from $HOME/.gac.env, then ./.gac.env, then environment variables."""
16
67
  user_config = Path.home() / ".gac.env"
17
68
  if user_config.exists():
18
69
  load_dotenv(user_config)
19
70
 
20
- # Check for both .gac.env and .env in project directory
71
+ # Check for .gac.env in project directory
21
72
  project_gac_env = Path(".gac.env")
22
- project_env = Path(".env")
23
73
 
24
74
  if project_gac_env.exists():
25
75
  load_dotenv(project_gac_env, override=True)
26
- elif project_env.exists():
27
- load_dotenv(project_env, override=True)
28
76
 
29
77
  config = {
30
78
  "model": os.getenv("GAC_MODEL"),
@@ -37,7 +85,16 @@ def load_config() -> dict[str, str | int | float | bool | None]:
37
85
  in ("true", "1", "yes", "on"),
38
86
  "skip_secret_scan": os.getenv("GAC_SKIP_SECRET_SCAN", str(EnvDefaults.SKIP_SECRET_SCAN)).lower()
39
87
  in ("true", "1", "yes", "on"),
88
+ "no_tiktoken": os.getenv("GAC_NO_TIKTOKEN", str(EnvDefaults.NO_TIKTOKEN)).lower() in ("true", "1", "yes", "on"),
89
+ "no_verify_ssl": os.getenv("GAC_NO_VERIFY_SSL", str(EnvDefaults.NO_VERIFY_SSL)).lower()
90
+ in ("true", "1", "yes", "on"),
40
91
  "verbose": os.getenv("GAC_VERBOSE", str(EnvDefaults.VERBOSE)).lower() in ("true", "1", "yes", "on"),
92
+ "system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
93
+ "language": os.getenv("GAC_LANGUAGE"),
94
+ "translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
95
+ "rtl_confirmed": os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on"),
96
+ "hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
41
97
  }
42
98
 
99
+ validate_config(config)
43
100
  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
@@ -20,11 +20,20 @@ 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
28
+ NO_TIKTOKEN: bool = False
29
+ NO_VERIFY_SSL: bool = False # Skip SSL certificate verification (for corporate proxies)
30
+ HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
31
+
32
+
33
+ class ProviderDefaults:
34
+ """Default values for provider configurations."""
35
+
36
+ HTTP_TIMEOUT: int = 120 # seconds - timeout for HTTP requests to LLM providers
28
37
 
29
38
 
30
39
  class Logging:
@@ -150,3 +159,170 @@ class CodePatternImportance:
150
159
  r"\+\s*(test|describe|it|should)\s*\(": 1.1, # Test definitions
151
160
  r"\+\s*(assert|expect)": 1.0, # Assertions
152
161
  }
162
+
163
+
164
+ class Languages:
165
+ """Language code mappings and utilities."""
166
+
167
+ # Language code to full name mapping
168
+ # Supports ISO 639-1 codes and common variants
169
+ CODE_MAP: dict[str, str] = {
170
+ # English
171
+ "en": "English",
172
+ # Chinese
173
+ "zh": "Simplified Chinese",
174
+ "zh-cn": "Simplified Chinese",
175
+ "zh-hans": "Simplified Chinese",
176
+ "zh-tw": "Traditional Chinese",
177
+ "zh-hant": "Traditional Chinese",
178
+ # Japanese
179
+ "ja": "Japanese",
180
+ # Korean
181
+ "ko": "Korean",
182
+ # Spanish
183
+ "es": "Spanish",
184
+ # Portuguese
185
+ "pt": "Portuguese",
186
+ # French
187
+ "fr": "French",
188
+ # German
189
+ "de": "German",
190
+ # Russian
191
+ "ru": "Russian",
192
+ # Hindi
193
+ "hi": "Hindi",
194
+ # Italian
195
+ "it": "Italian",
196
+ # Polish
197
+ "pl": "Polish",
198
+ # Turkish
199
+ "tr": "Turkish",
200
+ # Dutch
201
+ "nl": "Dutch",
202
+ # Vietnamese
203
+ "vi": "Vietnamese",
204
+ # Thai
205
+ "th": "Thai",
206
+ # Indonesian
207
+ "id": "Indonesian",
208
+ # Swedish
209
+ "sv": "Swedish",
210
+ # Arabic
211
+ "ar": "Arabic",
212
+ # Hebrew
213
+ "he": "Hebrew",
214
+ # Greek
215
+ "el": "Greek",
216
+ # Danish
217
+ "da": "Danish",
218
+ # Norwegian
219
+ "no": "Norwegian",
220
+ "nb": "Norwegian",
221
+ "nn": "Norwegian",
222
+ # Finnish
223
+ "fi": "Finnish",
224
+ }
225
+
226
+ # List of languages with display names and English names for CLI selection
227
+ # Format: (display_name, english_name)
228
+ LANGUAGES: list[tuple[str, str]] = [
229
+ ("English", "English"),
230
+ ("简体中文", "Simplified Chinese"),
231
+ ("繁體中文", "Traditional Chinese"),
232
+ ("日本語", "Japanese"),
233
+ ("한국어", "Korean"),
234
+ ("Español", "Spanish"),
235
+ ("Português", "Portuguese"),
236
+ ("Français", "French"),
237
+ ("Deutsch", "German"),
238
+ ("Русский", "Russian"),
239
+ ("हिन्दी", "Hindi"),
240
+ ("Italiano", "Italian"),
241
+ ("Polski", "Polish"),
242
+ ("Türkçe", "Turkish"),
243
+ ("Nederlands", "Dutch"),
244
+ ("Tiếng Việt", "Vietnamese"),
245
+ ("ไทย", "Thai"),
246
+ ("Bahasa Indonesia", "Indonesian"),
247
+ ("Svenska", "Swedish"),
248
+ ("العربية", "Arabic"),
249
+ ("עברית", "Hebrew"),
250
+ ("Ελληνικά", "Greek"),
251
+ ("Dansk", "Danish"),
252
+ ("Norsk", "Norwegian"),
253
+ ("Suomi", "Finnish"),
254
+ ("Custom", "Custom"),
255
+ ]
256
+
257
+ @staticmethod
258
+ def resolve_code(language: str) -> str:
259
+ """Resolve a language code to its full name.
260
+
261
+ Args:
262
+ language: Language name or code (e.g., 'Spanish', 'es', 'zh-CN')
263
+
264
+ Returns:
265
+ Full language name (e.g., 'Spanish', 'Simplified Chinese')
266
+
267
+ If the input is already a full language name, it's returned as-is.
268
+ If it's a recognized code, it's converted to the full name.
269
+ Otherwise, the input is returned unchanged (for custom languages).
270
+ """
271
+ # Normalize the code to lowercase for lookup
272
+ code_lower = language.lower().strip()
273
+
274
+ # Check if it's a recognized code
275
+ if code_lower in Languages.CODE_MAP:
276
+ return Languages.CODE_MAP[code_lower]
277
+
278
+ # Return as-is (could be a full name or custom language)
279
+ return language
280
+
281
+
282
+ class CommitMessageConstants:
283
+ """Constants for commit message generation and cleaning."""
284
+
285
+ # Conventional commit type prefixes
286
+ CONVENTIONAL_PREFIXES: list[str] = [
287
+ "feat",
288
+ "fix",
289
+ "docs",
290
+ "style",
291
+ "refactor",
292
+ "perf",
293
+ "test",
294
+ "build",
295
+ "ci",
296
+ "chore",
297
+ ]
298
+
299
+ # XML tags that may leak from prompt templates into AI responses
300
+ XML_TAGS_TO_REMOVE: list[str] = [
301
+ "<git-status>",
302
+ "</git-status>",
303
+ "<git_status>",
304
+ "</git_status>",
305
+ "<git-diff>",
306
+ "</git-diff>",
307
+ "<git_diff>",
308
+ "</git_diff>",
309
+ "<repository_context>",
310
+ "</repository_context>",
311
+ "<instructions>",
312
+ "</instructions>",
313
+ "<format>",
314
+ "</format>",
315
+ "<conventions>",
316
+ "</conventions>",
317
+ ]
318
+
319
+ # Indicators that mark the start of the actual commit message in AI responses
320
+ COMMIT_INDICATORS: list[str] = [
321
+ "# Your commit message:",
322
+ "Your commit message:",
323
+ "The commit message is:",
324
+ "Here's the commit message:",
325
+ "Commit message:",
326
+ "Final commit message:",
327
+ "# Commit Message",
328
+ ]