gac 3.6.0__py3-none-any.whl → 3.10.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gac/__init__.py +4 -6
- gac/__version__.py +1 -1
- gac/ai_utils.py +59 -43
- gac/auth_cli.py +181 -36
- gac/cli.py +26 -9
- gac/commit_executor.py +59 -0
- gac/config.py +81 -2
- gac/config_cli.py +19 -7
- gac/constants/__init__.py +34 -0
- gac/constants/commit.py +63 -0
- gac/constants/defaults.py +40 -0
- gac/constants/file_patterns.py +110 -0
- gac/constants/languages.py +119 -0
- gac/diff_cli.py +0 -22
- gac/errors.py +8 -2
- gac/git.py +6 -6
- gac/git_state_validator.py +193 -0
- gac/grouped_commit_workflow.py +458 -0
- gac/init_cli.py +2 -1
- gac/interactive_mode.py +179 -0
- gac/language_cli.py +0 -1
- gac/main.py +231 -926
- gac/model_cli.py +67 -11
- gac/model_identifier.py +70 -0
- gac/oauth/__init__.py +26 -0
- gac/oauth/claude_code.py +89 -22
- gac/oauth/qwen_oauth.py +327 -0
- gac/oauth/token_store.py +81 -0
- gac/oauth_retry.py +161 -0
- gac/postprocess.py +155 -0
- gac/prompt.py +21 -479
- gac/prompt_builder.py +88 -0
- gac/providers/README.md +437 -0
- gac/providers/__init__.py +70 -78
- gac/providers/anthropic.py +12 -46
- gac/providers/azure_openai.py +48 -88
- gac/providers/base.py +329 -0
- gac/providers/cerebras.py +10 -33
- gac/providers/chutes.py +16 -62
- gac/providers/claude_code.py +64 -87
- gac/providers/custom_anthropic.py +51 -81
- gac/providers/custom_openai.py +29 -83
- gac/providers/deepseek.py +10 -33
- gac/providers/error_handler.py +139 -0
- gac/providers/fireworks.py +10 -33
- gac/providers/gemini.py +66 -63
- gac/providers/groq.py +10 -58
- gac/providers/kimi_coding.py +19 -55
- gac/providers/lmstudio.py +64 -43
- gac/providers/minimax.py +10 -33
- gac/providers/mistral.py +10 -33
- gac/providers/moonshot.py +10 -33
- gac/providers/ollama.py +56 -33
- gac/providers/openai.py +30 -36
- gac/providers/openrouter.py +15 -52
- gac/providers/protocol.py +71 -0
- gac/providers/qwen.py +64 -0
- gac/providers/registry.py +58 -0
- gac/providers/replicate.py +140 -82
- gac/providers/streamlake.py +26 -46
- gac/providers/synthetic.py +35 -37
- gac/providers/together.py +10 -33
- gac/providers/zai.py +29 -57
- gac/py.typed +0 -0
- gac/security.py +1 -1
- gac/templates/__init__.py +1 -0
- gac/templates/question_generation.txt +60 -0
- gac/templates/system_prompt.txt +224 -0
- gac/templates/user_prompt.txt +28 -0
- gac/utils.py +36 -6
- gac/workflow_context.py +162 -0
- gac/workflow_utils.py +3 -8
- {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/METADATA +6 -4
- gac-3.10.10.dist-info/RECORD +79 -0
- gac/constants.py +0 -321
- gac-3.6.0.dist-info/RECORD +0 -53
- {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
- {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
- {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
gac/model_cli.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from typing import cast
|
|
5
6
|
|
|
6
7
|
import click
|
|
7
8
|
import questionary
|
|
@@ -61,7 +62,7 @@ def _prompt_required_text(prompt: str) -> str | None:
|
|
|
61
62
|
return None
|
|
62
63
|
value = response.strip()
|
|
63
64
|
if value:
|
|
64
|
-
return value
|
|
65
|
+
return cast(str, value)
|
|
65
66
|
click.echo("A value is required. Please try again.")
|
|
66
67
|
|
|
67
68
|
|
|
@@ -84,7 +85,7 @@ def _configure_model(existing_env: dict[str, str]) -> bool:
|
|
|
84
85
|
("Azure OpenAI", "gpt-5-mini"),
|
|
85
86
|
("Cerebras", "zai-glm-4.6"),
|
|
86
87
|
("Chutes", "zai-org/GLM-4.6-FP8"),
|
|
87
|
-
("Claude Code", "claude-sonnet-4-5"),
|
|
88
|
+
("Claude Code (OAuth)", "claude-sonnet-4-5"),
|
|
88
89
|
("Custom (Anthropic)", ""),
|
|
89
90
|
("Custom (OpenAI)", ""),
|
|
90
91
|
("DeepSeek", "deepseek-chat"),
|
|
@@ -99,6 +100,7 @@ def _configure_model(existing_env: dict[str, str]) -> bool:
|
|
|
99
100
|
("Ollama", "gemma3"),
|
|
100
101
|
("OpenAI", "gpt-5-mini"),
|
|
101
102
|
("OpenRouter", "openrouter/auto"),
|
|
103
|
+
("Qwen.ai (OAuth)", "qwen3-coder-plus"),
|
|
102
104
|
("Replicate", "openai/gpt-oss-120b"),
|
|
103
105
|
("Streamlake", ""),
|
|
104
106
|
("Synthetic.new", "hf:zai-org/GLM-4.6"),
|
|
@@ -116,22 +118,27 @@ def _configure_model(existing_env: dict[str, str]) -> bool:
|
|
|
116
118
|
provider_key = provider.lower().replace(".", "").replace(" ", "-").replace("(", "").replace(")", "")
|
|
117
119
|
|
|
118
120
|
is_azure_openai = provider_key == "azure-openai"
|
|
119
|
-
is_claude_code = provider_key == "claude-code"
|
|
121
|
+
is_claude_code = provider_key == "claude-code-oauth"
|
|
120
122
|
is_custom_anthropic = provider_key == "custom-anthropic"
|
|
121
123
|
is_custom_openai = provider_key == "custom-openai"
|
|
122
124
|
is_lmstudio = provider_key == "lm-studio"
|
|
123
125
|
is_ollama = provider_key == "ollama"
|
|
126
|
+
is_qwen = provider_key == "qwenai-oauth"
|
|
124
127
|
is_streamlake = provider_key == "streamlake"
|
|
125
128
|
is_zai = provider_key in ("zai", "zai-coding")
|
|
126
129
|
|
|
127
|
-
if provider_key == "
|
|
130
|
+
if provider_key == "claude-code-oauth":
|
|
131
|
+
provider_key = "claude-code"
|
|
132
|
+
elif provider_key == "kimi-for-coding":
|
|
133
|
+
provider_key = "kimi-coding"
|
|
134
|
+
elif provider_key == "minimaxio":
|
|
128
135
|
provider_key = "minimax"
|
|
129
|
-
elif provider_key == "syntheticnew":
|
|
130
|
-
provider_key = "synthetic"
|
|
131
136
|
elif provider_key == "moonshot-ai":
|
|
132
137
|
provider_key = "moonshot"
|
|
133
|
-
elif provider_key == "
|
|
134
|
-
provider_key = "
|
|
138
|
+
elif provider_key == "qwenai-oauth":
|
|
139
|
+
provider_key = "qwen"
|
|
140
|
+
elif provider_key == "syntheticnew":
|
|
141
|
+
provider_key = "synthetic"
|
|
135
142
|
|
|
136
143
|
if is_streamlake:
|
|
137
144
|
endpoint_id = _prompt_required_text("Enter the Streamlake inference endpoint ID (required):")
|
|
@@ -269,10 +276,12 @@ def _configure_model(existing_env: dict[str, str]) -> bool:
|
|
|
269
276
|
|
|
270
277
|
# Handle Claude Code OAuth separately
|
|
271
278
|
if is_claude_code:
|
|
272
|
-
from gac.oauth.claude_code import authenticate_and_save
|
|
279
|
+
from gac.oauth.claude_code import authenticate_and_save
|
|
280
|
+
from gac.oauth.token_store import TokenStore
|
|
273
281
|
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
token_store = TokenStore()
|
|
283
|
+
existing_token_data = token_store.get_token("claude-code")
|
|
284
|
+
if existing_token_data:
|
|
276
285
|
click.echo("\n✓ Claude Code access token already configured.")
|
|
277
286
|
action = questionary.select(
|
|
278
287
|
"What would you like to do?",
|
|
@@ -305,6 +314,53 @@ def _configure_model(existing_env: dict[str, str]) -> bool:
|
|
|
305
314
|
return False
|
|
306
315
|
return True
|
|
307
316
|
|
|
317
|
+
# Handle Qwen OAuth separately
|
|
318
|
+
if is_qwen:
|
|
319
|
+
from gac.oauth import QwenOAuthProvider, TokenStore
|
|
320
|
+
|
|
321
|
+
token_store = TokenStore()
|
|
322
|
+
qwen_token = token_store.get_token("qwen")
|
|
323
|
+
if qwen_token:
|
|
324
|
+
click.echo("\n✓ Qwen access token already configured.")
|
|
325
|
+
action = questionary.select(
|
|
326
|
+
"What would you like to do?",
|
|
327
|
+
choices=[
|
|
328
|
+
"Keep existing token",
|
|
329
|
+
"Re-authenticate (get new token)",
|
|
330
|
+
],
|
|
331
|
+
use_shortcuts=True,
|
|
332
|
+
use_arrow_keys=True,
|
|
333
|
+
use_jk_keys=False,
|
|
334
|
+
).ask()
|
|
335
|
+
|
|
336
|
+
if action is None or action.startswith("Keep existing"):
|
|
337
|
+
if action is None:
|
|
338
|
+
click.echo("Qwen configuration cancelled. Keeping existing token.")
|
|
339
|
+
else:
|
|
340
|
+
click.echo("Keeping existing Qwen token")
|
|
341
|
+
return True
|
|
342
|
+
else:
|
|
343
|
+
click.echo("\n🔐 Starting Qwen OAuth authentication...")
|
|
344
|
+
provider = QwenOAuthProvider(token_store)
|
|
345
|
+
try:
|
|
346
|
+
provider.initiate_auth(open_browser=True)
|
|
347
|
+
click.echo("✅ Qwen authentication completed successfully!")
|
|
348
|
+
return True
|
|
349
|
+
except Exception as e:
|
|
350
|
+
click.echo(f"❌ Qwen authentication failed: {e}")
|
|
351
|
+
return False
|
|
352
|
+
else:
|
|
353
|
+
click.echo("\n🔐 Starting Qwen OAuth authentication...")
|
|
354
|
+
click.echo(" (Your browser will open automatically)\n")
|
|
355
|
+
provider = QwenOAuthProvider(token_store)
|
|
356
|
+
try:
|
|
357
|
+
provider.initiate_auth(open_browser=True)
|
|
358
|
+
click.echo("\n✅ Qwen authentication completed successfully!")
|
|
359
|
+
return True
|
|
360
|
+
except Exception as e:
|
|
361
|
+
click.echo(f"\n❌ Qwen authentication failed: {e}")
|
|
362
|
+
return False
|
|
363
|
+
|
|
308
364
|
# Determine API key name based on provider
|
|
309
365
|
if is_lmstudio:
|
|
310
366
|
api_key_name = "LMSTUDIO_API_KEY"
|
gac/model_identifier.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Model identifier value object for parsing and validating model strings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from gac.errors import ConfigError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ModelIdentifier:
|
|
12
|
+
"""Represents a parsed model identifier in the format 'provider:model_name'.
|
|
13
|
+
|
|
14
|
+
This is an immutable value object that ensures model identifiers are
|
|
15
|
+
properly validated and provides convenient access to the components.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
provider: The provider name (e.g., 'openai', 'anthropic', 'claude-code')
|
|
19
|
+
model_name: The model name (e.g., 'gpt-4o-mini', 'claude-haiku-4-5')
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
provider: str
|
|
23
|
+
model_name: str
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def parse(cls, model_string: str) -> ModelIdentifier:
|
|
27
|
+
"""Parse a model string into a ModelIdentifier.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
model_string: A string in the format 'provider:model_name'
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A ModelIdentifier instance
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ConfigError: If the format is invalid or components are empty
|
|
37
|
+
"""
|
|
38
|
+
normalized = model_string.strip()
|
|
39
|
+
|
|
40
|
+
if ":" not in normalized:
|
|
41
|
+
raise ConfigError(
|
|
42
|
+
f"Invalid model format: '{model_string}'. Expected 'provider:model', "
|
|
43
|
+
"e.g. 'openai:gpt-4o-mini'. Use 'gac config set model <provider:model>' "
|
|
44
|
+
"to update your configuration."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
provider, model_name = normalized.split(":", 1)
|
|
48
|
+
|
|
49
|
+
if not provider or not model_name:
|
|
50
|
+
raise ConfigError(
|
|
51
|
+
f"Invalid model format: '{model_string}'. Both provider and model name "
|
|
52
|
+
"are required (example: 'anthropic:claude-haiku-4-5')."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return cls(provider=provider, model_name=model_name)
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
"""Return the canonical string representation."""
|
|
59
|
+
return f"{self.provider}:{self.model_name}"
|
|
60
|
+
|
|
61
|
+
def starts_with_provider(self, prefix: str) -> bool:
|
|
62
|
+
"""Check if the provider starts with the given prefix.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
prefix: The prefix to check (e.g., 'claude-code', 'qwen')
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if the provider matches or the full identifier starts with prefix
|
|
69
|
+
"""
|
|
70
|
+
return self.provider == prefix or str(self).startswith(f"{prefix}:")
|
gac/oauth/__init__.py
CHANGED
|
@@ -1 +1,27 @@
|
|
|
1
1
|
"""OAuth authentication utilities for GAC."""
|
|
2
|
+
|
|
3
|
+
from .claude_code import (
|
|
4
|
+
authenticate_and_save,
|
|
5
|
+
is_token_expired,
|
|
6
|
+
load_stored_token,
|
|
7
|
+
perform_oauth_flow,
|
|
8
|
+
refresh_token_if_expired,
|
|
9
|
+
remove_token,
|
|
10
|
+
save_token,
|
|
11
|
+
)
|
|
12
|
+
from .qwen_oauth import QwenDeviceFlow, QwenOAuthProvider
|
|
13
|
+
from .token_store import OAuthToken, TokenStore
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"authenticate_and_save",
|
|
17
|
+
"is_token_expired",
|
|
18
|
+
"load_stored_token",
|
|
19
|
+
"OAuthToken",
|
|
20
|
+
"perform_oauth_flow",
|
|
21
|
+
"QwenDeviceFlow",
|
|
22
|
+
"QwenOAuthProvider",
|
|
23
|
+
"refresh_token_if_expired",
|
|
24
|
+
"remove_token",
|
|
25
|
+
"save_token",
|
|
26
|
+
"TokenStore",
|
|
27
|
+
]
|
gac/oauth/claude_code.py
CHANGED
|
@@ -12,12 +12,14 @@ import time
|
|
|
12
12
|
import webbrowser
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
15
|
-
from
|
|
16
|
-
from typing import Any, TypedDict
|
|
15
|
+
from typing import Any, TypedDict, cast
|
|
17
16
|
from urllib.parse import parse_qs, urlencode, urlparse
|
|
18
17
|
|
|
19
18
|
import httpx
|
|
20
19
|
|
|
20
|
+
from gac.oauth.token_store import OAuthToken, TokenStore
|
|
21
|
+
from gac.utils import get_ssl_verify
|
|
22
|
+
|
|
21
23
|
logger = logging.getLogger(__name__)
|
|
22
24
|
|
|
23
25
|
|
|
@@ -250,6 +252,7 @@ def exchange_code_for_tokens(auth_code: str, context: OAuthContext) -> dict[str,
|
|
|
250
252
|
json=payload,
|
|
251
253
|
headers=headers,
|
|
252
254
|
timeout=30,
|
|
255
|
+
verify=get_ssl_verify(),
|
|
253
256
|
)
|
|
254
257
|
logger.info("Token exchange response: %s", response.status_code)
|
|
255
258
|
if response.status_code == 200:
|
|
@@ -328,7 +331,7 @@ def perform_oauth_flow(quiet: bool = False) -> dict[str, Any] | None:
|
|
|
328
331
|
print("✓ Authorization code received")
|
|
329
332
|
print(" Exchanging for access token...\n")
|
|
330
333
|
|
|
331
|
-
tokens = exchange_code_for_tokens(result.code, context)
|
|
334
|
+
tokens = exchange_code_for_tokens(cast(str, result.code), context)
|
|
332
335
|
if not tokens:
|
|
333
336
|
if not quiet:
|
|
334
337
|
print("❌ Token exchange failed. Please try again.")
|
|
@@ -340,32 +343,82 @@ def perform_oauth_flow(quiet: bool = False) -> dict[str, Any] | None:
|
|
|
340
343
|
return tokens
|
|
341
344
|
|
|
342
345
|
|
|
343
|
-
def
|
|
344
|
-
"""
|
|
345
|
-
|
|
346
|
+
def load_stored_token() -> str | None:
|
|
347
|
+
"""Load stored access token from token store."""
|
|
348
|
+
store = TokenStore()
|
|
349
|
+
token = store.get_token("claude-code")
|
|
350
|
+
if token:
|
|
351
|
+
return token.get("access_token")
|
|
352
|
+
return None
|
|
346
353
|
|
|
347
354
|
|
|
348
|
-
def
|
|
349
|
-
"""
|
|
350
|
-
from dotenv import dotenv_values
|
|
355
|
+
def is_token_expired() -> bool:
|
|
356
|
+
"""Check if the stored Claude Code token has expired.
|
|
351
357
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
358
|
+
Returns True if the token is expired or close to expiring (within 5 minutes).
|
|
359
|
+
"""
|
|
360
|
+
store = TokenStore()
|
|
361
|
+
token = store.get_token("claude-code")
|
|
362
|
+
if not token:
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
expiry = token.get("expiry")
|
|
366
|
+
if not expiry:
|
|
367
|
+
# No expiry information, assume it's still valid
|
|
368
|
+
return False
|
|
355
369
|
|
|
356
|
-
|
|
357
|
-
|
|
370
|
+
# Consider token expired if it expires within 5 minutes
|
|
371
|
+
current_time = time.time()
|
|
372
|
+
return current_time >= (expiry - 300)
|
|
358
373
|
|
|
359
374
|
|
|
360
|
-
def
|
|
361
|
-
"""
|
|
362
|
-
|
|
375
|
+
def refresh_token_if_expired(quiet: bool = True) -> bool:
|
|
376
|
+
"""Refresh the Claude Code token if it has expired.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
quiet: If True, suppress output messages
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
True if token is valid (or was successfully refreshed), False otherwise
|
|
383
|
+
"""
|
|
384
|
+
if not is_token_expired():
|
|
385
|
+
return True
|
|
386
|
+
|
|
387
|
+
if not quiet:
|
|
388
|
+
logger.info("Claude Code token expired, attempting to refresh...")
|
|
363
389
|
|
|
364
|
-
|
|
390
|
+
# Perform OAuth flow to get a new token
|
|
391
|
+
success = authenticate_and_save(quiet=quiet)
|
|
392
|
+
if not success and not quiet:
|
|
393
|
+
logger.error("Failed to refresh Claude Code token")
|
|
365
394
|
|
|
366
|
-
|
|
395
|
+
return success
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def save_token(access_token: str, token_data: dict[str, Any] | None = None) -> bool:
|
|
399
|
+
"""Save access token to token store.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
access_token: The OAuth access token string
|
|
403
|
+
token_data: Optional full token response data (includes expiry info)
|
|
404
|
+
"""
|
|
405
|
+
import os
|
|
406
|
+
|
|
407
|
+
store = TokenStore()
|
|
367
408
|
try:
|
|
368
|
-
|
|
409
|
+
token: OAuthToken = {
|
|
410
|
+
"access_token": access_token,
|
|
411
|
+
"token_type": "Bearer",
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Add expiry information if available
|
|
415
|
+
if token_data:
|
|
416
|
+
if "expires_at" in token_data:
|
|
417
|
+
token["expiry"] = int(token_data["expires_at"])
|
|
418
|
+
elif "expires_in" in token_data:
|
|
419
|
+
token["expiry"] = int(time.time() + token_data["expires_in"])
|
|
420
|
+
|
|
421
|
+
store.save_token("claude-code", token)
|
|
369
422
|
# Also update the current environment so the token is immediately available
|
|
370
423
|
os.environ["CLAUDE_CODE_ACCESS_TOKEN"] = access_token
|
|
371
424
|
return True
|
|
@@ -374,6 +427,20 @@ def save_token(access_token: str) -> bool:
|
|
|
374
427
|
return False
|
|
375
428
|
|
|
376
429
|
|
|
430
|
+
def remove_token() -> bool:
|
|
431
|
+
"""Remove stored access token from token store."""
|
|
432
|
+
import os
|
|
433
|
+
|
|
434
|
+
store = TokenStore()
|
|
435
|
+
try:
|
|
436
|
+
store.remove_token("claude-code")
|
|
437
|
+
os.environ.pop("CLAUDE_CODE_ACCESS_TOKEN", None)
|
|
438
|
+
return True
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
logger.error("Failed to remove token: %s", exc)
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
|
|
377
444
|
def authenticate_and_save(quiet: bool = False) -> bool:
|
|
378
445
|
"""Perform OAuth flow and save token."""
|
|
379
446
|
tokens = perform_oauth_flow(quiet=quiet)
|
|
@@ -386,12 +453,12 @@ def authenticate_and_save(quiet: bool = False) -> bool:
|
|
|
386
453
|
print("❌ No access token returned from authentication")
|
|
387
454
|
return False
|
|
388
455
|
|
|
389
|
-
if not save_token(access_token):
|
|
456
|
+
if not save_token(access_token, token_data=tokens):
|
|
390
457
|
if not quiet:
|
|
391
458
|
print("❌ Failed to save access token")
|
|
392
459
|
return False
|
|
393
460
|
|
|
394
461
|
if not quiet:
|
|
395
|
-
print(
|
|
462
|
+
print("✓ Access token saved to ~/.gac/oauth/claude-code.json")
|
|
396
463
|
|
|
397
464
|
return True
|