gac 3.10.3__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/__init__.py +15 -0
- gac/__version__.py +3 -0
- gac/ai.py +109 -0
- gac/ai_utils.py +246 -0
- gac/auth_cli.py +214 -0
- gac/cli.py +218 -0
- gac/commit_executor.py +62 -0
- gac/config.py +125 -0
- gac/config_cli.py +95 -0
- gac/constants.py +328 -0
- gac/diff_cli.py +159 -0
- gac/errors.py +231 -0
- gac/git.py +372 -0
- gac/git_state_validator.py +184 -0
- gac/grouped_commit_workflow.py +423 -0
- gac/init_cli.py +70 -0
- gac/interactive_mode.py +182 -0
- gac/language_cli.py +377 -0
- gac/main.py +476 -0
- gac/model_cli.py +430 -0
- gac/oauth/__init__.py +27 -0
- gac/oauth/claude_code.py +464 -0
- gac/oauth/qwen_oauth.py +327 -0
- gac/oauth/token_store.py +81 -0
- gac/preprocess.py +511 -0
- gac/prompt.py +878 -0
- gac/prompt_builder.py +88 -0
- gac/providers/README.md +437 -0
- gac/providers/__init__.py +80 -0
- gac/providers/anthropic.py +17 -0
- gac/providers/azure_openai.py +57 -0
- gac/providers/base.py +329 -0
- gac/providers/cerebras.py +15 -0
- gac/providers/chutes.py +25 -0
- gac/providers/claude_code.py +79 -0
- gac/providers/custom_anthropic.py +103 -0
- gac/providers/custom_openai.py +44 -0
- gac/providers/deepseek.py +15 -0
- gac/providers/error_handler.py +139 -0
- gac/providers/fireworks.py +15 -0
- gac/providers/gemini.py +90 -0
- gac/providers/groq.py +15 -0
- gac/providers/kimi_coding.py +27 -0
- gac/providers/lmstudio.py +80 -0
- gac/providers/minimax.py +15 -0
- gac/providers/mistral.py +15 -0
- gac/providers/moonshot.py +15 -0
- gac/providers/ollama.py +73 -0
- gac/providers/openai.py +32 -0
- gac/providers/openrouter.py +21 -0
- gac/providers/protocol.py +71 -0
- gac/providers/qwen.py +64 -0
- gac/providers/registry.py +58 -0
- gac/providers/replicate.py +156 -0
- gac/providers/streamlake.py +31 -0
- gac/providers/synthetic.py +40 -0
- gac/providers/together.py +15 -0
- gac/providers/zai.py +31 -0
- gac/py.typed +0 -0
- gac/security.py +293 -0
- gac/utils.py +401 -0
- gac/workflow_utils.py +217 -0
- gac-3.10.3.dist-info/METADATA +283 -0
- gac-3.10.3.dist-info/RECORD +67 -0
- gac-3.10.3.dist-info/WHEEL +4 -0
- gac-3.10.3.dist-info/entry_points.txt +2 -0
- gac-3.10.3.dist-info/licenses/LICENSE +16 -0
gac/cli.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# flake8: noqa: E304
|
|
2
|
+
|
|
3
|
+
"""CLI entry point for gac.
|
|
4
|
+
|
|
5
|
+
Defines the Click-based command-line interface and delegates execution to the main workflow.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from gac import __version__
|
|
17
|
+
from gac.auth_cli import auth as auth_cli
|
|
18
|
+
from gac.config import GACConfig, load_config
|
|
19
|
+
from gac.config_cli import config as config_cli
|
|
20
|
+
from gac.constants import Languages, Logging
|
|
21
|
+
from gac.diff_cli import diff as diff_cli
|
|
22
|
+
from gac.errors import handle_error
|
|
23
|
+
from gac.init_cli import init as init_cli
|
|
24
|
+
from gac.language_cli import language as language_cli
|
|
25
|
+
from gac.main import main
|
|
26
|
+
from gac.model_cli import model as model_cli
|
|
27
|
+
from gac.utils import setup_logging
|
|
28
|
+
|
|
29
|
+
config: GACConfig = load_config()
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.group(invoke_without_command=True, context_settings={"ignore_unknown_options": True})
|
|
35
|
+
# Git workflow options
|
|
36
|
+
@click.option("--add-all", "-a", is_flag=True, help="Stage all changes before committing")
|
|
37
|
+
@click.option("--group", "-g", is_flag=True, help="Group changes into multiple logical commits")
|
|
38
|
+
@click.option(
|
|
39
|
+
"--interactive", "-i", is_flag=True, help="Ask interactive questions to gather more context for the commit message"
|
|
40
|
+
)
|
|
41
|
+
@click.option("--push", "-p", is_flag=True, help="Push changes to remote after committing")
|
|
42
|
+
@click.option("--dry-run", is_flag=True, help="Dry run the commit workflow")
|
|
43
|
+
@click.option("--message-only", is_flag=True, help="Output only the generated commit message without committing")
|
|
44
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
45
|
+
# Commit message options
|
|
46
|
+
@click.option("--one-liner", "-o", is_flag=True, help="Generate a single-line commit message")
|
|
47
|
+
@click.option("--show-prompt", is_flag=True, help="Show the prompt sent to the LLM")
|
|
48
|
+
@click.option(
|
|
49
|
+
"--scope",
|
|
50
|
+
"-s",
|
|
51
|
+
is_flag=True,
|
|
52
|
+
default=False,
|
|
53
|
+
help="Infer an appropriate scope for the commit message",
|
|
54
|
+
)
|
|
55
|
+
@click.option("--hint", "-h", default="", help="Additional context to include in the prompt")
|
|
56
|
+
# Model options
|
|
57
|
+
@click.option("--model", "-m", help="Override the default model (format: 'provider:model_name')")
|
|
58
|
+
@click.option(
|
|
59
|
+
"--language", "-l", help="Override the language for commit messages (e.g., 'Spanish', 'es', 'zh-CN', 'ja')"
|
|
60
|
+
)
|
|
61
|
+
# Output options
|
|
62
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
|
|
63
|
+
@click.option(
|
|
64
|
+
"--verbose",
|
|
65
|
+
"-v",
|
|
66
|
+
is_flag=True,
|
|
67
|
+
help="Generate detailed commit messages with motivation, architecture, and impact sections",
|
|
68
|
+
)
|
|
69
|
+
@click.option(
|
|
70
|
+
"--log-level",
|
|
71
|
+
default=config["log_level"],
|
|
72
|
+
type=click.Choice(Logging.LEVELS, case_sensitive=False),
|
|
73
|
+
help=f"Set log level (default: {config['log_level']})",
|
|
74
|
+
)
|
|
75
|
+
# Advanced options
|
|
76
|
+
@click.option("--no-verify", is_flag=True, help="Skip pre-commit and lefthook hooks when committing")
|
|
77
|
+
@click.option("--skip-secret-scan", is_flag=True, help="Skip security scan for secrets in staged changes")
|
|
78
|
+
@click.option(
|
|
79
|
+
"--no-verify-ssl",
|
|
80
|
+
is_flag=True,
|
|
81
|
+
help="Skip SSL certificate verification (useful for corporate proxies)",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--hook-timeout",
|
|
85
|
+
type=int,
|
|
86
|
+
default=0,
|
|
87
|
+
help="Timeout for pre-commit and lefthook hooks in seconds (0 to use configuration)",
|
|
88
|
+
)
|
|
89
|
+
# Other options
|
|
90
|
+
@click.option("--version", is_flag=True, help="Show the version of the Git Auto Commit (gac) tool")
|
|
91
|
+
@click.pass_context
|
|
92
|
+
def cli(
|
|
93
|
+
ctx: click.Context,
|
|
94
|
+
add_all: bool = False,
|
|
95
|
+
group: bool = False,
|
|
96
|
+
interactive: bool = False,
|
|
97
|
+
log_level: str = str(config["log_level"]),
|
|
98
|
+
one_liner: bool = False,
|
|
99
|
+
push: bool = False,
|
|
100
|
+
show_prompt: bool = False,
|
|
101
|
+
scope: bool = False,
|
|
102
|
+
quiet: bool = False,
|
|
103
|
+
yes: bool = False,
|
|
104
|
+
hint: str = "",
|
|
105
|
+
model: str | None = None,
|
|
106
|
+
language: str | None = None,
|
|
107
|
+
version: bool = False,
|
|
108
|
+
dry_run: bool = False,
|
|
109
|
+
message_only: bool = False,
|
|
110
|
+
verbose: bool = False,
|
|
111
|
+
no_verify: bool = False,
|
|
112
|
+
skip_secret_scan: bool = False,
|
|
113
|
+
no_verify_ssl: bool = False,
|
|
114
|
+
hook_timeout: int = 0,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Git Auto Commit - Generate commit messages with AI."""
|
|
117
|
+
if ctx.invoked_subcommand is None:
|
|
118
|
+
if version:
|
|
119
|
+
print(f"Git Auto Commit (gac) version: {__version__}")
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
effective_log_level = log_level
|
|
122
|
+
if quiet:
|
|
123
|
+
effective_log_level = "ERROR"
|
|
124
|
+
setup_logging(effective_log_level)
|
|
125
|
+
logger.info("Starting gac")
|
|
126
|
+
|
|
127
|
+
# Set SSL verification environment variable if flag is used or config is set
|
|
128
|
+
if no_verify_ssl or config.get("no_verify_ssl", False):
|
|
129
|
+
os.environ["GAC_NO_VERIFY_SSL"] = "true"
|
|
130
|
+
logger.info("SSL certificate verification disabled")
|
|
131
|
+
|
|
132
|
+
# Validate incompatible flag combinations
|
|
133
|
+
if message_only and group:
|
|
134
|
+
console.print("[red]Error: --message-only and --group options are mutually exclusive[/red]")
|
|
135
|
+
console.print("[yellow]--message-only is for generating a single commit message for external use[/yellow]")
|
|
136
|
+
console.print("[yellow]--group is for organizing multiple commits within the current workflow[/yellow]")
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
|
|
139
|
+
# Determine if we should infer scope based on -s flag or always_include_scope setting
|
|
140
|
+
infer_scope = bool(scope or config.get("always_include_scope", False))
|
|
141
|
+
|
|
142
|
+
# Determine if verbose mode should be enabled based on -v flag or verbose config setting
|
|
143
|
+
use_verbose = bool(verbose or config.get("verbose", False))
|
|
144
|
+
|
|
145
|
+
# Resolve language code to full name if provided
|
|
146
|
+
resolved_language = Languages.resolve_code(language) if language else None
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
main(
|
|
150
|
+
stage_all=add_all,
|
|
151
|
+
group=group,
|
|
152
|
+
interactive=interactive,
|
|
153
|
+
model=model,
|
|
154
|
+
hint=hint,
|
|
155
|
+
one_liner=one_liner,
|
|
156
|
+
show_prompt=show_prompt,
|
|
157
|
+
infer_scope=bool(infer_scope),
|
|
158
|
+
require_confirmation=not yes,
|
|
159
|
+
push=push,
|
|
160
|
+
quiet=quiet,
|
|
161
|
+
dry_run=dry_run,
|
|
162
|
+
message_only=message_only,
|
|
163
|
+
verbose=use_verbose,
|
|
164
|
+
no_verify=no_verify,
|
|
165
|
+
skip_secret_scan=skip_secret_scan or bool(config.get("skip_secret_scan", False)),
|
|
166
|
+
language=resolved_language,
|
|
167
|
+
hook_timeout=hook_timeout if hook_timeout > 0 else int(config.get("hook_timeout", 120) or 120),
|
|
168
|
+
)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
handle_error(e, exit_program=True)
|
|
171
|
+
else:
|
|
172
|
+
# Determine if we should infer scope based on -s flag or always_include_scope setting
|
|
173
|
+
infer_scope = bool(scope or config.get("always_include_scope", False))
|
|
174
|
+
|
|
175
|
+
ctx.obj = {
|
|
176
|
+
"add_all": add_all,
|
|
177
|
+
"group": group,
|
|
178
|
+
"interactive": interactive,
|
|
179
|
+
"log_level": log_level,
|
|
180
|
+
"one_liner": one_liner,
|
|
181
|
+
"push": push,
|
|
182
|
+
"show_prompt": show_prompt,
|
|
183
|
+
"scope": infer_scope,
|
|
184
|
+
"quiet": quiet,
|
|
185
|
+
"yes": yes,
|
|
186
|
+
"hint": hint,
|
|
187
|
+
"model": model,
|
|
188
|
+
"language": language,
|
|
189
|
+
"version": version,
|
|
190
|
+
"dry_run": dry_run,
|
|
191
|
+
"message_only": message_only,
|
|
192
|
+
"verbose": verbose,
|
|
193
|
+
"no_verify": no_verify,
|
|
194
|
+
"skip_secret_scan": skip_secret_scan,
|
|
195
|
+
"no_verify_ssl": no_verify_ssl,
|
|
196
|
+
"hook_timeout": hook_timeout,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
cli.add_command(auth_cli)
|
|
201
|
+
cli.add_command(config_cli)
|
|
202
|
+
cli.add_command(diff_cli)
|
|
203
|
+
cli.add_command(init_cli)
|
|
204
|
+
cli.add_command(language_cli)
|
|
205
|
+
cli.add_command(model_cli)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@click.command(context_settings=language_cli.context_settings)
|
|
209
|
+
@click.pass_context
|
|
210
|
+
def lang(ctx: Any) -> None:
|
|
211
|
+
"""Set the language for commit messages interactively. (Alias for 'language')"""
|
|
212
|
+
ctx.forward(language_cli)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
cli.add_command(lang) # Add the lang alias
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
cli()
|
gac/commit_executor.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Commit execution logic for gac."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from gac.errors import GitError
|
|
10
|
+
from gac.git import get_staged_files, push_changes
|
|
11
|
+
from gac.workflow_utils import execute_commit
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommitExecutor:
|
|
18
|
+
"""Handles commit creation and related operations."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, dry_run: bool = False, quiet: bool = False, no_verify: bool = False, hook_timeout: int = 120):
|
|
21
|
+
self.dry_run = dry_run
|
|
22
|
+
self.quiet = quiet
|
|
23
|
+
self.no_verify = no_verify
|
|
24
|
+
self.hook_timeout = hook_timeout
|
|
25
|
+
|
|
26
|
+
def create_commit(self, commit_message: str) -> None:
|
|
27
|
+
"""Create a single commit with the given message."""
|
|
28
|
+
if self.dry_run:
|
|
29
|
+
console.print("[yellow]Dry run: Commit message generated but not applied[/yellow]")
|
|
30
|
+
console.print("Would commit with message:")
|
|
31
|
+
from rich.panel import Panel
|
|
32
|
+
|
|
33
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
34
|
+
staged_files = get_staged_files(existing_only=False)
|
|
35
|
+
console.print(f"Would commit {len(staged_files)} files")
|
|
36
|
+
logger.info(f"Would commit {len(staged_files)} files")
|
|
37
|
+
else:
|
|
38
|
+
execute_commit(commit_message, self.no_verify, self.hook_timeout)
|
|
39
|
+
|
|
40
|
+
def push_to_remote(self) -> None:
|
|
41
|
+
"""Push changes to remote repository."""
|
|
42
|
+
try:
|
|
43
|
+
if self.dry_run:
|
|
44
|
+
staged_files = get_staged_files(existing_only=False)
|
|
45
|
+
logger.info("Dry run: Would push changes")
|
|
46
|
+
logger.info(f"Would push {len(staged_files)} files")
|
|
47
|
+
console.print("[yellow]Dry run: Would push changes[/yellow]")
|
|
48
|
+
console.print(f"Would push {len(staged_files)} files")
|
|
49
|
+
sys.exit(0)
|
|
50
|
+
|
|
51
|
+
if push_changes():
|
|
52
|
+
logger.info("Changes pushed successfully")
|
|
53
|
+
if not self.quiet:
|
|
54
|
+
console.print("[green]Changes pushed successfully[/green]")
|
|
55
|
+
else:
|
|
56
|
+
console.print(
|
|
57
|
+
"[red]Failed to push changes. Check your remote configuration and network connection.[/red]"
|
|
58
|
+
)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
except (GitError, OSError) as e:
|
|
61
|
+
console.print(f"[red]Error pushing changes: {e}[/red]")
|
|
62
|
+
sys.exit(1)
|
gac/config.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Configuration loading for gac.
|
|
2
|
+
|
|
3
|
+
Handles environment variable and .gac.env file precedence for application settings.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TypedDict
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
from gac.constants import EnvDefaults, Logging
|
|
13
|
+
from gac.errors import ConfigError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GACConfig(TypedDict, total=False):
|
|
17
|
+
"""TypedDict for GAC configuration values.
|
|
18
|
+
|
|
19
|
+
Fields that can be None or omitted are marked with total=False.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model: str | None
|
|
23
|
+
temperature: float
|
|
24
|
+
max_output_tokens: int
|
|
25
|
+
max_retries: int
|
|
26
|
+
log_level: str
|
|
27
|
+
warning_limit_tokens: int
|
|
28
|
+
always_include_scope: bool
|
|
29
|
+
skip_secret_scan: bool
|
|
30
|
+
no_tiktoken: bool
|
|
31
|
+
no_verify_ssl: bool
|
|
32
|
+
verbose: bool
|
|
33
|
+
system_prompt_path: str | None
|
|
34
|
+
language: str | None
|
|
35
|
+
translate_prefixes: bool
|
|
36
|
+
rtl_confirmed: bool
|
|
37
|
+
hook_timeout: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_config(config: GACConfig) -> None:
|
|
41
|
+
"""Validate configuration values at load time.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: Configuration dictionary to validate
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ConfigError: If any configuration value is invalid
|
|
48
|
+
"""
|
|
49
|
+
# Validate temperature (0.0 to 2.0)
|
|
50
|
+
if config.get("temperature") is not None:
|
|
51
|
+
temp = config["temperature"]
|
|
52
|
+
if not isinstance(temp, (int, float)):
|
|
53
|
+
raise ConfigError(f"temperature must be a number, got {type(temp).__name__}")
|
|
54
|
+
if not 0.0 <= temp <= 2.0:
|
|
55
|
+
raise ConfigError(f"temperature must be between 0.0 and 2.0, got {temp}")
|
|
56
|
+
|
|
57
|
+
# Validate max_output_tokens (1 to 100000)
|
|
58
|
+
if config.get("max_output_tokens") is not None:
|
|
59
|
+
tokens = config["max_output_tokens"]
|
|
60
|
+
if not isinstance(tokens, int):
|
|
61
|
+
raise ConfigError(f"max_output_tokens must be an integer, got {type(tokens).__name__}")
|
|
62
|
+
if tokens < 1 or tokens > 100000:
|
|
63
|
+
raise ConfigError(f"max_output_tokens must be between 1 and 100000, got {tokens}")
|
|
64
|
+
|
|
65
|
+
# Validate max_retries (1 to 10)
|
|
66
|
+
if config.get("max_retries") is not None:
|
|
67
|
+
retries = config["max_retries"]
|
|
68
|
+
if not isinstance(retries, int):
|
|
69
|
+
raise ConfigError(f"max_retries must be an integer, got {type(retries).__name__}")
|
|
70
|
+
if retries < 1 or retries > 10:
|
|
71
|
+
raise ConfigError(f"max_retries must be between 1 and 10, got {retries}")
|
|
72
|
+
|
|
73
|
+
# Validate warning_limit_tokens (must be positive)
|
|
74
|
+
if config.get("warning_limit_tokens") is not None:
|
|
75
|
+
warning_limit = config["warning_limit_tokens"]
|
|
76
|
+
if not isinstance(warning_limit, int):
|
|
77
|
+
raise ConfigError(f"warning_limit_tokens must be an integer, got {type(warning_limit).__name__}")
|
|
78
|
+
if warning_limit < 1:
|
|
79
|
+
raise ConfigError(f"warning_limit_tokens must be positive, got {warning_limit}")
|
|
80
|
+
|
|
81
|
+
# Validate hook_timeout (must be positive)
|
|
82
|
+
if config.get("hook_timeout") is not None:
|
|
83
|
+
hook_timeout = config["hook_timeout"]
|
|
84
|
+
if not isinstance(hook_timeout, int):
|
|
85
|
+
raise ConfigError(f"hook_timeout must be an integer, got {type(hook_timeout).__name__}")
|
|
86
|
+
if hook_timeout < 1:
|
|
87
|
+
raise ConfigError(f"hook_timeout must be positive, got {hook_timeout}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_config() -> GACConfig:
|
|
91
|
+
"""Load configuration from $HOME/.gac.env, then ./.gac.env, then environment variables."""
|
|
92
|
+
user_config = Path.home() / ".gac.env"
|
|
93
|
+
if user_config.exists():
|
|
94
|
+
load_dotenv(user_config)
|
|
95
|
+
|
|
96
|
+
# Check for .gac.env in project directory
|
|
97
|
+
project_gac_env = Path(".gac.env")
|
|
98
|
+
|
|
99
|
+
if project_gac_env.exists():
|
|
100
|
+
load_dotenv(project_gac_env, override=True)
|
|
101
|
+
|
|
102
|
+
config: GACConfig = {
|
|
103
|
+
"model": os.getenv("GAC_MODEL"),
|
|
104
|
+
"temperature": float(os.getenv("GAC_TEMPERATURE", EnvDefaults.TEMPERATURE)),
|
|
105
|
+
"max_output_tokens": int(os.getenv("GAC_MAX_OUTPUT_TOKENS", EnvDefaults.MAX_OUTPUT_TOKENS)),
|
|
106
|
+
"max_retries": int(os.getenv("GAC_RETRIES", EnvDefaults.MAX_RETRIES)),
|
|
107
|
+
"log_level": os.getenv("GAC_LOG_LEVEL", Logging.DEFAULT_LEVEL),
|
|
108
|
+
"warning_limit_tokens": int(os.getenv("GAC_WARNING_LIMIT_TOKENS", EnvDefaults.WARNING_LIMIT_TOKENS)),
|
|
109
|
+
"always_include_scope": os.getenv("GAC_ALWAYS_INCLUDE_SCOPE", str(EnvDefaults.ALWAYS_INCLUDE_SCOPE)).lower()
|
|
110
|
+
in ("true", "1", "yes", "on"),
|
|
111
|
+
"skip_secret_scan": os.getenv("GAC_SKIP_SECRET_SCAN", str(EnvDefaults.SKIP_SECRET_SCAN)).lower()
|
|
112
|
+
in ("true", "1", "yes", "on"),
|
|
113
|
+
"no_tiktoken": os.getenv("GAC_NO_TIKTOKEN", str(EnvDefaults.NO_TIKTOKEN)).lower() in ("true", "1", "yes", "on"),
|
|
114
|
+
"no_verify_ssl": os.getenv("GAC_NO_VERIFY_SSL", str(EnvDefaults.NO_VERIFY_SSL)).lower()
|
|
115
|
+
in ("true", "1", "yes", "on"),
|
|
116
|
+
"verbose": os.getenv("GAC_VERBOSE", str(EnvDefaults.VERBOSE)).lower() in ("true", "1", "yes", "on"),
|
|
117
|
+
"system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
|
|
118
|
+
"language": os.getenv("GAC_LANGUAGE"),
|
|
119
|
+
"translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
|
|
120
|
+
"rtl_confirmed": os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on"),
|
|
121
|
+
"hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
validate_config(config)
|
|
125
|
+
return config
|
gac/config_cli.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""CLI for managing gac configuration in $HOME/.gac.env."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from dotenv import load_dotenv, set_key
|
|
8
|
+
|
|
9
|
+
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
def config() -> None:
|
|
14
|
+
"""Manage gac configuration."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@config.command()
|
|
19
|
+
def show() -> None:
|
|
20
|
+
"""Show all current config values."""
|
|
21
|
+
from dotenv import dotenv_values
|
|
22
|
+
|
|
23
|
+
project_env_path = Path(".gac.env")
|
|
24
|
+
user_exists = GAC_ENV_PATH.exists()
|
|
25
|
+
project_exists = project_env_path.exists()
|
|
26
|
+
|
|
27
|
+
if not user_exists and not project_exists:
|
|
28
|
+
click.echo("No $HOME/.gac.env found.")
|
|
29
|
+
click.echo("No project-level .gac.env found.")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
if user_exists:
|
|
33
|
+
click.echo(f"User config ({GAC_ENV_PATH}):")
|
|
34
|
+
user_config = dotenv_values(str(GAC_ENV_PATH))
|
|
35
|
+
for key, value in sorted(user_config.items()):
|
|
36
|
+
if value is not None:
|
|
37
|
+
if any(sensitive in key.lower() for sensitive in ["key", "token", "secret"]):
|
|
38
|
+
display_value = "***hidden***"
|
|
39
|
+
else:
|
|
40
|
+
display_value = value
|
|
41
|
+
click.echo(f" {key}={display_value}")
|
|
42
|
+
else:
|
|
43
|
+
click.echo("No $HOME/.gac.env found.")
|
|
44
|
+
|
|
45
|
+
if project_exists:
|
|
46
|
+
if user_exists:
|
|
47
|
+
click.echo("")
|
|
48
|
+
click.echo("Project config (./.gac.env):")
|
|
49
|
+
project_config = dotenv_values(str(project_env_path))
|
|
50
|
+
for key, value in sorted(project_config.items()):
|
|
51
|
+
if value is not None:
|
|
52
|
+
if any(sensitive in key.lower() for sensitive in ["key", "token", "secret"]):
|
|
53
|
+
display_value = "***hidden***"
|
|
54
|
+
else:
|
|
55
|
+
display_value = value
|
|
56
|
+
click.echo(f" {key}={display_value}")
|
|
57
|
+
click.echo("")
|
|
58
|
+
click.echo("Note: Project-level .gac.env overrides $HOME/.gac.env values for any duplicated variables.")
|
|
59
|
+
else:
|
|
60
|
+
click.echo("No project-level .gac.env found.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@config.command()
|
|
64
|
+
@click.argument("key")
|
|
65
|
+
@click.argument("value")
|
|
66
|
+
def set(key: str, value: str) -> None:
|
|
67
|
+
"""Set a config KEY to VALUE in $HOME/.gac.env."""
|
|
68
|
+
GAC_ENV_PATH.touch(exist_ok=True)
|
|
69
|
+
set_key(str(GAC_ENV_PATH), key, value)
|
|
70
|
+
click.echo(f"Set {key} in $HOME/.gac.env")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@config.command()
|
|
74
|
+
@click.argument("key")
|
|
75
|
+
def get(key: str) -> None:
|
|
76
|
+
"""Get a config value by KEY."""
|
|
77
|
+
load_dotenv(GAC_ENV_PATH, override=True)
|
|
78
|
+
value = os.getenv(key)
|
|
79
|
+
if value is None:
|
|
80
|
+
click.echo(f"{key} not set.")
|
|
81
|
+
else:
|
|
82
|
+
click.echo(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@config.command()
|
|
86
|
+
@click.argument("key")
|
|
87
|
+
def unset(key: str) -> None:
|
|
88
|
+
"""Remove a config KEY from $HOME/.gac.env."""
|
|
89
|
+
if not GAC_ENV_PATH.exists():
|
|
90
|
+
click.echo("No $HOME/.gac.env found.")
|
|
91
|
+
return
|
|
92
|
+
lines = GAC_ENV_PATH.read_text().splitlines()
|
|
93
|
+
new_lines = [line for line in lines if not line.strip().startswith(f"{key}=")]
|
|
94
|
+
GAC_ENV_PATH.write_text("\n".join(new_lines) + "\n")
|
|
95
|
+
click.echo(f"Unset {key} in $HOME/.gac.env")
|