magi-rs 0.6.0

Magi Agent: a terminal AI assistant in Rust with sandboxed tool execution, OAuth login, and encrypted local memory (Argon2 + AES-256-GCM-SIV + Reed-Solomon FEC).
// Author: Julian Bolivar
// Version: 1.0.0
// Date: 2026-06-09

//! Built-in default backend profile (Ollama-first). MANUAL MAINTENANCE: these
//! `:cloud` tags reflect the Ollama catalog at release time and rot as it changes
//! (e.g. `qwen3-max` never existed; `qwen3.6` appeared). Refresh per release; users
//! override via `magi.toml`/env. All default literals live HERE, in one place.

use std::path::{Path, PathBuf};

/// Default provider when no `magi.toml`/env is present (RF-1).
pub const DEFAULT_PROVIDER: &str = "openai";
/// Default OpenAI-compatible base URL — local Ollama (RF-2).
pub const DEFAULT_OPENAI_BASE_URL: &str = "http://localhost:11434/v1";
/// Default principal model on the openai path (RF-3).
pub const DEFAULT_OPENAI_MODEL: &str = "kimi-k2.6:cloud";
/// Default MAGI trio (openai path only, RF-4). Lineages: Alibaba / OpenAI / DeepSeek.
pub const DEFAULT_MAGI_MELCHIOR: &str = "qwen3.5:397b-cloud";
pub const DEFAULT_MAGI_BALTHASAR: &str = "gpt-oss:120b-cloud";
pub const DEFAULT_MAGI_CASPAR: &str = "deepseek-v4-pro:cloud";
/// Default Anthropic model on the opt-in path (RF-5). Was `main.rs::DEFAULT_MODEL`.
pub const DEFAULT_ANTHROPIC_MODEL: &str = "claude-sonnet-4-6";

/// Startup notice shown when no `magi.toml` is present (RF-9). Built by
/// interpolating the default constants (RF-8 DRY) so it tracks any constant edit.
pub fn no_config_notice() -> String {
    format!(
        "No magi.toml — using Ollama defaults ({base}, {model}, \
         Melchior: {mel}, Balthasar: {bal}, Caspar: {cas}). Copy \
         magi.toml.example to customize, or set provider=\"anthropic\" \
         for Anthropic.",
        base = DEFAULT_OPENAI_BASE_URL,
        model = DEFAULT_OPENAI_MODEL,
        mel = DEFAULT_MAGI_MELCHIOR,
        bal = DEFAULT_MAGI_BALTHASAR,
        cas = DEFAULT_MAGI_CASPAR,
    )
}

/// Renders a default `magi.toml` (the active Ollama-first profile) from the
/// constants (RF-11, DRY). Editable starting point; comments point at the
/// Anthropic opt-in.
pub fn render_default_magi_toml() -> String {
    format!(
        "# Generated by magi-rs --init-config / /init-config — built-in Ollama-first defaults.\n\
         # Edit to customize, or set provider = \"anthropic\" to use Anthropic instead.\n\
         provider = \"{provider}\"\n\n\
         [openai]\n\
         base_url = \"{base}\"\n\
         model    = \"{model}\"\n\n\
         [magi]\n\
         melchior_model  = \"{mel}\"\n\
         balthasar_model = \"{bal}\"\n\
         caspar_model    = \"{cas}\"\n",
        provider = DEFAULT_PROVIDER,
        base = DEFAULT_OPENAI_BASE_URL,
        model = DEFAULT_OPENAI_MODEL,
        mel = DEFAULT_MAGI_MELCHIOR,
        bal = DEFAULT_MAGI_BALTHASAR,
        cas = DEFAULT_MAGI_CASPAR,
    )
}

/// Writes a default `magi.toml` into `dir`. **Refuses to overwrite** an existing
/// file (RF-10) — returns `Err` and leaves it untouched. On success returns the
/// path written.
pub fn write_default_config(dir: &Path) -> anyhow::Result<PathBuf> {
    let path = dir.join("magi.toml");
    match std::fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&path)
    {
        Ok(mut f) => {
            use std::io::Write;
            f.write_all(render_default_magi_toml().as_bytes())?;
            Ok(path)
        }
        Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => anyhow::bail!(
            "{} already exists — refusing to overwrite (edit it manually or delete it first)",
            path.display()
        ),
        Err(e) => Err(anyhow::anyhow!("failed to write {}: {e}", path.display())),
    }
}

/// Whether to emit the no-config Ollama-defaults startup notice (RF-9): only when
/// the resolved backend is the openai (Ollama) default AND no `magi.toml` exists.
/// Prevents a misleading "using Ollama defaults" notice under `MAGI_PROVIDER=anthropic`
/// with no file.
pub fn should_emit_default_notice(provider_kind: &str, magi_toml_exists: bool) -> bool {
    provider_kind == "openai" && !magi_toml_exists
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_constants_are_the_ollama_first_profile() {
        assert_eq!(DEFAULT_PROVIDER, "openai");
        assert_eq!(DEFAULT_OPENAI_BASE_URL, "http://localhost:11434/v1");
        assert_eq!(DEFAULT_OPENAI_MODEL, "kimi-k2.6:cloud");
        assert_eq!(DEFAULT_MAGI_MELCHIOR, "qwen3.5:397b-cloud");
        assert_eq!(DEFAULT_MAGI_BALTHASAR, "gpt-oss:120b-cloud");
        assert_eq!(DEFAULT_MAGI_CASPAR, "deepseek-v4-pro:cloud");
        assert_eq!(DEFAULT_ANTHROPIC_MODEL, "claude-sonnet-4-6");
    }

    #[test]
    fn test_should_emit_default_notice_only_for_openai_without_file() {
        // Notice is for the no-config Ollama default ONLY: openai backend + no magi.toml.
        assert!(should_emit_default_notice("openai", false));
        assert!(!should_emit_default_notice("openai", true)); // file present
        assert!(!should_emit_default_notice("anthropic", false)); // env opt-in to anthropic, no file
        assert!(!should_emit_default_notice("anthropic", true));
    }

    #[test]
    fn test_no_config_notice_interpolates_all_defaults() {
        // S-9: the notice is built from the constants (DRY), not hardcoded strings.
        let n = no_config_notice();
        assert!(n.contains(DEFAULT_OPENAI_BASE_URL));
        assert!(n.contains(DEFAULT_OPENAI_MODEL));
        assert!(n.contains(DEFAULT_MAGI_MELCHIOR));
        assert!(n.contains(DEFAULT_MAGI_BALTHASAR));
        assert!(n.contains(DEFAULT_MAGI_CASPAR));
    }

    #[test]
    fn test_render_default_magi_toml_interpolates_and_parses() {
        // S-12: rendered from constants (DRY) and valid TOML with provider="openai".
        let s = render_default_magi_toml();
        assert!(s.contains(DEFAULT_OPENAI_BASE_URL));
        assert!(s.contains(DEFAULT_OPENAI_MODEL));
        assert!(s.contains(DEFAULT_MAGI_MELCHIOR));
        assert!(s.contains(DEFAULT_MAGI_BALTHASAR));
        assert!(s.contains(DEFAULT_MAGI_CASPAR));
        let parsed = crate::config::MagiConfig::from_toml_str(&s).unwrap();
        assert_eq!(parsed.provider.as_deref(), Some("openai"));
        assert_eq!(
            parsed.magi.melchior_model.as_deref(),
            Some(DEFAULT_MAGI_MELCHIOR)
        );
    }

    #[test]
    fn test_write_default_config_writes_when_absent() {
        // S-10
        let dir = tempfile::tempdir().unwrap();
        let path = write_default_config(dir.path()).unwrap();
        assert_eq!(path, dir.path().join("magi.toml"));
        assert!(path.exists());
        let body = std::fs::read_to_string(&path).unwrap();
        assert!(body.contains(DEFAULT_OPENAI_MODEL));
    }

    #[test]
    fn test_write_default_config_refuses_to_overwrite() {
        // S-11
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("magi.toml"), "provider = \"anthropic\"").unwrap();
        let err = write_default_config(dir.path()).unwrap_err();
        assert!(err.to_string().contains("magi.toml"));
        let body = std::fs::read_to_string(dir.path().join("magi.toml")).unwrap();
        assert_eq!(body, "provider = \"anthropic\"");
    }
}