gigi-cli 1.0.1

Gigi — A Claude Code-like AI coding assistant CLI in Rust
use std::path::PathBuf;

// =============================================================================
// Application Configuration
//
// Loaded from environment variables with sensible defaults.
// Supports multiple API providers and local model runtimes.
// =============================================================================

#[derive(Debug, Clone)]
pub struct AppConfig {
    // --- Provider API keys ---
    pub anthropic_api_key: Option<String>,
    pub groq_api_key: Option<String>,
    pub google_api_key: Option<String>,

    // --- Default provider selection ---
    /// Which provider to use by default: "anthropic", "groq", "google",
    /// "ollama", "lm_studio", "llama_cpp", "custom"
    pub default_provider: String,

    /// Default model for the selected provider (empty = use provider default)
    pub default_model: Option<String>,

    // --- Local model configuration ---
    pub ollama_url: String,
    pub lm_studio_url: String,
    pub llama_cpp_url: String,
    pub custom_api_url: String,
    pub custom_api_key: Option<String>,

    // --- Tool configuration ---
    pub tech_query_url: String,

    // --- Session & storage ---
    pub session_dir: PathBuf,

    // --- Agent loop ---
    /// Maximum tool-use loops before forcing a stop (safety limit).
    pub max_tool_turns: usize,

    /// Maximum tokens per completion.
    pub max_tokens: u32,
}

impl AppConfig {
    /// Prompt the user for an API key if it's missing for the active provider,
    /// and save it persistently.
    pub fn prompt_for_key_if_missing(&mut self, provider: &str) -> anyhow::Result<()> {
        let is_missing = match provider.to_lowercase().as_str() {
            "anthropic" => self.anthropic_api_key.is_none(),
            "groq" => self.groq_api_key.is_none(),
            "google" => self.google_api_key.is_none(),
            "custom" => self.custom_api_key.is_none(),
            _ => false, // local providers don't need keys
        };

        if is_missing {
            use colored::*;
            println!("{}", format!("No API key found for provider: {}", provider).yellow());
            print!("Please enter your {} API Key: ", provider);
            use std::io::Write;
            let _ = std::io::stdout().flush();
            
            let mut input = String::new();
            std::io::stdin().read_line(&mut input)?;
            let key = input.trim().to_string();
            
            if key.is_empty() {
                anyhow::bail!("API key cannot be empty.");
            }

            let mut store = crate::config_store::ConfigStore::load();
            match provider.to_lowercase().as_str() {
                "anthropic" => {
                    self.anthropic_api_key = Some(key.clone());
                    store.anthropic_api_key = Some(key);
                }
                "groq" => {
                    self.groq_api_key = Some(key.clone());
                    store.groq_api_key = Some(key);
                }
                "google" => {
                    self.google_api_key = Some(key.clone());
                    store.google_api_key = Some(key);
                }
                "custom" => {
                    self.custom_api_key = Some(key.clone());
                    store.custom_api_key = Some(key);
                }
                _ => {}
            }
            
            store.save()?;
            println!("{}", "✓ API Key saved successfully to ~/.gigi_config.json\n".green());
        }

        Ok(())
    }

    /// Load configuration from environment variables.
    pub fn from_env() -> Self {
        use std::io::IsTerminal;
        
        let mut store = crate::config_store::ConfigStore::load();
        if !crate::config_store::ConfigStore::exists() && std::io::stdin().is_terminal() {
            if let Ok(new_store) = crate::config_store::ConfigStore::run_setup_wizard() {
                store = new_store;
            }
        }

        Self {
            // API keys
            anthropic_api_key: std::env::var("ANTHROPIC_API_KEY")
                .ok()
                .or_else(|| store.anthropic_api_key.clone()),
            groq_api_key: std::env::var("GROQ_API_KEY")
                .ok()
                .or_else(|| store.groq_api_key.clone()),
            google_api_key: std::env::var("GOOGLE_API_KEY")
                .or_else(|_| std::env::var("GEMINI_API_KEY"))
                .ok()
                .or_else(|| store.google_api_key.clone()),

            // Default provider
            default_provider: std::env::var("MYAPP_PROVIDER")
                .ok()
                .or_else(|| store.default_provider.clone())
                .unwrap_or_else(|| "anthropic".to_string()),
            default_model: std::env::var("MYAPP_MODEL")
                .ok()
                .or_else(|| store.default_model.clone()),

            // Local model URLs
            ollama_url: std::env::var("OLLAMA_URL")
                .ok()
                .or_else(|| store.ollama_url.clone())
                .unwrap_or_else(|| "http://localhost:11434".to_string()),
            lm_studio_url: std::env::var("LM_STUDIO_URL")
                .ok()
                .or_else(|| store.lm_studio_url.clone())
                .unwrap_or_else(|| "http://localhost:1234".to_string()),
            llama_cpp_url: std::env::var("LLAMA_CPP_URL")
                .ok()
                .or_else(|| store.llama_cpp_url.clone())
                .unwrap_or_else(|| "http://localhost:8080".to_string()),
            custom_api_url: std::env::var("CUSTOM_API_URL")
                .ok()
                .or_else(|| store.custom_api_url.clone())
                .unwrap_or_else(|| "http://localhost:8000".to_string()),
            custom_api_key: std::env::var("CUSTOM_API_KEY")
                .ok()
                .or_else(|| store.custom_api_key.clone()),

            // Tools
            tech_query_url: std::env::var("TECH_QUERY_URL")
                .ok()
                .or_else(|| store.tech_query_url.clone())
                .unwrap_or_else(|| "http://localhost:5000/search".to_string()),

            // Session
            session_dir: std::env::var("MYAPP_SESSION_DIR")
                .map(PathBuf::from)
                .unwrap_or_else(|_| PathBuf::from(".sessions")),

            // Agent loop
            max_tool_turns: std::env::var("MYAPP_MAX_TOOL_TURNS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(25),
            max_tokens: std::env::var("MYAPP_MAX_TOKENS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(8192),
        }
    }

    /// Pretty-print the current configuration (redacting API keys).
    pub fn display_summary(&self) -> String {
        let redact = |key: &Option<String>| -> String {
            match key {
                Some(k) if k.len() > 8 => format!("{}...{}", &k[..4], &k[k.len() - 4..]),
                Some(_) => "****".to_string(),
                None => "(not set)".to_string(),
            }
        };

        format!(
            "Configuration:\n\
             ├── Provider: {}\n\
             ├── Model: {}\n\
             ├── Anthropic key: {}\n\
             ├── Groq key: {}\n\
             ├── Google key: {}\n\
             ├── Ollama URL: {}\n\
             ├── LM Studio URL: {}\n\
             ├── llama.cpp URL: {}\n\
             ├── Custom URL: {}\n\
             ├── Tech Query URL: {}\n\
             ├── Session dir: {}\n\
             ├── Max tool turns: {}\n\
             └── Max tokens: {}",
            self.default_provider,
            self.default_model.as_deref().unwrap_or("(provider default)"),
            redact(&self.anthropic_api_key),
            redact(&self.groq_api_key),
            redact(&self.google_api_key),
            self.ollama_url,
            self.lm_studio_url,
            self.llama_cpp_url,
            self.custom_api_url,
            self.tech_query_url,
            self.session_dir.display(),
            self.max_tool_turns,
            self.max_tokens,
        )
    }
}