lc/cli/
config.rs

1//! Configuration management commands
2
3use crate::cli::ConfigCommands;
4use crate::cli::{DeleteCommands, GetCommands, SetCommands};
5use crate::config;
6use anyhow::Result;
7use colored::Colorize;
8
9/// Handle config-related commands
10pub async fn handle(command: Option<ConfigCommands>) -> Result<()> {
11    match command {
12        Some(ConfigCommands::Set { command }) => handle_set_command(command).await,
13        Some(ConfigCommands::Get { command }) => handle_get_command(command).await,
14        Some(ConfigCommands::Delete { command }) => handle_delete_command(command).await,
15        Some(ConfigCommands::Path) => handle_path_command().await,
16        None => handle_show_current_config().await,
17    }
18}
19
20async fn handle_set_command(command: SetCommands) -> Result<()> {
21    match command {
22        SetCommands::Provider { name } => {
23            let mut config = config::Config::load()?;
24
25            if !config.has_provider(&name) {
26                anyhow::bail!(
27                    "Provider '{}' not found. Add it first with 'lc providers add'",
28                    name
29                );
30            }
31
32            config.default_provider = Some(name.clone());
33            config.save()?;
34            println!("{} Default provider set to '{}'", "✓".green(), name);
35        }
36        SetCommands::Model { name } => {
37            let mut config = config::Config::load()?;
38            config.default_model = Some(name.clone());
39            config.save()?;
40            println!("{} Default model set to '{}'", "✓".green(), name);
41        }
42        SetCommands::SystemPrompt { prompt } => {
43            let mut config = config::Config::load()?;
44            let resolved_prompt = config.resolve_template_or_prompt(&prompt);
45            config.system_prompt = Some(resolved_prompt);
46            config.save()?;
47            println!("{} System prompt set", "✓".green());
48        }
49        SetCommands::MaxTokens { value } => {
50            let mut config = config::Config::load()?;
51            let parsed_value = config::Config::parse_max_tokens(&value)?;
52            config.max_tokens = Some(parsed_value);
53            config.save()?;
54            println!("{} Max tokens set to {}", "✓".green(), parsed_value);
55        }
56        SetCommands::Temperature { value } => {
57            let mut config = config::Config::load()?;
58            let parsed_value = config::Config::parse_temperature(&value)?;
59            config.temperature = Some(parsed_value);
60            config.save()?;
61            println!("{} Temperature set to {}", "✓".green(), parsed_value);
62        }
63        SetCommands::Search { name } => {
64            let mut search_config = crate::search::SearchConfig::load()?;
65
66            if !search_config.has_provider(&name) {
67                anyhow::bail!(
68                    "Search provider '{}' not found. Add it first with 'lc search provider add'",
69                    name
70                );
71            }
72
73            search_config.set_default_provider(name.clone())?;
74            search_config.save()?;
75            println!("{} Default search provider set to '{}'", "✓".green(), name);
76        }
77        SetCommands::Stream { value } => {
78            let mut config = config::Config::load()?;
79            let stream_value = match value.to_lowercase().as_str() {
80                "true" | "1" | "yes" | "on" => true,
81                "false" | "0" | "no" | "off" => false,
82                _ => anyhow::bail!("Invalid stream value '{}'. Use 'true' or 'false'", value),
83            };
84            config.stream = Some(stream_value);
85            config.save()?;
86            println!("{} Streaming mode set to {}", "✓".green(), stream_value);
87        }
88    }
89    Ok(())
90}
91
92async fn handle_get_command(command: GetCommands) -> Result<()> {
93    let config = config::Config::load()?;
94    match command {
95        GetCommands::Provider => {
96            if let Some(provider) = &config.default_provider {
97                println!("{}", provider);
98            } else {
99                anyhow::bail!("No default provider configured");
100            }
101        }
102        GetCommands::Model => {
103            if let Some(model) = &config.default_model {
104                println!("{}", model);
105            } else {
106                anyhow::bail!("No default model configured");
107            }
108        }
109        GetCommands::SystemPrompt => {
110            if let Some(system_prompt) = &config.system_prompt {
111                println!("{}", system_prompt);
112            } else {
113                anyhow::bail!("No system prompt configured");
114            }
115        }
116        GetCommands::MaxTokens => {
117            if let Some(max_tokens) = &config.max_tokens {
118                println!("{}", max_tokens);
119            } else {
120                anyhow::bail!("No max tokens configured");
121            }
122        }
123        GetCommands::Temperature => {
124            if let Some(temperature) = &config.temperature {
125                println!("{}", temperature);
126            } else {
127                anyhow::bail!("No temperature configured");
128            }
129        }
130        GetCommands::Search => {
131            let search_config = crate::search::SearchConfig::load()?;
132            if let Some(provider) = search_config.get_default_provider() {
133                println!("{}", provider);
134            } else {
135                anyhow::bail!("No default search provider configured");
136            }
137        }
138        GetCommands::Stream => {
139            if let Some(stream) = &config.stream {
140                println!("{}", stream);
141            } else {
142                anyhow::bail!("No streaming mode configured");
143            }
144        }
145    }
146    Ok(())
147}
148
149async fn handle_delete_command(command: DeleteCommands) -> Result<()> {
150    let mut config = config::Config::load()?;
151    match command {
152        DeleteCommands::Provider => {
153            if config.default_provider.is_some() {
154                config.default_provider = None;
155                config.save()?;
156                println!("{} Default provider deleted", "✓".green());
157            } else {
158                anyhow::bail!("No default provider configured to delete");
159            }
160        }
161        DeleteCommands::Model => {
162            if config.default_model.is_some() {
163                config.default_model = None;
164                config.save()?;
165                println!("{} Default model deleted", "✓".green());
166            } else {
167                anyhow::bail!("No default model configured to delete");
168            }
169        }
170        DeleteCommands::SystemPrompt => {
171            if config.system_prompt.is_some() {
172                config.system_prompt = None;
173                config.save()?;
174                println!("{} System prompt deleted", "✓".green());
175            } else {
176                anyhow::bail!("No system prompt configured to delete");
177            }
178        }
179        DeleteCommands::MaxTokens => {
180            if config.max_tokens.is_some() {
181                config.max_tokens = None;
182                config.save()?;
183                println!("{} Max tokens deleted", "✓".green());
184            } else {
185                anyhow::bail!("No max tokens configured to delete");
186            }
187        }
188        DeleteCommands::Temperature => {
189            if config.temperature.is_some() {
190                config.temperature = None;
191                config.save()?;
192                println!("{} Temperature deleted", "✓".green());
193            } else {
194                anyhow::bail!("No temperature configured to delete");
195            }
196        }
197        DeleteCommands::Search => {
198            let mut search_config = crate::search::SearchConfig::load()?;
199            if search_config.get_default_provider().is_some() {
200                search_config.set_default_provider(String::new())?;
201                search_config.save()?;
202                println!("{} Default search provider deleted", "✓".green());
203            } else {
204                anyhow::bail!("No default search provider configured to delete");
205            }
206        }
207        DeleteCommands::Stream => {
208            let mut config = config::Config::load()?;
209            if config.stream.is_some() {
210                config.stream = None;
211                config.save()?;
212                println!("{} Streaming mode deleted", "✓".green());
213            } else {
214                anyhow::bail!("No streaming mode configured to delete");
215            }
216        }
217    }
218    Ok(())
219}
220
221async fn handle_path_command() -> Result<()> {
222    let config_dir = config::Config::config_dir()?;
223    println!("\n{}", "Configuration Directory:".bold().blue());
224    println!("{}", config_dir.display());
225    println!("\n{}", "Files:".bold().blue());
226    println!("  {} config.toml", "•".blue());
227    println!("  {} logs.db (synced to cloud)", "•".blue());
228    println!("\n{}", "Database Management:".bold().blue());
229    println!(
230        "  {} Purge old logs: {}",
231        "•".blue(),
232        "lc logs purge --older-than-days 30".dimmed()
233    );
234    println!(
235        "  {} Keep recent logs: {}",
236        "•".blue(),
237        "lc logs purge --keep-recent 1000".dimmed()
238    );
239    println!(
240        "  {} Size-based purge: {}",
241        "•".blue(),
242        "lc logs purge --max-size-mb 50".dimmed()
243    );
244    Ok(())
245}
246
247async fn handle_show_current_config() -> Result<()> {
248    // Show current configuration with enhanced model metadata
249    let config = config::Config::load()?;
250    println!("\n{}", "Current Configuration:".bold().blue());
251
252    if let Some(provider) = &config.default_provider {
253        println!("provider {}", provider);
254    } else {
255        println!("provider {}", "not set".dimmed());
256    }
257
258    if let Some(model) = &config.default_model {
259        // Try to find model metadata to display rich information
260        if let Some(provider) = &config.default_provider {
261            match load_provider_enhanced_models(provider).await {
262                Ok(models) => {
263                    // Find the specific model
264                    if let Some(model_metadata) = models.iter().find(|m| m.id == *model) {
265                        // Display model with metadata
266                        let _model_info = vec![model.clone()];
267
268                        // Build capability indicators
269                        let mut capabilities = Vec::new();
270                        if model_metadata.supports_tools || model_metadata.supports_function_calling
271                        {
272                            capabilities.push("🔧 tools".blue());
273                        }
274                        if model_metadata.supports_vision {
275                            capabilities.push("👁 vision".magenta());
276                        }
277                        if model_metadata.supports_audio {
278                            capabilities.push("🔊 audio".yellow());
279                        }
280                        if model_metadata.supports_reasoning {
281                            capabilities.push("🧠 reasoning".cyan());
282                        }
283                        if model_metadata.supports_code {
284                            capabilities.push("💻 code".green());
285                        }
286
287                        // Build context and pricing info
288                        let mut info_parts = Vec::new();
289                        if let Some(ctx) = model_metadata.context_length {
290                            if ctx >= 1000000 {
291                                info_parts.push(format!("{}m ctx", ctx / 1000000));
292                            } else if ctx >= 1000 {
293                                info_parts.push(format!("{}k ctx", ctx / 1000));
294                            } else {
295                                info_parts.push(format!("{} ctx", ctx));
296                            }
297                        }
298                        if let Some(input_price) = model_metadata.input_price_per_m {
299                            info_parts.push(format!("${:.2}/M in", input_price));
300                        }
301                        if let Some(output_price) = model_metadata.output_price_per_m {
302                            info_parts.push(format!("${:.2}/M out", output_price));
303                        }
304
305                        // Display model name with metadata
306                        let model_display =
307                            if let Some(ref display_name) = model_metadata.display_name {
308                                if display_name != &model_metadata.id {
309                                    format!("{} ({})", model, display_name)
310                                } else {
311                                    model.clone()
312                                }
313                            } else {
314                                model.clone()
315                            };
316
317                        print!("model {}", model_display);
318
319                        if !capabilities.is_empty() {
320                            let capability_strings: Vec<String> =
321                                capabilities.iter().map(|c| c.to_string()).collect();
322                            print!(" [{}]", capability_strings.join(" "));
323                        }
324
325                        if !info_parts.is_empty() {
326                            print!(" ({})", info_parts.join(", ").dimmed());
327                        }
328
329                        println!();
330                    } else {
331                        // Model not found in metadata, show basic info
332                        println!("model {}", model);
333                    }
334                }
335                Err(_) => {
336                    // Failed to load metadata, show basic info
337                    println!("model {}", model);
338                }
339            }
340        } else {
341            // No provider set, show basic info
342            println!("model {}", model);
343        }
344    } else {
345        println!("model {}", "not set".dimmed());
346    }
347
348    if let Some(system_prompt) = &config.system_prompt {
349        println!("system_prompt {}", system_prompt);
350    } else {
351        println!("system_prompt {}", "not set".dimmed());
352    }
353
354    if let Some(max_tokens) = &config.max_tokens {
355        println!("max_tokens {}", max_tokens);
356    } else {
357        println!("max_tokens {}", "not set".dimmed());
358    }
359
360    if let Some(temperature) = &config.temperature {
361        println!("temperature {}", temperature);
362    } else {
363        println!("temperature {}", "not set".dimmed());
364    }
365
366    if let Some(stream) = &config.stream {
367        println!("stream {}", stream);
368    } else {
369        println!("stream {}", "not set".dimmed());
370    }
371
372    Ok(())
373}
374
375// Helper function to load enhanced models for a specific provider
376async fn load_provider_enhanced_models(
377    provider_name: &str,
378) -> Result<Vec<crate::model_metadata::ModelMetadata>> {
379    use crate::model_metadata::MetadataExtractor;
380    use std::fs;
381
382    let filename = format!("models/{}.json", provider_name);
383
384    if !std::path::Path::new(&filename).exists() {
385        return Ok(Vec::new());
386    }
387
388    match fs::read_to_string(&filename) {
389        Ok(json_content) => {
390            match MetadataExtractor::extract_from_provider(provider_name, &json_content) {
391                Ok(models) => Ok(models),
392                Err(e) => {
393                    eprintln!(
394                        "Warning: Failed to extract metadata from {}: {}",
395                        provider_name, e
396                    );
397                    Ok(Vec::new())
398                }
399            }
400        }
401        Err(e) => {
402            eprintln!("Warning: Failed to read {}: {}", filename, e);
403            Ok(Vec::new())
404        }
405    }
406}