doum_cli/cli/
secret.rs

1use crate::llm::{AnthropicSecret, OpenAISecret, Provider, load_presets, verify_client};
2use crate::system::{LLMConfig, SecretManager};
3use anyhow::{Context, Result};
4use cliclack::{input, password, select, spinner};
5
6pub async fn handle_secret_command() -> Result<()> {
7    cliclack::intro("🔐 Configure LLM Provider Secret")?;
8
9    // Step 1: Select provider
10    let providers = Provider::all();
11    let provider_items: Vec<_> = providers
12        .iter()
13        .map(|p| (p.as_str(), p.as_display(), ""))
14        .collect();
15
16    let provider_str = select("Select provider to configure")
17        .items(&provider_items)
18        .interact()
19        .context("Selection failed")?;
20
21    let provider: Provider = provider_str.parse()?;
22
23    // Step 2: Input secrets based on provider
24    match provider {
25        Provider::OpenAI => {
26            let api_key = password("OpenAI API Key (required)")
27                .interact()
28                .context("Password input failed")?;
29
30            let organization: String = input("Organization ID (optional, press Enter to skip)")
31                .placeholder("Press Enter to skip")
32                .required(false)
33                .interact()
34                .context("Input failed")?;
35
36            let project: String = input("Project ID (optional, press Enter to skip)")
37                .placeholder("Press Enter to skip")
38                .required(false)
39                .interact()
40                .context("Input failed")?;
41
42            let secret = OpenAISecret {
43                api_key: api_key.trim().to_string(),
44                organization: if organization.trim().is_empty() {
45                    None
46                } else {
47                    Some(organization.trim().to_string())
48                },
49                project: if project.trim().is_empty() {
50                    None
51                } else {
52                    Some(project.trim().to_string())
53                },
54            };
55            SecretManager::save(&provider, &secret)?;
56        }
57        Provider::Anthropic => {
58            let api_key = password("Anthropic API Key (required)")
59                .interact()
60                .context("Password input failed")?;
61
62            let secret = AnthropicSecret {
63                api_key: api_key.trim().to_string(),
64            };
65            SecretManager::save(&provider, &secret)?;
66        }
67    }
68
69    // Step 3: Get first model and verification
70    let first_model = load_presets(&provider)
71        .first()
72        .map(|m| m.id.clone())
73        .context("No preset models available for this provider")?;
74
75    let llm_config = LLMConfig {
76        provider,
77        model: first_model.clone(),
78        timeout: 30,
79        use_thinking: false,
80        use_web_search: false,
81    };
82
83    let sp = spinner();
84    sp.start("Verifying API key...");
85
86    match verify_client(&llm_config).await {
87        Ok(true) => {
88            sp.stop(format!(
89                "{} secrets saved and verified successfully",
90                provider_str.to_uppercase()
91            ));
92            cliclack::outro("✅ Configuration complete!")?;
93            Ok(())
94        }
95        Ok(false) => {
96            sp.stop("API key verification failed");
97            cliclack::outro(
98                "⚠️ Secrets saved but verification failed. Please check your API key.",
99            )?;
100            Ok(())
101        }
102        Err(e) => {
103            sp.stop("Verification error");
104            cliclack::outro(format!("❌ Error: {}", e))?;
105            Err(e)
106        }
107    }
108}