chasm_cli/commands/
providers.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Provider management commands
4
5use anyhow::Result;
6use colored::*;
7
8use crate::providers::{
9    config::{CsmConfig, ProviderConfig},
10    discovery::print_provider_summary,
11    ProviderRegistry, ProviderType,
12};
13
14/// List all discovered providers
15pub fn list_providers() -> Result<()> {
16    let registry = ProviderRegistry::new();
17    print_provider_summary(&registry);
18    Ok(())
19}
20
21/// Show detailed info about a specific provider
22pub fn provider_info(provider_name: &str) -> Result<()> {
23    let provider_type = parse_provider_name(provider_name)?;
24    let registry = ProviderRegistry::new();
25
26    if let Some(provider) = registry.get_provider(provider_type) {
27        println!("{}", format!("Provider: {}", provider.name()).bold());
28        println!();
29
30        println!("  Type:      {}", provider_type.display_name());
31        println!(
32            "  Available: {}",
33            if provider.is_available() {
34                "Yes".green()
35            } else {
36                "No".red()
37            }
38        );
39
40        if let Some(path) = provider.sessions_path() {
41            println!("  Data Path: {}", path.display());
42        }
43
44        if let Some(endpoint) = provider_type.default_endpoint() {
45            println!("  Endpoint:  {}", endpoint);
46        }
47
48        println!(
49            "  OpenAI Compatible: {}",
50            if provider_type.is_openai_compatible() {
51                "Yes".green()
52            } else {
53                "No".dimmed()
54            }
55        );
56
57        println!(
58            "  File Storage: {}",
59            if provider_type.uses_file_storage() {
60                "Yes".green()
61            } else {
62                "No".dimmed()
63            }
64        );
65
66        // Show sessions if available
67        if provider.is_available() {
68            match provider.list_sessions() {
69                Ok(sessions) => {
70                    println!();
71                    println!("  Sessions:  {}", sessions.len());
72
73                    if !sessions.is_empty() {
74                        println!();
75                        println!("  Recent sessions:");
76                        for session in sessions.iter().take(5) {
77                            println!("    - {}", session.title());
78                        }
79                    }
80                }
81                Err(_) => {
82                    println!("  Sessions:  (unable to list)");
83                }
84            }
85        }
86    } else {
87        eprintln!("{} Provider not found: {}", "Error:".red(), provider_name);
88        eprintln!();
89        eprintln!("Available providers:");
90        list_provider_types();
91        return Err(anyhow::anyhow!("Provider not found"));
92    }
93
94    Ok(())
95}
96
97/// Configure a provider
98pub fn configure_provider(
99    provider_name: &str,
100    endpoint: Option<&str>,
101    api_key: Option<&str>,
102    model: Option<&str>,
103    enabled: Option<bool>,
104) -> Result<()> {
105    let provider_type = parse_provider_name(provider_name)?;
106    let mut config = CsmConfig::load()?;
107
108    // Get or create provider config
109    let mut provider_config = config
110        .get_provider(provider_type)
111        .cloned()
112        .unwrap_or_else(|| ProviderConfig::new(provider_type));
113
114    // Update settings
115    if let Some(endpoint) = endpoint {
116        provider_config.endpoint = Some(endpoint.to_string());
117    }
118
119    if let Some(api_key) = api_key {
120        provider_config.api_key = Some(api_key.to_string());
121    }
122
123    if let Some(model) = model {
124        provider_config.model = Some(model.to_string());
125    }
126
127    if let Some(enabled) = enabled {
128        provider_config.enabled = enabled;
129    }
130
131    // Save config
132    config.set_provider(provider_config.clone());
133    config.save()?;
134
135    println!("{} Configured provider: {}", "+".green(), provider_name);
136    println!();
137    println!(
138        "  Endpoint: {}",
139        provider_config.endpoint.as_deref().unwrap_or("(default)")
140    );
141    println!(
142        "  API Key:  {}",
143        if provider_config.api_key.is_some() {
144            "(set)".green().to_string()
145        } else {
146            "(none)".dimmed().to_string()
147        }
148    );
149    println!(
150        "  Model:    {}",
151        provider_config.model.as_deref().unwrap_or("(default)")
152    );
153    println!(
154        "  Enabled:  {}",
155        if provider_config.enabled {
156            "Yes".green()
157        } else {
158            "No".red()
159        }
160    );
161
162    Ok(())
163}
164
165/// Import sessions from another provider
166pub fn import_from_provider(
167    from_provider: &str,
168    target_path: Option<&str>,
169    session_id: Option<&str>,
170) -> Result<()> {
171    let provider_type = parse_provider_name(from_provider)?;
172    let registry = ProviderRegistry::new();
173
174    let provider = registry
175        .get_provider(provider_type)
176        .ok_or_else(|| anyhow::anyhow!("Provider not found: {}", from_provider))?;
177
178    if !provider.is_available() {
179        return Err(anyhow::anyhow!(
180            "Provider {} is not available",
181            from_provider
182        ));
183    }
184
185    let project_path = target_path
186        .map(String::from)
187        .or_else(|| {
188            std::env::current_dir()
189                .ok()
190                .map(|p| p.to_string_lossy().to_string())
191        })
192        .ok_or_else(|| anyhow::anyhow!("Could not determine target path"))?;
193
194    if let Some(session_id) = session_id {
195        // Import specific session
196        println!(
197            "Importing session {} from {}...",
198            session_id,
199            provider.name()
200        );
201
202        let session = provider.import_session(session_id)?;
203
204        // Save to target workspace
205        let workspace = crate::workspace::get_workspace_by_path(&project_path)?
206            .ok_or_else(|| anyhow::anyhow!("Workspace not found for path: {}", project_path))?;
207        let sessions_dir = workspace.chat_sessions_path;
208        std::fs::create_dir_all(&sessions_dir)?;
209
210        let session_file = sessions_dir.join(format!("{}.json", session_id));
211        let content = serde_json::to_string_pretty(&session)?;
212        std::fs::write(&session_file, content)?;
213
214        println!("{} Imported session: {}", "+".green(), session.title());
215    } else {
216        // Import all sessions
217        println!("Importing all sessions from {}...", provider.name());
218
219        let sessions = provider.list_sessions()?;
220
221        if sessions.is_empty() {
222            println!("  No sessions found");
223            return Ok(());
224        }
225
226        let workspace = crate::workspace::get_workspace_by_path(&project_path)?
227            .ok_or_else(|| anyhow::anyhow!("Workspace not found for path: {}", project_path))?;
228        let sessions_dir = workspace.chat_sessions_path;
229        std::fs::create_dir_all(&sessions_dir)?;
230
231        let mut imported = 0;
232        for session in &sessions {
233            let id = session
234                .session_id
235                .clone()
236                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
237            let session_file = sessions_dir.join(format!("{}.json", id));
238
239            if !session_file.exists() {
240                let content = serde_json::to_string_pretty(&session)?;
241                std::fs::write(&session_file, content)?;
242                imported += 1;
243                println!("  {} {}", "+".green(), session.title());
244            }
245        }
246
247        println!();
248        println!(
249            "Imported {} of {} sessions",
250            imported.to_string().green(),
251            sessions.len()
252        );
253    }
254
255    Ok(())
256}
257
258/// Test connection to a provider
259pub fn test_provider(provider_name: &str) -> Result<()> {
260    let provider_type = parse_provider_name(provider_name)?;
261    let registry = ProviderRegistry::new();
262
263    print!("Testing {} connection... ", provider_type.display_name());
264
265    if let Some(provider) = registry.get_provider(provider_type) {
266        if provider.is_available() {
267            println!("{}", "OK".green());
268
269            // Try to list sessions
270            match provider.list_sessions() {
271                Ok(sessions) => {
272                    println!("  Found {} sessions", sessions.len());
273                }
274                Err(e) => {
275                    println!("  {}: {}", "Warning".yellow(), e);
276                }
277            }
278
279            Ok(())
280        } else {
281            println!("{}", "FAILED".red());
282            println!();
283
284            if let Some(endpoint) = provider_type.default_endpoint() {
285                println!("  Expected endpoint: {}", endpoint);
286                println!("  Make sure the service is running.");
287            }
288
289            Err(anyhow::anyhow!("Provider not available"))
290        }
291    } else {
292        println!("{}", "NOT FOUND".red());
293        Err(anyhow::anyhow!("Provider not found"))
294    }
295}
296
297/// Parse a provider name string into ProviderType
298fn parse_provider_name(name: &str) -> Result<ProviderType> {
299    match name.to_lowercase().as_str() {
300        "copilot" | "github-copilot" | "vscode" => Ok(ProviderType::Copilot),
301        "cursor" => Ok(ProviderType::Cursor),
302        "ollama" => Ok(ProviderType::Ollama),
303        "vllm" => Ok(ProviderType::Vllm),
304        "foundry" | "azure-foundry" | "foundry-local" | "ai-foundry" => Ok(ProviderType::Foundry),
305        "openai" => Ok(ProviderType::OpenAI),
306        "lm-studio" | "lmstudio" => Ok(ProviderType::LmStudio),
307        "localai" | "local-ai" => Ok(ProviderType::LocalAI),
308        "text-gen-webui" | "textgenwebui" | "oobabooga" => Ok(ProviderType::TextGenWebUI),
309        "jan" | "jan-ai" | "janai" => Ok(ProviderType::Jan),
310        "gpt4all" => Ok(ProviderType::Gpt4All),
311        "llamafile" => Ok(ProviderType::Llamafile),
312        "custom" => Ok(ProviderType::Custom),
313        _ => {
314            eprintln!("{} Unknown provider: {}", "Error:".red(), name);
315            eprintln!();
316            list_provider_types();
317            Err(anyhow::anyhow!("Unknown provider"))
318        }
319    }
320}
321
322/// Print list of available provider type names
323fn list_provider_types() {
324    eprintln!("Supported providers:");
325    eprintln!("  copilot      - GitHub Copilot (VS Code)");
326    eprintln!("  cursor       - Cursor IDE");
327    eprintln!("  ollama       - Ollama local models");
328    eprintln!("  vllm         - vLLM server");
329    eprintln!("  foundry      - Azure AI Foundry / Foundry Local");
330    eprintln!("  openai       - OpenAI API");
331    eprintln!("  lm-studio    - LM Studio");
332    eprintln!("  localai      - LocalAI");
333    eprintln!("  text-gen-webui - Text Generation WebUI");
334    eprintln!("  jan          - Jan.ai");
335    eprintln!("  gpt4all      - GPT4All");
336    eprintln!("  llamafile    - Llamafile");
337    eprintln!("  custom       - Custom OpenAI-compatible endpoint");
338}