cortex-agent 0.2.1

Self-learning AI agent with persistent memory, tools, plugins, and a beautiful terminal UI
use serde::{Deserialize, Serialize};
use std::path::Path;

/// Runtime session state — last used provider/model, saved between runs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionState {
    pub last_provider: String,
    pub last_model: String,
    pub last_session_turns: u64,
    pub last_session_tokens: u64,
    pub last_session_duration_secs: u64,
}

impl Default for SessionState {
    fn default() -> Self {
        Self {
            last_provider: String::new(),
            last_model: String::new(),
            last_session_turns: 0,
            last_session_tokens: 0,
            last_session_duration_secs: 0,
        }
    }
}

fn session_path() -> String {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    format!("{}/.cortex/session.json", home)
}

/// Save session state to `~/.cortex/session.json`.
pub fn save_session_state(state: &SessionState) {
    let path = session_path();
    if let Some(parent) = Path::new(&path).parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    if let Ok(json) = serde_json::to_string_pretty(state) {
        let _ = std::fs::write(&path, &json);
    }
}

/// Load session state from `~/.cortex/session.json`.
pub fn load_session_state() -> SessionState {
    let path = session_path();
    std::fs::read_to_string(&path)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default()
}

/// Ping a provider's /v1/models endpoint to verify connectivity.
/// Returns (success, model_count_or_error_message).
pub async fn health_check(base_url: &str, api_key: &str) -> (bool, String) {
    let url = format!("{}/models", base_url.trim_end_matches('/'));
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(5))
        .build()
        .unwrap_or_default();

    if api_key.is_empty() || api_key.contains("${") {
        return (false, "API key not configured".into());
    }

    match client
        .get(&url)
        .header("Authorization", format!("Bearer {}", api_key))
        .header("Content-Type", "application/json")
        .send()
        .await
    {
        Ok(resp) if resp.status().is_success() => {
            match resp.json::<serde_json::Value>().await {
                Ok(data) => {
                    let count = data["data"].as_array().map(|a| a.len()).unwrap_or(0);
                    (true, format!("{} models available", count))
                }
                Err(_) => (false, "Failed to parse model list".into()),
            }
        }
        Ok(resp) => (false, format!("HTTP {}", resp.status())),
        Err(e) => (false, format!("Connection failed: {}", e)),
    }
}