""" 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()