Clover/config/settings.py
opencode 1207f2c9cf fix: remove hardcoded LAN IP from default base_url (issue #5)
- Replace hardcoded 192.168.8.223:11434 with error placeholder
- Fail loudly if CLOVER_BASE_URL not set in .env
- Add helpful error message in validate_config()
2026-07-05 07:01:34 +00:00

247 lines
8.1 KiB
Python

"""
Configuration settings handler for Clover - A terminal assistant for AI-powered project management
"""
import json
import os
from pathlib import Path
from dotenv import load_dotenv
def load_config():
"""
Load configuration from .env file and environment variables, with fallback defaults.
Returns:
dict: Configuration dictionary with loaded or default values
"""
# Load .env file if it exists
env_file = Path(__file__).parent.parent / ".env"
if env_file.exists():
load_dotenv(env_file)
else:
# Try to load from current directory as fallback
load_dotenv()
config = {
"timeout": int(os.getenv("CLOVER_TIMEOUT", "300")),
"threads": int(os.getenv("CLOVER_THREADS", "5")),
"model": os.getenv("CLOVER_MODEL", "qwen3-coder:30b"),
"api_key": os.getenv("CLOVER_API_KEY"),
"base_url": os.getenv(
"CLOVER_BASE_URL", "error-set-CLOVER_BASE_URL-in-.env-file"
),
"debug": os.getenv("CLOVER_DEBUG", "false").lower() == "true",
"verbose": os.getenv("CLOVER_VERBOSE", "false").lower() == "true",
"cache_enabled": os.getenv("CLOVER_CACHE_ENABLED", "true").lower() == "true",
"cache_size": int(os.getenv("CLOVER_CACHE_SIZE", "1000")),
"allow_command_execution": os.getenv(
"CLOVER_ALLOW_COMMAND_EXECUTION", "true"
).lower()
== "true",
"require_confirmation": os.getenv("CLOVER_REQUIRE_CONFIRMATION", "true").lower()
== "true",
"project_root": os.getenv("CLOVER_PROJECT_ROOT", "."),
"summary_file": os.getenv("CLOVER_SUMMARY_FILE", "clover.md"),
"structure_file": os.getenv("CLOVER_STRUCTURE_FILE", "structure.md"),
}
# Debug output if enabled
if config["debug"]:
print(f"Debug: Loaded configuration:")
for key, value in config.items():
if key == "api_key" and value:
print(f" {key}: {'*' * len(str(value))}")
else:
print(f" {key}: {value}")
return config
def save_config(config):
"""
Save configuration to .env file.
Args:
config (dict): Configuration dictionary to save
"""
try:
env_file = Path(__file__).parent.parent / ".env"
# Read existing .env file if it exists
existing_lines = []
if env_file.exists():
with open(env_file, "r") as f:
existing_lines = f.readlines()
# Update or add configuration values
updated_lines = []
config_keys_set = set()
for line in existing_lines:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key = line.split("=")[0].strip()
# Check if this is a config key we want to update
config_key = None
if key == "CLOVER_TIMEOUT":
config_key = "timeout"
elif key == "CLOVER_THREADS":
config_key = "threads"
elif key == "CLOVER_MODEL":
config_key = "model"
elif key == "CLOVER_API_KEY":
config_key = "api_key"
elif key == "CLOVER_BASE_URL":
config_key = "base_url"
elif key == "CLOVER_DEBUG":
config_key = "debug"
elif key == "CLOVER_VERBOSE":
config_key = "verbose"
elif key == "CLOVER_CACHE_ENABLED":
config_key = "cache_enabled"
elif key == "CLOVER_CACHE_SIZE":
config_key = "cache_size"
elif key == "CLOVER_ALLOW_COMMAND_EXECUTION":
config_key = "allow_command_execution"
elif key == "CLOVER_REQUIRE_CONFIRMATION":
config_key = "require_confirmation"
elif key == "CLOVER_PROJECT_ROOT":
config_key = "project_root"
elif key == "CLOVER_SUMMARY_FILE":
config_key = "summary_file"
elif key == "CLOVER_STRUCTURE_FILE":
config_key = "structure_file"
if config_key and config_key in config:
# Update with new value
value = config[config_key]
if isinstance(value, bool):
value = str(value).lower()
elif value is None:
value = ""
updated_lines.append(f"{key}={value}\n")
config_keys_set.add(config_key)
else:
# Keep existing line
updated_lines.append(line + "\n")
else:
# Keep comments and empty lines
updated_lines.append(line + "\n")
# Add any new configuration keys that weren't in the file
new_configs = {
"timeout": "CLOVER_TIMEOUT",
"threads": "CLOVER_THREADS",
"model": "CLOVER_MODEL",
"api_key": "CLOVER_API_KEY",
"base_url": "CLOVER_BASE_URL",
"debug": "CLOVER_DEBUG",
"verbose": "CLOVER_VERBOSE",
"cache_enabled": "CLOVER_CACHE_ENABLED",
"cache_size": "CLOVER_CACHE_SIZE",
"allow_command_execution": "CLOVER_ALLOW_COMMAND_EXECUTION",
"require_confirmation": "CLOVER_REQUIRE_CONFIRMATION",
"project_root": "CLOVER_PROJECT_ROOT",
"summary_file": "CLOVER_SUMMARY_FILE",
"structure_file": "CLOVER_STRUCTURE_FILE",
}
for config_key, env_key in new_configs.items():
if config_key not in config_keys_set and config_key in config:
value = config[config_key]
if isinstance(value, bool):
value = str(value).lower()
elif value is None:
value = ""
updated_lines.append(f"{env_key}={value}\n")
# Write updated .env file
with open(env_file, "w") as f:
f.writelines(updated_lines)
print(f"Configuration saved to {env_file}")
except Exception as e:
print(f"Error saving configuration: {e}")
def get_config_value(key, default=None):
"""
Get a specific configuration value.
Args:
key (str): Configuration key
default: Default value if key not found
Returns:
Configuration value or default
"""
config = load_config()
return config.get(key, default)
def set_config_value(key, value):
"""
Set a specific configuration value and save to file.
Args:
key (str): Configuration key
value: Value to set
"""
config = load_config()
config[key] = value
save_config(config)
def validate_config():
"""
Validate configuration and return any issues found.
Returns:
list: List of validation issues (empty if valid)
"""
config = load_config()
issues = []
# Check required settings
base_url = config.get("base_url", "")
if not base_url or base_url.startswith("error-set-"):
issues.append(
"CLOVER_BASE_URL is not set. Set it in your .env file or environment. "
"Example: CLOVER_BASE_URL=http://localhost:11434"
)
# Check numeric values
try:
timeout = int(config.get("timeout", 300))
if timeout <= 0:
issues.append("timeout must be positive")
except (ValueError, TypeError):
issues.append("timeout must be a valid integer")
try:
threads = int(config.get("threads", 5))
if threads <= 0 or threads > 50:
issues.append("threads must be between 1 and 50")
except (ValueError, TypeError):
issues.append("threads must be a valid integer")
# Check cache size
try:
cache_size = int(config.get("cache_size", 1000))
if cache_size < 0:
issues.append("cache_size must be non-negative")
except (ValueError, TypeError):
issues.append("cache_size must be a valid integer")
return issues
# For backwards compatibility
def load_settings():
"""Legacy function name - use load_config() instead"""
return load_config()