gac 2.3.0__py3-none-any.whl → 2.7.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.
Potentially problematic release.
This version of gac might be problematic. Click here for more details.
- gac/__version__.py +1 -1
- gac/ai.py +4 -2
- gac/ai_utils.py +1 -0
- gac/auth_cli.py +69 -0
- gac/cli.py +14 -1
- gac/config.py +2 -0
- gac/constants.py +1 -0
- gac/git.py +69 -12
- gac/init_cli.py +175 -19
- gac/language_cli.py +170 -2
- gac/main.py +57 -8
- gac/oauth/__init__.py +1 -0
- gac/oauth/claude_code.py +397 -0
- gac/providers/__init__.py +2 -0
- gac/providers/claude_code.py +102 -0
- gac/providers/custom_anthropic.py +1 -1
- gac/utils.py +104 -3
- gac/workflow_utils.py +5 -2
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/METADATA +29 -10
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/RECORD +23 -19
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/WHEEL +0 -0
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/entry_points.txt +0 -0
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/licenses/LICENSE +0 -0
gac/__version__.py
CHANGED
gac/ai.py
CHANGED
|
@@ -13,6 +13,7 @@ from gac.providers import (
|
|
|
13
13
|
call_anthropic_api,
|
|
14
14
|
call_cerebras_api,
|
|
15
15
|
call_chutes_api,
|
|
16
|
+
call_claude_code_api,
|
|
16
17
|
call_custom_anthropic_api,
|
|
17
18
|
call_custom_openai_api,
|
|
18
19
|
call_deepseek_api,
|
|
@@ -48,7 +49,7 @@ def generate_commit_message(
|
|
|
48
49
|
"""Generate a commit message using direct API calls to AI providers.
|
|
49
50
|
|
|
50
51
|
Args:
|
|
51
|
-
model: The model to use in provider:model_name format (e.g., 'anthropic:claude-
|
|
52
|
+
model: The model to use in provider:model_name format (e.g., 'anthropic:claude-haiku-4-5')
|
|
52
53
|
prompt: Either a string prompt (for backward compatibility) or tuple of (system_prompt, user_prompt)
|
|
53
54
|
temperature: Controls randomness (0.0-1.0), lower values are more deterministic
|
|
54
55
|
max_tokens: Maximum tokens in the response
|
|
@@ -62,7 +63,7 @@ def generate_commit_message(
|
|
|
62
63
|
AIError: If generation fails after max_retries attempts
|
|
63
64
|
|
|
64
65
|
Example:
|
|
65
|
-
>>> model = "anthropic:claude-
|
|
66
|
+
>>> model = "anthropic:claude-haiku-4-5"
|
|
66
67
|
>>> system_prompt, user_prompt = build_prompt("On branch main", "diff --git a/README.md b/README.md")
|
|
67
68
|
>>> generate_commit_message(model, (system_prompt, user_prompt))
|
|
68
69
|
'docs: Update README with installation instructions'
|
|
@@ -88,6 +89,7 @@ def generate_commit_message(
|
|
|
88
89
|
provider_funcs = {
|
|
89
90
|
"anthropic": call_anthropic_api,
|
|
90
91
|
"cerebras": call_cerebras_api,
|
|
92
|
+
"claude-code": call_claude_code_api,
|
|
91
93
|
"chutes": call_chutes_api,
|
|
92
94
|
"custom-anthropic": call_custom_anthropic_api,
|
|
93
95
|
"custom-openai": call_custom_openai_api,
|
gac/ai_utils.py
CHANGED
gac/auth_cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""CLI for authenticating Claude Code OAuth tokens.
|
|
2
|
+
|
|
3
|
+
Provides a command to authenticate and re-authenticate Claude Code subscriptions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from gac.oauth.claude_code import authenticate_and_save, load_stored_token
|
|
11
|
+
from gac.utils import setup_logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.option(
|
|
18
|
+
"--quiet",
|
|
19
|
+
"-q",
|
|
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.
|
|
31
|
+
|
|
32
|
+
This command allows you to authenticate or re-authenticate your
|
|
33
|
+
Claude Code OAuth token when it expires or you want to refresh it.
|
|
34
|
+
It opens a browser window for the OAuth flow and saves the token
|
|
35
|
+
to ~/.gac.env.
|
|
36
|
+
|
|
37
|
+
The token is used by the Claude Code provider to access your
|
|
38
|
+
Claude Code subscription instead of requiring an Anthropic API key.
|
|
39
|
+
"""
|
|
40
|
+
# Setup logging
|
|
41
|
+
if quiet:
|
|
42
|
+
effective_log_level = "ERROR"
|
|
43
|
+
else:
|
|
44
|
+
effective_log_level = log_level
|
|
45
|
+
setup_logging(effective_log_level)
|
|
46
|
+
|
|
47
|
+
# Check if there's an existing token
|
|
48
|
+
existing_token = load_stored_token()
|
|
49
|
+
if existing_token and not quiet:
|
|
50
|
+
click.echo("✓ Found existing Claude Code access token.")
|
|
51
|
+
click.echo()
|
|
52
|
+
|
|
53
|
+
if not quiet:
|
|
54
|
+
click.echo("🔐 Starting Claude Code OAuth authentication...")
|
|
55
|
+
click.echo(" Your browser will open automatically")
|
|
56
|
+
click.echo(" (Waiting up to 3 minutes for callback)")
|
|
57
|
+
click.echo()
|
|
58
|
+
|
|
59
|
+
# Perform OAuth authentication
|
|
60
|
+
success = authenticate_and_save(quiet=quiet)
|
|
61
|
+
|
|
62
|
+
if success:
|
|
63
|
+
if not quiet:
|
|
64
|
+
click.echo("✅ Claude Code authentication completed successfully!")
|
|
65
|
+
click.echo(" Your new token has been saved and is ready to use.")
|
|
66
|
+
else:
|
|
67
|
+
click.echo("❌ Claude Code authentication failed.")
|
|
68
|
+
click.echo(" Please try again or check your network connection.")
|
|
69
|
+
raise click.ClickException("Claude Code authentication failed")
|
gac/cli.py
CHANGED
|
@@ -11,12 +11,14 @@ import sys
|
|
|
11
11
|
import click
|
|
12
12
|
|
|
13
13
|
from gac import __version__
|
|
14
|
+
from gac.auth_cli import auth as auth_cli
|
|
14
15
|
from gac.config import load_config
|
|
15
16
|
from gac.config_cli import config as config_cli
|
|
16
17
|
from gac.constants import Languages, Logging
|
|
17
18
|
from gac.diff_cli import diff as diff_cli
|
|
18
19
|
from gac.errors import handle_error
|
|
19
20
|
from gac.init_cli import init as init_cli
|
|
21
|
+
from gac.init_cli import model as model_cli
|
|
20
22
|
from gac.language_cli import language as language_cli
|
|
21
23
|
from gac.main import main
|
|
22
24
|
from gac.utils import setup_logging
|
|
@@ -65,6 +67,12 @@ logger = logging.getLogger(__name__)
|
|
|
65
67
|
# Advanced options
|
|
66
68
|
@click.option("--no-verify", is_flag=True, help="Skip pre-commit and lefthook hooks when committing")
|
|
67
69
|
@click.option("--skip-secret-scan", is_flag=True, help="Skip security scan for secrets in staged changes")
|
|
70
|
+
@click.option(
|
|
71
|
+
"--hook-timeout",
|
|
72
|
+
type=int,
|
|
73
|
+
default=0,
|
|
74
|
+
help="Timeout for pre-commit and lefthook hooks in seconds (0 to use configuration)",
|
|
75
|
+
)
|
|
68
76
|
# Other options
|
|
69
77
|
@click.option("--version", is_flag=True, help="Show the version of the Git Auto Commit (gac) tool")
|
|
70
78
|
@click.pass_context
|
|
@@ -87,6 +95,7 @@ def cli(
|
|
|
87
95
|
verbose: bool = False,
|
|
88
96
|
no_verify: bool = False,
|
|
89
97
|
skip_secret_scan: bool = False,
|
|
98
|
+
hook_timeout: int = 0,
|
|
90
99
|
) -> None:
|
|
91
100
|
"""Git Auto Commit - Generate commit messages with AI."""
|
|
92
101
|
if ctx.invoked_subcommand is None:
|
|
@@ -125,6 +134,7 @@ def cli(
|
|
|
125
134
|
no_verify=no_verify,
|
|
126
135
|
skip_secret_scan=skip_secret_scan or bool(config.get("skip_secret_scan", False)),
|
|
127
136
|
language=resolved_language,
|
|
137
|
+
hook_timeout=hook_timeout if hook_timeout > 0 else int(config.get("hook_timeout", 120) or 120),
|
|
128
138
|
)
|
|
129
139
|
except Exception as e:
|
|
130
140
|
handle_error(e, exit_program=True)
|
|
@@ -150,13 +160,16 @@ def cli(
|
|
|
150
160
|
"verbose": verbose,
|
|
151
161
|
"no_verify": no_verify,
|
|
152
162
|
"skip_secret_scan": skip_secret_scan,
|
|
163
|
+
"hook_timeout": hook_timeout,
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
|
|
167
|
+
cli.add_command(auth_cli)
|
|
156
168
|
cli.add_command(config_cli)
|
|
169
|
+
cli.add_command(diff_cli)
|
|
157
170
|
cli.add_command(init_cli)
|
|
158
171
|
cli.add_command(language_cli)
|
|
159
|
-
cli.add_command(
|
|
172
|
+
cli.add_command(model_cli)
|
|
160
173
|
|
|
161
174
|
|
|
162
175
|
@click.command(context_settings=language_cli.context_settings)
|
gac/config.py
CHANGED
|
@@ -41,6 +41,8 @@ def load_config() -> dict[str, str | int | float | bool | None]:
|
|
|
41
41
|
"system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
|
|
42
42
|
"language": os.getenv("GAC_LANGUAGE"),
|
|
43
43
|
"translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
|
|
44
|
+
"rtl_confirmed": os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on"),
|
|
45
|
+
"hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
return config
|
gac/constants.py
CHANGED
gac/git.py
CHANGED
|
@@ -14,6 +14,63 @@ from gac.utils import run_subprocess
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def run_subprocess_with_encoding_fallback(
|
|
18
|
+
command: list[str], silent: bool = False, timeout: int = 60
|
|
19
|
+
) -> subprocess.CompletedProcess:
|
|
20
|
+
"""Run subprocess with encoding fallback, returning full CompletedProcess object.
|
|
21
|
+
|
|
22
|
+
This is used for cases where we need both stdout and stderr separately,
|
|
23
|
+
like pre-commit and lefthook hook execution.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
command: List of command arguments
|
|
27
|
+
silent: If True, suppress debug logging
|
|
28
|
+
timeout: Command timeout in seconds
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
CompletedProcess object with stdout, stderr, and returncode
|
|
32
|
+
"""
|
|
33
|
+
from gac.utils import get_safe_encodings
|
|
34
|
+
|
|
35
|
+
encodings = get_safe_encodings()
|
|
36
|
+
last_exception: Exception | None = None
|
|
37
|
+
|
|
38
|
+
for encoding in encodings:
|
|
39
|
+
try:
|
|
40
|
+
if not silent:
|
|
41
|
+
logger.debug(f"Running command: {' '.join(command)} (encoding: {encoding})")
|
|
42
|
+
|
|
43
|
+
result = subprocess.run(
|
|
44
|
+
command,
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
check=False,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
encoding=encoding,
|
|
50
|
+
errors="replace",
|
|
51
|
+
)
|
|
52
|
+
return result
|
|
53
|
+
except UnicodeError as e:
|
|
54
|
+
last_exception = e
|
|
55
|
+
if not silent:
|
|
56
|
+
logger.debug(f"Failed to decode with {encoding}: {e}")
|
|
57
|
+
continue
|
|
58
|
+
except subprocess.TimeoutExpired:
|
|
59
|
+
raise
|
|
60
|
+
except Exception as e:
|
|
61
|
+
if not silent:
|
|
62
|
+
logger.debug(f"Command error: {e}")
|
|
63
|
+
# Try next encoding for non-timeout errors
|
|
64
|
+
last_exception = e
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# If we get here, all encodings failed
|
|
68
|
+
if last_exception:
|
|
69
|
+
raise subprocess.CalledProcessError(1, command, "", f"Encoding error: {last_exception}") from last_exception
|
|
70
|
+
else:
|
|
71
|
+
raise subprocess.CalledProcessError(1, command, "", "All encoding attempts failed")
|
|
72
|
+
|
|
73
|
+
|
|
17
74
|
def run_git_command(args: list[str], silent: bool = False, timeout: int = 30) -> str:
|
|
18
75
|
"""Run a git command and return the output."""
|
|
19
76
|
command = ["git"] + args
|
|
@@ -132,23 +189,23 @@ def get_diff(staged: bool = True, color: bool = True, commit1: str | None = None
|
|
|
132
189
|
|
|
133
190
|
def get_repo_root() -> str:
|
|
134
191
|
"""Get absolute path of repository root."""
|
|
135
|
-
result =
|
|
136
|
-
return result
|
|
192
|
+
result = run_git_command(["rev-parse", "--show-toplevel"])
|
|
193
|
+
return result
|
|
137
194
|
|
|
138
195
|
|
|
139
196
|
def get_current_branch() -> str:
|
|
140
197
|
"""Get name of current git branch."""
|
|
141
|
-
result =
|
|
142
|
-
return result
|
|
198
|
+
result = run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
|
|
199
|
+
return result
|
|
143
200
|
|
|
144
201
|
|
|
145
202
|
def get_commit_hash() -> str:
|
|
146
203
|
"""Get SHA-1 hash of current commit."""
|
|
147
|
-
result =
|
|
148
|
-
return result
|
|
204
|
+
result = run_git_command(["rev-parse", "HEAD"])
|
|
205
|
+
return result
|
|
149
206
|
|
|
150
207
|
|
|
151
|
-
def run_pre_commit_hooks() -> bool:
|
|
208
|
+
def run_pre_commit_hooks(hook_timeout: int = 120) -> bool:
|
|
152
209
|
"""Run pre-commit hooks if they exist.
|
|
153
210
|
|
|
154
211
|
Returns:
|
|
@@ -168,9 +225,9 @@ def run_pre_commit_hooks() -> bool:
|
|
|
168
225
|
return True
|
|
169
226
|
|
|
170
227
|
# Run pre-commit hooks on staged files
|
|
171
|
-
logger.info("Running pre-commit hooks...")
|
|
228
|
+
logger.info(f"Running pre-commit hooks with {hook_timeout}s timeout...")
|
|
172
229
|
# Run pre-commit and capture both stdout and stderr
|
|
173
|
-
result =
|
|
230
|
+
result = run_subprocess_with_encoding_fallback(["pre-commit", "run"], timeout=hook_timeout)
|
|
174
231
|
|
|
175
232
|
if result.returncode == 0:
|
|
176
233
|
# All hooks passed
|
|
@@ -195,7 +252,7 @@ def run_pre_commit_hooks() -> bool:
|
|
|
195
252
|
return True
|
|
196
253
|
|
|
197
254
|
|
|
198
|
-
def run_lefthook_hooks() -> bool:
|
|
255
|
+
def run_lefthook_hooks(hook_timeout: int = 120) -> bool:
|
|
199
256
|
"""Run Lefthook hooks if they exist.
|
|
200
257
|
|
|
201
258
|
Returns:
|
|
@@ -218,9 +275,9 @@ def run_lefthook_hooks() -> bool:
|
|
|
218
275
|
return True
|
|
219
276
|
|
|
220
277
|
# Run lefthook hooks on staged files
|
|
221
|
-
logger.info("Running Lefthook hooks...")
|
|
278
|
+
logger.info(f"Running Lefthook hooks with {hook_timeout}s timeout...")
|
|
222
279
|
# Run lefthook and capture both stdout and stderr
|
|
223
|
-
result =
|
|
280
|
+
result = run_subprocess_with_encoding_fallback(["lefthook", "run", "pre-commit"], timeout=hook_timeout)
|
|
224
281
|
|
|
225
282
|
if result.returncode == 0:
|
|
226
283
|
# All hooks passed
|
gac/init_cli.py
CHANGED
|
@@ -1,16 +1,58 @@
|
|
|
1
1
|
"""CLI for initializing gac configuration interactively."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
import questionary
|
|
7
|
-
from dotenv import dotenv_values, set_key
|
|
8
|
+
from dotenv import dotenv_values, load_dotenv, set_key
|
|
8
9
|
|
|
9
10
|
from gac.constants import Languages
|
|
10
11
|
|
|
11
12
|
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def _should_show_rtl_warning_for_init() -> bool:
|
|
16
|
+
"""Check if RTL warning should be shown based on init's GAC_ENV_PATH.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
True if warning should be shown, False if user previously confirmed
|
|
20
|
+
"""
|
|
21
|
+
if GAC_ENV_PATH.exists():
|
|
22
|
+
load_dotenv(GAC_ENV_PATH)
|
|
23
|
+
rtl_confirmed = os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on")
|
|
24
|
+
return not rtl_confirmed
|
|
25
|
+
return True # Show warning if no config exists
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _show_rtl_warning_for_init(language_name: str) -> bool:
|
|
29
|
+
"""Show RTL language warning for init command and save preference to GAC_ENV_PATH.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
language_name: Name of the RTL language
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if user wants to proceed, False if they cancel
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
terminal_width = 80 # Use default width
|
|
39
|
+
title = "⚠️ RTL Language Detected".center(terminal_width)
|
|
40
|
+
|
|
41
|
+
click.echo()
|
|
42
|
+
click.echo(click.style(title, fg="yellow", bold=True))
|
|
43
|
+
click.echo()
|
|
44
|
+
click.echo("Right-to-left (RTL) languages may not display correctly in gac due to terminal limitations.")
|
|
45
|
+
click.echo("However, the commit messages will work fine and should be readable in Git clients")
|
|
46
|
+
click.echo("that properly support RTL text (like most web interfaces and modern tools).\n")
|
|
47
|
+
|
|
48
|
+
proceed = questionary.confirm("Do you want to proceed anyway?").ask()
|
|
49
|
+
if proceed:
|
|
50
|
+
# Remember that user has confirmed RTL acceptance
|
|
51
|
+
set_key(str(GAC_ENV_PATH), "GAC_RTL_CONFIRMED", "true")
|
|
52
|
+
click.echo("✓ RTL preference saved - you won't see this warning again")
|
|
53
|
+
return proceed if proceed is not None else False
|
|
54
|
+
|
|
55
|
+
|
|
14
56
|
def _prompt_required_text(prompt: str) -> str | None:
|
|
15
57
|
"""Prompt until a non-empty string is provided or the user cancels."""
|
|
16
58
|
while True:
|
|
@@ -23,24 +65,25 @@ def _prompt_required_text(prompt: str) -> str | None:
|
|
|
23
65
|
click.echo("A value is required. Please try again.")
|
|
24
66
|
|
|
25
67
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
click.echo("Welcome to gac initialization!\n")
|
|
30
|
-
|
|
31
|
-
# Load existing environment values
|
|
32
|
-
existing_env = {}
|
|
68
|
+
def _load_existing_env() -> dict[str, str]:
|
|
69
|
+
"""Ensure the env file exists and return its current values."""
|
|
70
|
+
existing_env: dict[str, str] = {}
|
|
33
71
|
if GAC_ENV_PATH.exists():
|
|
34
72
|
click.echo(f"$HOME/.gac.env already exists at {GAC_ENV_PATH}. Values will be updated.")
|
|
35
|
-
existing_env =
|
|
73
|
+
existing_env = {k: v for k, v in dotenv_values(str(GAC_ENV_PATH)).items() if v is not None}
|
|
36
74
|
else:
|
|
37
75
|
GAC_ENV_PATH.touch()
|
|
38
76
|
click.echo(f"Created $HOME/.gac.env at {GAC_ENV_PATH}.")
|
|
77
|
+
return existing_env
|
|
78
|
+
|
|
39
79
|
|
|
80
|
+
def _configure_model(existing_env: dict[str, str]) -> bool:
|
|
81
|
+
"""Run the provider/model/API key configuration flow."""
|
|
40
82
|
providers = [
|
|
41
83
|
("Anthropic", "claude-haiku-4-5"),
|
|
42
84
|
("Cerebras", "zai-glm-4.6"),
|
|
43
85
|
("Chutes", "zai-org/GLM-4.6-FP8"),
|
|
86
|
+
("Claude Code", "claude-sonnet-4-5"),
|
|
44
87
|
("Custom (Anthropic)", ""),
|
|
45
88
|
("Custom (OpenAI)", ""),
|
|
46
89
|
("DeepSeek", "deepseek-chat"),
|
|
@@ -63,13 +106,14 @@ def init() -> None:
|
|
|
63
106
|
provider = questionary.select("Select your provider:", choices=provider_names).ask()
|
|
64
107
|
if not provider:
|
|
65
108
|
click.echo("Provider selection cancelled. Exiting.")
|
|
66
|
-
return
|
|
109
|
+
return False
|
|
67
110
|
provider_key = provider.lower().replace(".", "").replace(" ", "-").replace("(", "").replace(")", "")
|
|
68
111
|
|
|
69
112
|
is_ollama = provider_key == "ollama"
|
|
70
113
|
is_lmstudio = provider_key == "lm-studio"
|
|
71
114
|
is_streamlake = provider_key == "streamlake"
|
|
72
115
|
is_zai = provider_key in ("zai", "zai-coding")
|
|
116
|
+
is_claude_code = provider_key == "claude-code"
|
|
73
117
|
is_custom_anthropic = provider_key == "custom-anthropic"
|
|
74
118
|
is_custom_openai = provider_key == "custom-openai"
|
|
75
119
|
|
|
@@ -77,7 +121,7 @@ def init() -> None:
|
|
|
77
121
|
endpoint_id = _prompt_required_text("Enter the Streamlake inference endpoint ID (required):")
|
|
78
122
|
if endpoint_id is None:
|
|
79
123
|
click.echo("Streamlake configuration cancelled. Exiting.")
|
|
80
|
-
return
|
|
124
|
+
return False
|
|
81
125
|
model_to_save = endpoint_id
|
|
82
126
|
else:
|
|
83
127
|
model_suggestion = dict(providers)[provider]
|
|
@@ -88,7 +132,7 @@ def init() -> None:
|
|
|
88
132
|
model = questionary.text(model_prompt, default=model_suggestion).ask()
|
|
89
133
|
if model is None:
|
|
90
134
|
click.echo("Model entry cancelled. Exiting.")
|
|
91
|
-
return
|
|
135
|
+
return False
|
|
92
136
|
model_to_save = model.strip() if model.strip() else model_suggestion
|
|
93
137
|
|
|
94
138
|
set_key(str(GAC_ENV_PATH), "GAC_MODEL", f"{provider_key}:{model_to_save}")
|
|
@@ -98,7 +142,7 @@ def init() -> None:
|
|
|
98
142
|
base_url = _prompt_required_text("Enter the custom Anthropic-compatible base URL (required):")
|
|
99
143
|
if base_url is None:
|
|
100
144
|
click.echo("Custom Anthropic base URL entry cancelled. Exiting.")
|
|
101
|
-
return
|
|
145
|
+
return False
|
|
102
146
|
set_key(str(GAC_ENV_PATH), "CUSTOM_ANTHROPIC_BASE_URL", base_url)
|
|
103
147
|
click.echo(f"Set CUSTOM_ANTHROPIC_BASE_URL={base_url}")
|
|
104
148
|
|
|
@@ -112,7 +156,7 @@ def init() -> None:
|
|
|
112
156
|
base_url = _prompt_required_text("Enter the custom OpenAI-compatible base URL (required):")
|
|
113
157
|
if base_url is None:
|
|
114
158
|
click.echo("Custom OpenAI base URL entry cancelled. Exiting.")
|
|
115
|
-
return
|
|
159
|
+
return False
|
|
116
160
|
set_key(str(GAC_ENV_PATH), "CUSTOM_OPENAI_BASE_URL", base_url)
|
|
117
161
|
click.echo(f"Set CUSTOM_OPENAI_BASE_URL={base_url}")
|
|
118
162
|
elif is_ollama:
|
|
@@ -120,7 +164,7 @@ def init() -> None:
|
|
|
120
164
|
url = questionary.text(f"Enter the Ollama API URL (default: {url_default}):", default=url_default).ask()
|
|
121
165
|
if url is None:
|
|
122
166
|
click.echo("Ollama URL entry cancelled. Exiting.")
|
|
123
|
-
return
|
|
167
|
+
return False
|
|
124
168
|
url_to_save = url.strip() if url.strip() else url_default
|
|
125
169
|
set_key(str(GAC_ENV_PATH), "OLLAMA_API_URL", url_to_save)
|
|
126
170
|
click.echo(f"Set OLLAMA_API_URL={url_to_save}")
|
|
@@ -129,11 +173,46 @@ def init() -> None:
|
|
|
129
173
|
url = questionary.text(f"Enter the LM Studio API URL (default: {url_default}):", default=url_default).ask()
|
|
130
174
|
if url is None:
|
|
131
175
|
click.echo("LM Studio URL entry cancelled. Exiting.")
|
|
132
|
-
return
|
|
176
|
+
return False
|
|
133
177
|
url_to_save = url.strip() if url.strip() else url_default
|
|
134
178
|
set_key(str(GAC_ENV_PATH), "LMSTUDIO_API_URL", url_to_save)
|
|
135
179
|
click.echo(f"Set LMSTUDIO_API_URL={url_to_save}")
|
|
136
180
|
|
|
181
|
+
# Handle Claude Code OAuth separately
|
|
182
|
+
if is_claude_code:
|
|
183
|
+
from gac.oauth.claude_code import authenticate_and_save, load_stored_token
|
|
184
|
+
|
|
185
|
+
existing_token = load_stored_token()
|
|
186
|
+
if existing_token:
|
|
187
|
+
click.echo("\n✓ Claude Code access token already configured.")
|
|
188
|
+
action = questionary.select(
|
|
189
|
+
"What would you like to do?",
|
|
190
|
+
choices=[
|
|
191
|
+
"Keep existing token",
|
|
192
|
+
"Re-authenticate (get new token)",
|
|
193
|
+
],
|
|
194
|
+
).ask()
|
|
195
|
+
|
|
196
|
+
if action is None or action.startswith("Keep existing"):
|
|
197
|
+
if action is None:
|
|
198
|
+
click.echo("Claude Code configuration cancelled. Keeping existing token.")
|
|
199
|
+
else:
|
|
200
|
+
click.echo("Keeping existing Claude Code token")
|
|
201
|
+
return True
|
|
202
|
+
else:
|
|
203
|
+
click.echo("\n🔐 Starting Claude Code OAuth authentication...")
|
|
204
|
+
if not authenticate_and_save(quiet=False):
|
|
205
|
+
click.echo("❌ Claude Code authentication failed. Keeping existing token.")
|
|
206
|
+
return False
|
|
207
|
+
return True
|
|
208
|
+
else:
|
|
209
|
+
click.echo("\n🔐 Starting Claude Code OAuth authentication...")
|
|
210
|
+
click.echo(" (Your browser will open automatically)\n")
|
|
211
|
+
if not authenticate_and_save(quiet=False):
|
|
212
|
+
click.echo("\n❌ Claude Code authentication failed. Exiting.")
|
|
213
|
+
return False
|
|
214
|
+
return True
|
|
215
|
+
|
|
137
216
|
# Determine API key name based on provider
|
|
138
217
|
if is_lmstudio:
|
|
139
218
|
api_key_name = "LMSTUDIO_API_KEY"
|
|
@@ -189,7 +268,13 @@ def init() -> None:
|
|
|
189
268
|
else:
|
|
190
269
|
click.echo("No API key entered. You can add one later by editing ~/.gac.env")
|
|
191
270
|
|
|
192
|
-
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _configure_language(existing_env: dict[str, str]) -> None:
|
|
275
|
+
"""Run the language configuration flow."""
|
|
276
|
+
from gac.language_cli import is_rtl_text
|
|
277
|
+
|
|
193
278
|
click.echo("\n")
|
|
194
279
|
existing_language = existing_env.get("GAC_LANGUAGE")
|
|
195
280
|
|
|
@@ -216,7 +301,11 @@ def init() -> None:
|
|
|
216
301
|
# Proceed with language selection
|
|
217
302
|
display_names = [lang[0] for lang in Languages.LANGUAGES]
|
|
218
303
|
language_selection = questionary.select(
|
|
219
|
-
"Select a language for commit messages:",
|
|
304
|
+
"Select a language for commit messages:",
|
|
305
|
+
choices=display_names,
|
|
306
|
+
use_shortcuts=True,
|
|
307
|
+
use_arrow_keys=True,
|
|
308
|
+
use_jk_keys=False,
|
|
220
309
|
).ask()
|
|
221
310
|
|
|
222
311
|
if not language_selection:
|
|
@@ -237,10 +326,30 @@ def init() -> None:
|
|
|
237
326
|
language_value = None
|
|
238
327
|
else:
|
|
239
328
|
language_value = custom_language.strip()
|
|
329
|
+
|
|
330
|
+
# Check if the custom language appears to be RTL
|
|
331
|
+
if is_rtl_text(language_value):
|
|
332
|
+
if not _should_show_rtl_warning_for_init():
|
|
333
|
+
click.echo(
|
|
334
|
+
f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)"
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
if not _show_rtl_warning_for_init(language_value):
|
|
338
|
+
click.echo("Language selection cancelled. Keeping existing language.")
|
|
339
|
+
language_value = None
|
|
240
340
|
else:
|
|
241
341
|
# Find the English name for the selected language
|
|
242
342
|
language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == language_selection)
|
|
243
343
|
|
|
344
|
+
# Check if predefined language is RTL
|
|
345
|
+
if is_rtl_text(language_value):
|
|
346
|
+
if not _should_show_rtl_warning_for_init():
|
|
347
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
348
|
+
else:
|
|
349
|
+
if not _show_rtl_warning_for_init(language_value):
|
|
350
|
+
click.echo("Language selection cancelled. Keeping existing language.")
|
|
351
|
+
language_value = None
|
|
352
|
+
|
|
244
353
|
if language_value:
|
|
245
354
|
# Ask about prefix translation
|
|
246
355
|
prefix_choice = questionary.select(
|
|
@@ -266,7 +375,11 @@ def init() -> None:
|
|
|
266
375
|
# No existing language - proceed with normal flow
|
|
267
376
|
display_names = [lang[0] for lang in Languages.LANGUAGES]
|
|
268
377
|
language_selection = questionary.select(
|
|
269
|
-
"Select a language for commit messages:",
|
|
378
|
+
"Select a language for commit messages:",
|
|
379
|
+
choices=display_names,
|
|
380
|
+
use_shortcuts=True,
|
|
381
|
+
use_arrow_keys=True,
|
|
382
|
+
use_jk_keys=False,
|
|
270
383
|
).ask()
|
|
271
384
|
|
|
272
385
|
if not language_selection:
|
|
@@ -287,10 +400,28 @@ def init() -> None:
|
|
|
287
400
|
language_value = None
|
|
288
401
|
else:
|
|
289
402
|
language_value = custom_language.strip()
|
|
403
|
+
|
|
404
|
+
# Check if the custom language appears to be RTL
|
|
405
|
+
if is_rtl_text(language_value):
|
|
406
|
+
if not _should_show_rtl_warning_for_init():
|
|
407
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
408
|
+
else:
|
|
409
|
+
if not _show_rtl_warning_for_init(language_value):
|
|
410
|
+
click.echo("Language selection cancelled. Using English (default).")
|
|
411
|
+
language_value = None
|
|
290
412
|
else:
|
|
291
413
|
# Find the English name for the selected language
|
|
292
414
|
language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == language_selection)
|
|
293
415
|
|
|
416
|
+
# Check if predefined language is RTL
|
|
417
|
+
if is_rtl_text(language_value):
|
|
418
|
+
if not _should_show_rtl_warning_for_init():
|
|
419
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
420
|
+
else:
|
|
421
|
+
if not _show_rtl_warning_for_init(language_value):
|
|
422
|
+
click.echo("Language selection cancelled. Using English (default).")
|
|
423
|
+
language_value = None
|
|
424
|
+
|
|
294
425
|
if language_value:
|
|
295
426
|
# Ask about prefix translation
|
|
296
427
|
prefix_choice = questionary.select(
|
|
@@ -313,4 +444,29 @@ def init() -> None:
|
|
|
313
444
|
click.echo(f"Set GAC_LANGUAGE={language_value}")
|
|
314
445
|
click.echo(f"Set GAC_TRANSLATE_PREFIXES={'true' if translate_prefixes else 'false'}")
|
|
315
446
|
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@click.command()
|
|
451
|
+
def init() -> None:
|
|
452
|
+
"""Interactively set up $HOME/.gac.env for gac."""
|
|
453
|
+
click.echo("Welcome to gac initialization!\n")
|
|
454
|
+
|
|
455
|
+
existing_env = _load_existing_env()
|
|
456
|
+
if not _configure_model(existing_env):
|
|
457
|
+
return
|
|
458
|
+
_configure_language(existing_env)
|
|
459
|
+
|
|
316
460
|
click.echo(f"\ngac environment setup complete. You can edit {GAC_ENV_PATH} to update values later.")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@click.command()
|
|
464
|
+
def model() -> None:
|
|
465
|
+
"""Interactively update provider/model/API key without language prompts."""
|
|
466
|
+
click.echo("Welcome to gac model configuration!\n")
|
|
467
|
+
|
|
468
|
+
existing_env = _load_existing_env()
|
|
469
|
+
if not _configure_model(existing_env):
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
click.echo(f"\nModel configuration complete. You can edit {GAC_ENV_PATH} to update values later.")
|