clawgarden-cli 0.7.3

ClawGarden CLI - Multi-bot/multi-agent Garden management tool
//! Provider manifest types and loader
//!
//! Provider definitions are stored as JSON files in the `providers/` directory
//! and bundled at compile time via `include_str!`. This follows the OpenClaw
//! plugin pattern where each provider is a self-contained manifest.

use std::sync::OnceLock;

use serde::Deserialize;

// ── Manifest types ───────────────────────────────────────────────────────────

/// Provider manifest loaded from JSON
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct ProviderManifest {
    /// Unique provider identifier (e.g. "anthropic", "openrouter")
    pub id: String,
    /// Human-readable name
    pub label: String,
    /// Display icon (emoji)
    pub icon: String,
    /// Documentation path
    #[serde(default)]
    pub docs_path: Option<String>,
    /// Internal hook aliases
    #[serde(default)]
    pub hook_aliases: Vec<String>,
    /// Provider-related env vars
    #[serde(default)]
    pub env_vars: Vec<String>,
    /// Default model ID
    #[serde(default)]
    pub default_model: Option<String>,
    /// Static model list (used as fallback for catalog providers)
    #[serde(default)]
    pub models: Vec<String>,
    /// Optional URL for dynamic model catalog fetching (OpenRouter pattern)
    #[serde(default)]
    pub catalog_url: Option<String>,
    /// Available authentication methods
    #[serde(default)]
    pub auth: Vec<ManifestAuth>,
}

/// Authentication method definition
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct ManifestAuth {
    /// Method identifier (e.g. "api-key", "api-global")
    pub id: String,
    /// Human-readable label
    pub label: String,
    /// Hint text for the user
    #[serde(default)]
    pub hint: Option<String>,
    /// Authentication kind
    pub kind: AuthKind,
}

/// Authentication method kind
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthKind {
    ApiKey,
    ApiGlobal,
    EnvVar,
    Custom,
}

// ── Manifest file registry ───────────────────────────────────────────────────
//
// To add a new provider: add ONE line to PROVIDER_MANIFESTS below.
//
// Path from src/providers/ → ../../../../ → project root → providers/
const PROVIDER_MANIFESTS: &[(&str, &str)] = &[
    (
        "anthropic",
        include_str!("../../providers/anthropic.json"),
    ),
    ("openai", include_str!("../../providers/openai.json")),
    ("google", include_str!("../../providers/google.json")),
    (
        "deepseek",
        include_str!("../../providers/deepseek.json"),
    ),
    (
        "openrouter",
        include_str!("../../providers/openrouter.json"),
    ),
    ("azure", include_str!("../../providers/azure.json")),
    ("groq", include_str!("../../providers/groq.json")),
    (
        "together",
        include_str!("../../providers/together.json"),
    ),
    (
        "minimax",
        include_str!("../../providers/minimax.json"),
    ),
    ("kimi", include_str!("../../providers/kimi.json")),
    ("zai", include_str!("../../providers/zai.json")),
    // Add new providers here — e.g. ("ollama", include_str!("../../providers/ollama.json"))
];

// ── Manifest loader ──────────────────────────────────────────────────────────

static MANIFESTS: OnceLock<Vec<ProviderManifest>> = OnceLock::new();

/// Load all bundled provider manifests (cached after first call).
pub fn load_manifests() -> &'static Vec<ProviderManifest> {
    MANIFESTS.get_or_init(|| {
        let mut results = Vec::new();
        for &(id, json) in PROVIDER_MANIFESTS {
            match serde_json::from_str::<ProviderManifest>(json) {
                Ok(m) => results.push(m),
                Err(e) => eprintln!("Warning: failed to parse '{}': {}", id, e),
            }
        }
        results
    })
}

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

    const TEST_JSON: &str = r#"{
        "id": "test",
        "label": "Test",
        "icon": "✅",
        "models": ["model-a"],
        "auth": [{"id": "key", "label": "API Key", "kind": "api_key"}]
    }"#;

    #[test]
    fn test_parse_manifest() {
        let m: ProviderManifest = serde_json::from_str(TEST_JSON).unwrap();
        assert_eq!(m.id, "test");
        assert_eq!(m.models.len(), 1);
    }
}