clifcode 1.28.0

Open-source AI coding agent for your terminal — tool-calling loop, streaming, sessions, any provider
//! Configuration persistence and interactive setup.

use crate::ui;
use std::path::PathBuf;

pub struct ProviderInfo {
    pub name: &'static str,
    pub url: &'static str,
    pub default_model: &'static str,
    pub needs_key: bool,
}

pub const PROVIDERS: &[ProviderInfo] = &[
    ProviderInfo {
        name: "OpenRouter",
        url: "https://openrouter.ai/api/v1",
        default_model: "anthropic/claude-sonnet-4",
        needs_key: true,
    },
    ProviderInfo {
        name: "OpenAI",
        url: "https://api.openai.com/v1",
        default_model: "gpt-4o",
        needs_key: true,
    },
    ProviderInfo {
        name: "Anthropic",
        url: "https://api.anthropic.com/v1",
        default_model: "claude-sonnet-4-20250514",
        needs_key: true,
    },
    ProviderInfo {
        name: "Ollama (local)",
        url: "http://localhost:11434/v1",
        default_model: "qwen2.5-coder:7b",
        needs_key: false,
    },
    ProviderInfo {
        name: "Custom endpoint",
        url: "",
        default_model: "",
        needs_key: true,
    },
];

pub fn config_dir() -> PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    PathBuf::from(home).join(".clifcode")
}

pub fn config_path() -> PathBuf {
    config_dir().join("config.json")
}

pub fn load_config() -> serde_json::Value {
    let path = config_path();
    if path.exists() {
        let text = std::fs::read_to_string(&path).unwrap_or_default();
        serde_json::from_str(&text).unwrap_or_else(|_| serde_json::json!({}))
    } else {
        serde_json::json!({})
    }
}

pub fn save_config(config: &serde_json::Value) {
    let dir = config_dir();
    let _ = std::fs::create_dir_all(&dir);
    let text = serde_json::to_string_pretty(config).unwrap_or_default();
    let _ = std::fs::write(config_path(), text);
}

pub fn saved_api_key() -> Option<String> {
    load_config()
        .get("api_key")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

pub fn saved_api_model() -> Option<String> {
    load_config()
        .get("api_model")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

pub fn saved_api_url() -> Option<String> {
    load_config()
        .get("api_url")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

/// Interactive first-run setup. Returns (key, url, model) or None on cancel.
pub fn interactive_setup() -> Option<(String, String, String)> {
    fn fetch_openai_models(api_key: &str) -> Vec<String> {
        let resp = ureq::get("https://api.openai.com/v1/models")
            .set("Authorization", &format!("Bearer {api_key}"))
            .call();

        match resp {
            Ok(r) => {
                if let Ok(json) = r.into_json::<serde_json::Value>() {
                    let empty = vec![];
                    let mut models: Vec<String> = json["data"]
                        .as_array()
                        .unwrap_or(&empty)
                        .iter()
                        .filter_map(|m| m["id"].as_str().map(|s| s.to_string()))
                        .collect();
                    models.sort();
                    models
                } else {
                    vec![]
                }
            }
            Err(_) => vec![],
        }
    }

    println!();
    println!("  {}{}Setup{}", ui::BOLD, ui::YELLOW, ui::RESET);
    println!(
        "  {}─────────────────────────────────────────{}",
        ui::DIM, ui::RESET
    );
    println!();

    let names: Vec<&str> = PROVIDERS.iter().map(|p| p.name).collect();
    let choice = ui::select_menu("Choose a provider:", &names)?;

    let provider = &PROVIDERS[choice];

    let url = if provider.url.is_empty() {
        let u = ui::prompt_input("  API base URL:");
        if u.is_empty() {
            println!("  {}No URL — skipping.{}", ui::DIM, ui::RESET);
            return None;
        }
        u
    } else {
        provider.url.to_string()
    };

    let key = if provider.needs_key {
        let saved_provider = load_config()
            .get("provider")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());
        let k = if saved_provider.as_deref() == Some(provider.name) {
            saved_api_key().unwrap_or_else(|| ui::prompt_input("  API key:"))
        } else {
            ui::prompt_input("  API key:")
        };
        if k.is_empty() {
            println!("  {}No key — skipping.{}", ui::DIM, ui::RESET);
            return None;
        }
        k
    } else {
        String::new()
    };

    let model = if provider.name == "OpenAI" {
        let available_models = fetch_openai_models(&key);
        if available_models.is_empty() {
            ui::print_dim("  (could not fetch models — using default)");
            provider.default_model.to_string()
        } else {
            let refs: Vec<&str> = available_models.iter().map(|s| s.as_str()).collect();
            let model_choice = ui::select_menu("Choose a model:", &refs)?;
            available_models[model_choice].clone()
        }
    } else if provider.default_model.is_empty() {
        ui::prompt_input("  Model name:")
    } else {
        ui::prompt_input_default("  Model:", provider.default_model)
    };

    if model.is_empty() {
        println!("  {}No model — skipping.{}", ui::DIM, ui::RESET);
        return None;
    }

    let mut config = load_config();
    config["provider"] = serde_json::json!(provider.name);
    config["api_key"] = serde_json::json!(key);
    config["api_url"] = serde_json::json!(url);
    config["api_model"] = serde_json::json!(model);
    save_config(&config);

    println!();
    println!(
        "  {}Saved to ~/.clifcode/config.json{}",
        ui::GREEN, ui::RESET
    );

    Some((key, url, model))
}