gac 3.6.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.
- gac/__version__.py +1 -1
- gac/ai_utils.py +47 -0
- gac/auth_cli.py +181 -36
- gac/cli.py +13 -0
- gac/config.py +54 -0
- gac/constants.py +7 -0
- gac/main.py +53 -11
- gac/model_cli.py +65 -10
- gac/oauth/__init__.py +26 -0
- gac/oauth/claude_code.py +87 -20
- gac/oauth/qwen_oauth.py +323 -0
- gac/oauth/token_store.py +81 -0
- gac/prompt.py +16 -4
- gac/providers/__init__.py +3 -0
- gac/providers/anthropic.py +11 -1
- gac/providers/azure_openai.py +5 -1
- gac/providers/cerebras.py +11 -1
- gac/providers/chutes.py +11 -1
- gac/providers/claude_code.py +11 -1
- gac/providers/custom_anthropic.py +5 -1
- gac/providers/custom_openai.py +5 -1
- gac/providers/deepseek.py +11 -1
- gac/providers/fireworks.py +11 -1
- gac/providers/gemini.py +11 -1
- gac/providers/groq.py +5 -1
- gac/providers/kimi_coding.py +5 -1
- gac/providers/lmstudio.py +12 -1
- gac/providers/minimax.py +11 -1
- gac/providers/mistral.py +11 -1
- gac/providers/moonshot.py +11 -1
- gac/providers/ollama.py +11 -1
- gac/providers/openai.py +11 -1
- gac/providers/openrouter.py +11 -1
- gac/providers/qwen.py +76 -0
- gac/providers/replicate.py +14 -2
- gac/providers/streamlake.py +11 -1
- gac/providers/synthetic.py +11 -1
- gac/providers/together.py +11 -1
- gac/providers/zai.py +11 -1
- gac/utils.py +30 -1
- gac/workflow_utils.py +3 -8
- {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/METADATA +6 -4
- gac-3.8.1.dist-info/RECORD +56 -0
- gac-3.6.0.dist-info/RECORD +0 -53
- {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/WHEEL +0 -0
- {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/entry_points.txt +0 -0
- {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/licenses/LICENSE +0 -0
gac/__version__.py
CHANGED
gac/ai_utils.py
CHANGED
|
@@ -124,6 +124,53 @@ def generate_with_retries(
|
|
|
124
124
|
if not messages:
|
|
125
125
|
raise AIError.model_error("No messages provided for AI generation")
|
|
126
126
|
|
|
127
|
+
# Load Claude Code token from TokenStore if needed
|
|
128
|
+
if provider == "claude-code":
|
|
129
|
+
from gac.oauth import refresh_token_if_expired
|
|
130
|
+
from gac.oauth.token_store import TokenStore
|
|
131
|
+
|
|
132
|
+
# Check token expiry and refresh if needed
|
|
133
|
+
if not refresh_token_if_expired(quiet=True):
|
|
134
|
+
raise AIError.authentication_error(
|
|
135
|
+
"Claude Code token not found or expired. Please authenticate with 'gac auth claude-code login'."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Load the (possibly refreshed) token
|
|
139
|
+
token_store = TokenStore()
|
|
140
|
+
token_data = token_store.get_token("claude-code")
|
|
141
|
+
if token_data and "access_token" in token_data:
|
|
142
|
+
os.environ["CLAUDE_CODE_ACCESS_TOKEN"] = token_data["access_token"]
|
|
143
|
+
else:
|
|
144
|
+
raise AIError.authentication_error(
|
|
145
|
+
"Claude Code token not found. Please authenticate with 'gac auth claude-code login'."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Check Qwen OAuth token expiry and refresh if needed
|
|
149
|
+
if provider == "qwen":
|
|
150
|
+
from gac.oauth import QwenOAuthProvider, TokenStore
|
|
151
|
+
|
|
152
|
+
oauth_provider = QwenOAuthProvider(TokenStore())
|
|
153
|
+
token = oauth_provider.get_token()
|
|
154
|
+
if not token:
|
|
155
|
+
if not quiet:
|
|
156
|
+
console.print("[yellow]⚠ Qwen authentication not found or expired[/yellow]")
|
|
157
|
+
console.print("[cyan]🔐 Starting automatic authentication...[/cyan]")
|
|
158
|
+
try:
|
|
159
|
+
oauth_provider.initiate_auth(open_browser=True)
|
|
160
|
+
token = oauth_provider.get_token()
|
|
161
|
+
if not token:
|
|
162
|
+
raise AIError.authentication_error(
|
|
163
|
+
"Qwen authentication failed. Run 'gac auth qwen login' to authenticate manually."
|
|
164
|
+
)
|
|
165
|
+
if not quiet:
|
|
166
|
+
console.print("[green]✓ Authentication successful![/green]\n")
|
|
167
|
+
except AIError:
|
|
168
|
+
raise
|
|
169
|
+
except Exception as e:
|
|
170
|
+
raise AIError.authentication_error(
|
|
171
|
+
f"Qwen authentication failed: {e}. Run 'gac auth qwen login' to authenticate manually."
|
|
172
|
+
) from e
|
|
173
|
+
|
|
127
174
|
# Set up spinner
|
|
128
175
|
if is_group:
|
|
129
176
|
message_type = f"grouped {task_description}s"
|
gac/auth_cli.py
CHANGED
|
@@ -1,54 +1,95 @@
|
|
|
1
|
-
"""CLI for
|
|
1
|
+
"""CLI for OAuth authentication with various providers.
|
|
2
2
|
|
|
3
|
-
Provides
|
|
3
|
+
Provides commands to authenticate and manage OAuth tokens for supported providers.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
10
|
-
from gac.oauth
|
|
10
|
+
from gac.oauth import (
|
|
11
|
+
QwenOAuthProvider,
|
|
12
|
+
TokenStore,
|
|
13
|
+
authenticate_and_save,
|
|
14
|
+
remove_token,
|
|
15
|
+
)
|
|
11
16
|
from gac.utils import setup_logging
|
|
12
17
|
|
|
13
18
|
logger = logging.getLogger(__name__)
|
|
14
19
|
|
|
15
20
|
|
|
16
|
-
@click.
|
|
17
|
-
@click.
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
is_flag=True,
|
|
21
|
-
help="Suppress non-error output",
|
|
22
|
-
)
|
|
23
|
-
@click.option(
|
|
24
|
-
"--log-level",
|
|
25
|
-
default="INFO",
|
|
26
|
-
type=click.Choice(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], case_sensitive=False),
|
|
27
|
-
help="Set log level (default: INFO)",
|
|
28
|
-
)
|
|
29
|
-
def auth(quiet: bool = False, log_level: str = "INFO") -> None:
|
|
30
|
-
"""Authenticate Claude Code OAuth token.
|
|
21
|
+
@click.group(invoke_without_command=True)
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def auth(ctx: click.Context) -> None:
|
|
24
|
+
"""Manage OAuth authentication for AI providers.
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
Claude Code OAuth
|
|
34
|
-
|
|
35
|
-
to ~/.gac.env.
|
|
26
|
+
Supports authentication for:
|
|
27
|
+
- claude-code: Claude Code subscription OAuth
|
|
28
|
+
- qwen: Qwen AI OAuth (device flow)
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
Examples:
|
|
31
|
+
gac auth # Show authentication status
|
|
32
|
+
gac auth claude-code login # Login to Claude Code
|
|
33
|
+
gac auth claude-code logout # Logout from Claude Code
|
|
34
|
+
gac auth claude-code status # Check Claude Code auth status
|
|
35
|
+
gac auth qwen login # Login to Qwen
|
|
36
|
+
gac auth qwen logout # Logout from Qwen
|
|
37
|
+
gac auth qwen status # Check Qwen auth status
|
|
39
38
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
if ctx.invoked_subcommand is None:
|
|
40
|
+
_show_auth_status()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _show_auth_status() -> None:
|
|
44
|
+
"""Show authentication status for all providers."""
|
|
45
|
+
click.echo("OAuth Authentication Status")
|
|
46
|
+
click.echo("-" * 40)
|
|
47
|
+
|
|
48
|
+
token_store = TokenStore()
|
|
49
|
+
|
|
50
|
+
claude_token = token_store.get_token("claude-code")
|
|
51
|
+
if claude_token:
|
|
52
|
+
click.echo("Claude Code: ✓ Authenticated")
|
|
43
53
|
else:
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
click.echo("Claude Code: ✗ Not authenticated")
|
|
55
|
+
click.echo(" Run 'gac auth claude-code login' to login")
|
|
56
|
+
|
|
57
|
+
qwen_token = token_store.get_token("qwen")
|
|
58
|
+
if qwen_token:
|
|
59
|
+
click.echo("Qwen: ✓ Authenticated")
|
|
60
|
+
else:
|
|
61
|
+
click.echo("Qwen: ✗ Not authenticated")
|
|
62
|
+
click.echo(" Run 'gac auth qwen login' to login")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Claude Code commands
|
|
66
|
+
@auth.group("claude-code")
|
|
67
|
+
def claude_code() -> None:
|
|
68
|
+
"""Manage Claude Code OAuth authentication.
|
|
69
|
+
|
|
70
|
+
Use browser-based authentication to log in to Claude Code.
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
|
|
75
|
+
@claude_code.command("login")
|
|
76
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
|
|
77
|
+
def claude_code_login(quiet: bool = False) -> None:
|
|
78
|
+
"""Login to Claude Code using OAuth.
|
|
79
|
+
|
|
80
|
+
Opens a browser to authenticate with Claude Code. The token is stored
|
|
81
|
+
securely in ~/.gac/oauth/claude-code.json.
|
|
82
|
+
"""
|
|
83
|
+
if not quiet:
|
|
84
|
+
setup_logging("INFO")
|
|
85
|
+
|
|
86
|
+
token_store = TokenStore()
|
|
87
|
+
existing_token = token_store.get_token("claude-code")
|
|
88
|
+
if existing_token:
|
|
89
|
+
if not quiet:
|
|
90
|
+
click.echo("✓ Already authenticated with Claude Code.")
|
|
91
|
+
if not click.confirm("Re-authenticate?"):
|
|
92
|
+
return
|
|
52
93
|
|
|
53
94
|
if not quiet:
|
|
54
95
|
click.echo("🔐 Starting Claude Code OAuth authentication...")
|
|
@@ -56,14 +97,118 @@ def auth(quiet: bool = False, log_level: str = "INFO") -> None:
|
|
|
56
97
|
click.echo(" (Waiting up to 3 minutes for callback)")
|
|
57
98
|
click.echo()
|
|
58
99
|
|
|
59
|
-
# Perform OAuth authentication
|
|
60
100
|
success = authenticate_and_save(quiet=quiet)
|
|
61
101
|
|
|
62
102
|
if success:
|
|
63
103
|
if not quiet:
|
|
104
|
+
click.echo()
|
|
64
105
|
click.echo("✅ Claude Code authentication completed successfully!")
|
|
65
|
-
click.echo(" Your new token has been saved and is ready to use.")
|
|
66
106
|
else:
|
|
67
107
|
click.echo("❌ Claude Code authentication failed.")
|
|
68
108
|
click.echo(" Please try again or check your network connection.")
|
|
69
109
|
raise click.ClickException("Claude Code authentication failed")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@claude_code.command("logout")
|
|
113
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
|
|
114
|
+
def claude_code_logout(quiet: bool = False) -> None:
|
|
115
|
+
"""Logout from Claude Code and remove stored tokens."""
|
|
116
|
+
token_store = TokenStore()
|
|
117
|
+
existing_token = token_store.get_token("claude-code")
|
|
118
|
+
|
|
119
|
+
if not existing_token:
|
|
120
|
+
if not quiet:
|
|
121
|
+
click.echo("Not currently authenticated with Claude Code.")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
remove_token()
|
|
126
|
+
if not quiet:
|
|
127
|
+
click.echo("✅ Successfully logged out from Claude Code.")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
click.echo("❌ Failed to remove Claude Code token.")
|
|
130
|
+
raise click.ClickException("Claude Code logout failed") from e
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@claude_code.command("status")
|
|
134
|
+
def claude_code_status() -> None:
|
|
135
|
+
"""Check Claude Code authentication status."""
|
|
136
|
+
token_store = TokenStore()
|
|
137
|
+
token = token_store.get_token("claude-code")
|
|
138
|
+
|
|
139
|
+
if token:
|
|
140
|
+
click.echo("Claude Code Authentication Status: ✓ Authenticated")
|
|
141
|
+
else:
|
|
142
|
+
click.echo("Claude Code Authentication Status: ✗ Not authenticated")
|
|
143
|
+
click.echo("Run 'gac auth claude-code login' to authenticate.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Qwen commands
|
|
147
|
+
@auth.group()
|
|
148
|
+
def qwen() -> None:
|
|
149
|
+
"""Manage Qwen OAuth authentication.
|
|
150
|
+
|
|
151
|
+
Use device flow authentication to log in to Qwen AI.
|
|
152
|
+
"""
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@qwen.command("login")
|
|
157
|
+
@click.option("--no-browser", is_flag=True, help="Don't automatically open browser")
|
|
158
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
|
|
159
|
+
def qwen_login(no_browser: bool = False, quiet: bool = False) -> None:
|
|
160
|
+
"""Login to Qwen using OAuth device flow.
|
|
161
|
+
|
|
162
|
+
Opens a browser to authenticate with Qwen. The token is stored
|
|
163
|
+
securely in ~/.gac/oauth/qwen.json.
|
|
164
|
+
"""
|
|
165
|
+
if not quiet:
|
|
166
|
+
setup_logging("INFO")
|
|
167
|
+
|
|
168
|
+
provider = QwenOAuthProvider()
|
|
169
|
+
|
|
170
|
+
if provider.is_authenticated():
|
|
171
|
+
if not quiet:
|
|
172
|
+
click.echo("✓ Already authenticated with Qwen.")
|
|
173
|
+
if not click.confirm("Re-authenticate?"):
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
provider.initiate_auth(open_browser=not no_browser)
|
|
178
|
+
if not quiet:
|
|
179
|
+
click.echo()
|
|
180
|
+
click.echo("✅ Qwen authentication completed successfully!")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
click.echo(f"❌ Qwen authentication failed: {e}")
|
|
183
|
+
raise click.ClickException("Qwen authentication failed") from None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@qwen.command("logout")
|
|
187
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
|
|
188
|
+
def qwen_logout(quiet: bool = False) -> None:
|
|
189
|
+
"""Logout from Qwen and remove stored tokens."""
|
|
190
|
+
provider = QwenOAuthProvider()
|
|
191
|
+
|
|
192
|
+
if not provider.is_authenticated():
|
|
193
|
+
if not quiet:
|
|
194
|
+
click.echo("Not currently authenticated with Qwen.")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
provider.logout()
|
|
198
|
+
if not quiet:
|
|
199
|
+
click.echo("✅ Successfully logged out from Qwen.")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@qwen.command("status")
|
|
203
|
+
def qwen_status() -> None:
|
|
204
|
+
"""Check Qwen authentication status."""
|
|
205
|
+
provider = QwenOAuthProvider()
|
|
206
|
+
token = provider.get_token()
|
|
207
|
+
|
|
208
|
+
if token:
|
|
209
|
+
click.echo("Qwen Authentication Status: ✓ Authenticated")
|
|
210
|
+
if token.get("resource_url"):
|
|
211
|
+
click.echo(f"API Endpoint: {token['resource_url']}")
|
|
212
|
+
else:
|
|
213
|
+
click.echo("Qwen Authentication Status: ✗ Not authenticated")
|
|
214
|
+
click.echo("Run 'gac auth qwen login' to authenticate.")
|
gac/cli.py
CHANGED
|
@@ -6,6 +6,7 @@ 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
|
|
@@ -73,6 +74,11 @@ console = Console()
|
|
|
73
74
|
# Advanced options
|
|
74
75
|
@click.option("--no-verify", is_flag=True, help="Skip pre-commit and lefthook hooks when committing")
|
|
75
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
|
+
)
|
|
76
82
|
@click.option(
|
|
77
83
|
"--hook-timeout",
|
|
78
84
|
type=int,
|
|
@@ -103,6 +109,7 @@ def cli(
|
|
|
103
109
|
verbose: bool = False,
|
|
104
110
|
no_verify: bool = False,
|
|
105
111
|
skip_secret_scan: bool = False,
|
|
112
|
+
no_verify_ssl: bool = False,
|
|
106
113
|
hook_timeout: int = 0,
|
|
107
114
|
) -> None:
|
|
108
115
|
"""Git Auto Commit - Generate commit messages with AI."""
|
|
@@ -116,6 +123,11 @@ def cli(
|
|
|
116
123
|
setup_logging(effective_log_level)
|
|
117
124
|
logger.info("Starting gac")
|
|
118
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
|
+
|
|
119
131
|
# Validate incompatible flag combinations
|
|
120
132
|
if message_only and group:
|
|
121
133
|
console.print("[red]Error: --message-only and --group options are mutually exclusive[/red]")
|
|
@@ -179,6 +191,7 @@ def cli(
|
|
|
179
191
|
"verbose": verbose,
|
|
180
192
|
"no_verify": no_verify,
|
|
181
193
|
"skip_secret_scan": skip_secret_scan,
|
|
194
|
+
"no_verify_ssl": no_verify_ssl,
|
|
182
195
|
"hook_timeout": hook_timeout,
|
|
183
196
|
}
|
|
184
197
|
|
gac/config.py
CHANGED
|
@@ -9,6 +9,57 @@ 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]:
|
|
@@ -35,6 +86,8 @@ def load_config() -> dict[str, str | int | float | bool | None]:
|
|
|
35
86
|
"skip_secret_scan": os.getenv("GAC_SKIP_SECRET_SCAN", str(EnvDefaults.SKIP_SECRET_SCAN)).lower()
|
|
36
87
|
in ("true", "1", "yes", "on"),
|
|
37
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"),
|
|
38
91
|
"verbose": os.getenv("GAC_VERBOSE", str(EnvDefaults.VERBOSE)).lower() in ("true", "1", "yes", "on"),
|
|
39
92
|
"system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
|
|
40
93
|
"language": os.getenv("GAC_LANGUAGE"),
|
|
@@ -43,4 +96,5 @@ def load_config() -> dict[str, str | int | float | bool | None]:
|
|
|
43
96
|
"hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
|
|
44
97
|
}
|
|
45
98
|
|
|
99
|
+
validate_config(config)
|
|
46
100
|
return config
|
gac/constants.py
CHANGED
|
@@ -26,9 +26,16 @@ class EnvDefaults:
|
|
|
26
26
|
SKIP_SECRET_SCAN: bool = False
|
|
27
27
|
VERBOSE: bool = False
|
|
28
28
|
NO_TIKTOKEN: bool = False
|
|
29
|
+
NO_VERIFY_SSL: bool = False # Skip SSL certificate verification (for corporate proxies)
|
|
29
30
|
HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
|
|
30
31
|
|
|
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
|
|
37
|
+
|
|
38
|
+
|
|
32
39
|
class Logging:
|
|
33
40
|
"""Logging configuration constants."""
|
|
34
41
|
|
gac/main.py
CHANGED
|
@@ -14,7 +14,7 @@ from gac.ai import generate_commit_message
|
|
|
14
14
|
from gac.ai_utils import count_tokens
|
|
15
15
|
from gac.config import load_config
|
|
16
16
|
from gac.constants import EnvDefaults, Utility
|
|
17
|
-
from gac.errors import AIError, GitError, handle_error
|
|
17
|
+
from gac.errors import AIError, ConfigError, GitError, handle_error
|
|
18
18
|
from gac.git import (
|
|
19
19
|
detect_rename_mappings,
|
|
20
20
|
get_staged_files,
|
|
@@ -233,7 +233,8 @@ def execute_grouped_commits_workflow(
|
|
|
233
233
|
|
|
234
234
|
if first_iteration:
|
|
235
235
|
warning_limit_val = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
|
|
236
|
-
|
|
236
|
+
if warning_limit_val is None:
|
|
237
|
+
raise ConfigError("warning_limit_tokens configuration missing")
|
|
237
238
|
warning_limit = int(warning_limit_val)
|
|
238
239
|
if not check_token_warning(prompt_tokens, warning_limit, require_confirmation):
|
|
239
240
|
sys.exit(0)
|
|
@@ -343,7 +344,7 @@ def execute_grouped_commits_workflow(
|
|
|
343
344
|
files = commit["files"]
|
|
344
345
|
files_display = ", ".join(files)
|
|
345
346
|
console.print(f"[dim]{files_display}[/dim]")
|
|
346
|
-
commit_msg = commit["message"]
|
|
347
|
+
commit_msg = commit["message"].strip()
|
|
347
348
|
console.print(Panel(commit_msg, title=f"Commit Message {idx}/{num_commits}", border_style="cyan"))
|
|
348
349
|
console.print()
|
|
349
350
|
|
|
@@ -396,7 +397,7 @@ def execute_grouped_commits_workflow(
|
|
|
396
397
|
for idx, commit in enumerate(grouped_result["commits"], 1):
|
|
397
398
|
console.print(f"\n[cyan]Commit {idx}/{num_commits}:[/cyan]")
|
|
398
399
|
console.print(f" Files: {', '.join(commit['files'])}")
|
|
399
|
-
console.print(f" Message: {commit['message'][:50]}...")
|
|
400
|
+
console.print(f" Message: {commit['message'].strip()[:50]}...")
|
|
400
401
|
else:
|
|
401
402
|
original_staged_files = get_staged_files(existing_only=False)
|
|
402
403
|
original_staged_diff = run_git_command(["diff", "--cached", "--binary"], silent=True)
|
|
@@ -418,7 +419,7 @@ def execute_grouped_commits_workflow(
|
|
|
418
419
|
run_git_command(["add", "-A", file_path])
|
|
419
420
|
else:
|
|
420
421
|
run_git_command(["add", "-A", file_path])
|
|
421
|
-
execute_commit(commit["message"], no_verify, hook_timeout)
|
|
422
|
+
execute_commit(commit["message"].strip(), no_verify, hook_timeout)
|
|
422
423
|
console.print(f"[green]✓ Commit {idx}/{num_commits} created[/green]")
|
|
423
424
|
except Exception as e:
|
|
424
425
|
console.print(f"[red]✗ Failed at commit {idx}/{num_commits}: {e}[/red]")
|
|
@@ -550,7 +551,8 @@ def execute_single_commit_workflow(
|
|
|
550
551
|
prompt_tokens = count_tokens(conversation_messages, model)
|
|
551
552
|
if first_iteration:
|
|
552
553
|
warning_limit_val = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
|
|
553
|
-
|
|
554
|
+
if warning_limit_val is None:
|
|
555
|
+
raise ConfigError("warning_limit_tokens configuration missing")
|
|
554
556
|
warning_limit = int(warning_limit_val)
|
|
555
557
|
if not check_token_warning(prompt_tokens, warning_limit, require_confirmation):
|
|
556
558
|
sys.exit(0)
|
|
@@ -773,15 +775,18 @@ def main(
|
|
|
773
775
|
model = str(model_from_config)
|
|
774
776
|
|
|
775
777
|
temperature_val = config["temperature"]
|
|
776
|
-
|
|
778
|
+
if temperature_val is None:
|
|
779
|
+
raise ConfigError("temperature configuration missing")
|
|
777
780
|
temperature = float(temperature_val)
|
|
778
781
|
|
|
779
782
|
max_tokens_val = config["max_output_tokens"]
|
|
780
|
-
|
|
783
|
+
if max_tokens_val is None:
|
|
784
|
+
raise ConfigError("max_output_tokens configuration missing")
|
|
781
785
|
max_output_tokens = int(max_tokens_val)
|
|
782
786
|
|
|
783
787
|
max_retries_val = config["max_retries"]
|
|
784
|
-
|
|
788
|
+
if max_retries_val is None:
|
|
789
|
+
raise ConfigError("max_retries configuration missing")
|
|
785
790
|
max_retries = int(max_retries_val)
|
|
786
791
|
|
|
787
792
|
if stage_all and (not dry_run):
|
|
@@ -882,7 +887,8 @@ def main(
|
|
|
882
887
|
logger.info("No secrets detected in staged changes")
|
|
883
888
|
|
|
884
889
|
logger.debug(f"Preprocessing diff ({len(diff)} characters)")
|
|
885
|
-
|
|
890
|
+
if model is None:
|
|
891
|
+
raise ConfigError("Model must be specified via GAC_MODEL environment variable or --model flag")
|
|
886
892
|
processed_diff = preprocess_diff(diff, token_limit=Utility.DEFAULT_DIFF_TOKEN_LIMIT, model=model)
|
|
887
893
|
logger.debug(f"Processed diff ({len(processed_diff)} characters)")
|
|
888
894
|
|
|
@@ -1012,8 +1018,44 @@ def main(
|
|
|
1012
1018
|
console.print(f"[red]Re-authentication error: {auth_error}[/red]")
|
|
1013
1019
|
console.print("[yellow]Run 'gac model' to re-authenticate manually.[/yellow]")
|
|
1014
1020
|
sys.exit(1)
|
|
1021
|
+
# Check if this is a Qwen OAuth token expiration
|
|
1022
|
+
elif e.error_type == "authentication" and model.startswith("qwen:"):
|
|
1023
|
+
logger.error(str(e))
|
|
1024
|
+
console.print("[yellow]⚠ Qwen authentication failed[/yellow]")
|
|
1025
|
+
console.print("[cyan]🔐 Starting automatic re-authentication...[/cyan]")
|
|
1026
|
+
|
|
1027
|
+
try:
|
|
1028
|
+
from gac.oauth import QwenOAuthProvider, TokenStore
|
|
1029
|
+
|
|
1030
|
+
oauth_provider = QwenOAuthProvider(TokenStore())
|
|
1031
|
+
oauth_provider.initiate_auth(open_browser=True)
|
|
1032
|
+
console.print("[green]✓ Re-authentication successful![/green]")
|
|
1033
|
+
console.print("[cyan]Retrying commit...[/cyan]\n")
|
|
1034
|
+
|
|
1035
|
+
# Retry the commit workflow
|
|
1036
|
+
execute_single_commit_workflow(
|
|
1037
|
+
system_prompt=system_prompt,
|
|
1038
|
+
user_prompt=user_prompt,
|
|
1039
|
+
model=model,
|
|
1040
|
+
temperature=temperature,
|
|
1041
|
+
max_output_tokens=max_output_tokens,
|
|
1042
|
+
max_retries=max_retries,
|
|
1043
|
+
require_confirmation=require_confirmation,
|
|
1044
|
+
quiet=quiet,
|
|
1045
|
+
no_verify=no_verify,
|
|
1046
|
+
dry_run=dry_run,
|
|
1047
|
+
message_only=message_only,
|
|
1048
|
+
push=push,
|
|
1049
|
+
show_prompt=show_prompt,
|
|
1050
|
+
hook_timeout=hook_timeout,
|
|
1051
|
+
interactive=interactive,
|
|
1052
|
+
)
|
|
1053
|
+
except Exception as auth_error:
|
|
1054
|
+
console.print(f"[red]Re-authentication error: {auth_error}[/red]")
|
|
1055
|
+
console.print("[yellow]Run 'gac auth qwen login' to re-authenticate manually.[/yellow]")
|
|
1056
|
+
sys.exit(1)
|
|
1015
1057
|
else:
|
|
1016
|
-
# Non-Claude Code error or non-auth error
|
|
1058
|
+
# Non-Claude Code/Qwen error or non-auth error
|
|
1017
1059
|
logger.error(str(e))
|
|
1018
1060
|
console.print(f"[red]Failed to generate commit message: {str(e)}[/red]")
|
|
1019
1061
|
sys.exit(1)
|