use anyhow::{Context, Result, bail}; use clap::{Parser, Subcommand}; use colored::Colorize; use dialoguer::{Confirm, Input, Password, Select}; use std::path::PathBuf; use crate::config::CommitFormat; use crate::config::{EncryptedPat, ExportData, Language, manager::ConfigManager}; use crate::utils::crypto::{decrypt, encrypt}; use crate::utils::keyring::{ get_default_base_url, get_default_model, get_supported_providers, provider_needs_api_key, }; /// Mask API key with asterisks for security fn mask_api_key(key: Option<&str>) -> String { match key { Some(k) => { if k.len() <= 8 { "*".repeat(k.len()) } else { format!("{}***{}", &k[..4], &k[k.len() - 4..]) } } None => "✗ not set".red().to_string(), } } /// Manage configuration settings #[derive(Parser)] pub struct ConfigCommand { #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum ConfigSubcommand { /// Show current configuration Show, /// Edit configuration file Edit, /// Set configuration value Set { /// Key (e.g., llm.provider, commit.format) key: String, /// Value value: String, }, /// Get configuration value Get { /// Key key: String, }, /// Configure LLM provider (interactive) SetLlm { /// Provider (ollama, openai, anthropic, kimi, deepseek, openrouter) #[arg(value_name = "PROVIDER")] provider: Option, /// Model name #[arg(short, long)] model: Option, /// API base URL (optional) #[arg(short, long)] base_url: Option, /// API key (will be stored in system keyring) #[arg(short = 'k', long)] api_key: Option, }, /// Set API key for current provider (stored in system keyring) SetApiKey { /// API key key: String, }, /// Delete API key from system keyring DeleteApiKey, /// Set commit format SetCommitFormat { /// Format (conventional, commitlint) format: String, }, /// Set version prefix for tags SetVersionPrefix { /// Prefix (e.g., 'v') prefix: String, }, /// Set changelog path SetChangelogPath { /// Path path: String, }, /// Set output language SetLanguage { /// Language code (en, zh, ja, ko, es, fr, de) language: Option, }, /// Set whether to keep commit types in English SetKeepTypesEnglish { /// Keep types in English (true/false) keep: bool, }, /// Set whether to keep changelog types in English SetKeepChangelogTypesEnglish { /// Keep types in English (true/false) keep: bool, }, /// Reset configuration to defaults Reset { /// Skip confirmation #[arg(short, long)] force: bool, }, /// Export configuration Export { /// Output file (defaults to stdout) #[arg(short, long)] output: Option, /// Password for encryption (will prompt if not provided) #[arg(short = 'p', long)] password: Option, }, /// Import configuration Import { /// Input file #[arg(short, long)] file: String, /// Password for decryption (will prompt if file is encrypted) #[arg(short = 'p', long)] password: Option, }, /// List available LLM models ListModels, /// Test LLM connection TestLlm, /// Show config file path Path, /// Check keyring availability CheckKeyring, } impl ConfigCommand { pub async fn execute(&self, config_path: Option) -> Result<()> { match &self.command { Some(ConfigSubcommand::Show) => self.show_config(&config_path).await, Some(ConfigSubcommand::Edit) => self.edit_config(&config_path).await, Some(ConfigSubcommand::Set { key, value }) => { self.set_value(key, value, &config_path).await } Some(ConfigSubcommand::Get { key }) => self.get_value(key, &config_path).await, Some(ConfigSubcommand::SetLlm { provider, model, base_url, api_key, }) => { self.set_llm( provider.as_deref(), model.as_deref(), base_url.as_deref(), api_key.as_deref(), &config_path, ) .await } Some(ConfigSubcommand::SetApiKey { key }) => self.set_api_key(key, &config_path).await, Some(ConfigSubcommand::DeleteApiKey) => self.delete_api_key(&config_path).await, Some(ConfigSubcommand::SetCommitFormat { format }) => { self.set_commit_format(format, &config_path).await } Some(ConfigSubcommand::SetVersionPrefix { prefix }) => { self.set_version_prefix(prefix, &config_path).await } Some(ConfigSubcommand::SetChangelogPath { path }) => { self.set_changelog_path(path, &config_path).await } Some(ConfigSubcommand::SetLanguage { language }) => { self.set_language(language.as_deref(), &config_path).await } Some(ConfigSubcommand::SetKeepTypesEnglish { keep }) => { self.set_keep_types_english(*keep, &config_path).await } Some(ConfigSubcommand::SetKeepChangelogTypesEnglish { keep }) => { self.set_keep_changelog_types_english(*keep, &config_path) .await } Some(ConfigSubcommand::Reset { force }) => self.reset(*force, &config_path).await, Some(ConfigSubcommand::Export { output, password }) => { self.export_config(output.as_deref(), password.as_deref(), &config_path) .await } Some(ConfigSubcommand::Import { file, password }) => { self.import_config(file, password.as_deref(), &config_path) .await } Some(ConfigSubcommand::ListModels) => self.list_models(&config_path).await, Some(ConfigSubcommand::TestLlm) => self.test_llm(&config_path).await, Some(ConfigSubcommand::Path) => self.show_path(&config_path).await, Some(ConfigSubcommand::CheckKeyring) => self.check_keyring(&config_path).await, None => self.show_config(&config_path).await, } } fn get_manager(&self, config_path: &Option) -> Result { match config_path { Some(path) => ConfigManager::with_path(path), None => ConfigManager::new(), } } async fn show_path(&self, config_path: &Option) -> Result<()> { let manager = self.get_manager(config_path)?; println!("{}", manager.path().display()); Ok(()) } async fn check_keyring(&self, config_path: &Option) -> Result<()> { let manager = self.get_manager(config_path)?; let keyring = manager.keyring(); println!("{}", "\nKeyring Status".bold()); println!("{}", "─".repeat(40)); if keyring.is_available() { println!("{} Keyring is available", "✓".green()); println!(" {}", keyring.get_status_message()); } else { println!("{} Keyring is not available", "✗".red()); println!(" {}", keyring.get_status_message()); } println!("\n{}", "Environment Variables:".bold()); if let Ok(key) = std::env::var("QUICOMMIT_API_KEY") { println!(" QUICOMMIT_API_KEY: {}", mask_api_key(Some(&key))); } else { println!(" QUICOMMIT_API_KEY: {}", "not set".dimmed()); } let provider = manager.llm_provider(); let provider_env = format!("QUICOMMIT_{}_API_KEY", provider.to_uppercase()); if let Ok(key) = std::env::var(&provider_env) { println!(" {}: {}", provider_env, mask_api_key(Some(&key))); } else { println!(" {}: {}", provider_env, "not set".dimmed()); } Ok(()) } async fn show_config(&self, config_path: &Option) -> Result<()> { let manager = self.get_manager(config_path)?; let config = manager.config(); println!("{}", "\nQuiCommit Configuration".bold()); println!("{}", "─".repeat(60)); println!("\n{}", "General:".bold()); println!(" Config file: {}", manager.path().display()); println!( " Default profile: {}", config.default_profile.as_deref().unwrap_or("(none)").cyan() ); println!(" Profiles: {}", config.profiles.len()); println!("\n{}", "LLM Configuration:".bold()); println!(" Provider: {}", config.llm.provider.cyan()); println!(" Model: {}", config.llm.model.cyan()); let base_url = manager.llm_base_url(); println!(" Base URL: {}", base_url); let api_key = manager.get_api_key(); println!(" API key: {}", mask_api_key(api_key.as_deref())); println!(" Max tokens: {}", config.llm.max_tokens); println!(" Temperature: {}", config.llm.temperature); println!(" Timeout: {}s", config.llm.timeout); println!("\n{}", "Commit Configuration:".bold()); println!(" Format: {}", config.commit.format.to_string().cyan()); println!( " Auto-generate: {}", if config.commit.auto_generate { "yes".green() } else { "no".red() } ); println!("\n{}", "Tag Configuration:".bold()); println!(" Version prefix: '{}'", config.tag.version_prefix); println!( " Auto-generate: {}", if config.tag.auto_generate { "yes".green() } else { "no".red() } ); println!("\n{}", "Language Configuration:".bold()); let language = manager.get_language().unwrap_or(Language::English); println!(" Output language: {}", language.display_name().cyan()); println!( " Keep commit types in English: {}", if manager.keep_types_english() { "yes".green() } else { "no".red() } ); println!( " Keep changelog types in English: {}", if manager.keep_changelog_types_english() { "yes".green() } else { "no".red() } ); println!("\n{}", "Changelog Configuration:".bold()); println!(" Path: {}", config.changelog.path); println!( " Auto-generate: {}", if config.changelog.auto_generate { "yes".green() } else { "no".red() } ); println!("\n{}", "Security:".bold()); println!(" Repository mappings: {} mapping(s)", config.repo_profiles.len()); println!( " Keyring: {}", if manager.keyring().is_available() { "available".green() } else { "unavailable".red() } ); Ok(()) } async fn edit_config(&self, config_path: &Option) -> Result<()> { let manager = self.get_manager(config_path)?; crate::utils::editor::edit_file(manager.path())?; println!("{} Configuration updated", "✓".green()); Ok(()) } async fn set_value(&self, key: &str, value: &str, config_path: &Option) -> Result<()> { let mut manager = self.get_manager(config_path)?; match key { "llm.provider" => manager.set_llm_provider(value.to_string()), "llm.model" => manager.set_llm_model(value.to_string()), "llm.base_url" => manager.set_llm_base_url(Some(value.to_string())), "llm.max_tokens" => { let tokens: u32 = value.parse()?; manager.config_mut().llm.max_tokens = tokens; } "llm.temperature" => { let temp: f32 = value.parse()?; manager.config_mut().llm.temperature = temp; } "llm.timeout" => { let timeout: u64 = value.parse()?; manager.config_mut().llm.timeout = timeout; } "llm.thinking_enabled" => { manager.config_mut().llm.thinking_enabled = value == "true"; } "llm.thinking_budget_tokens" => { let budget: u32 = value.parse()?; manager.config_mut().llm.thinking_budget_tokens = Some(budget); } "llm.api_key_storage" => { let valid_values = ["keyring", "config", "environment"]; if !valid_values.contains(&value) { bail!("Invalid value: {}. Use: {}", value, valid_values.join(", ")); } manager.config_mut().llm.api_key_storage = value.to_string(); } "commit.format" => { let format = match value { "conventional" => CommitFormat::Conventional, "commitlint" => CommitFormat::Commitlint, _ => bail!("Invalid format: {}. Use: conventional, commitlint", value), }; manager.set_commit_format(format); } "commit.auto_generate" => { manager.set_auto_generate_commits(value == "true"); } "tag.version_prefix" => manager.set_version_prefix(value.to_string()), "tag.auto_generate" => { manager.config_mut().tag.auto_generate = value == "true"; } "changelog.path" => manager.set_changelog_path(value.to_string()), "changelog.auto_generate" => { manager.config_mut().changelog.auto_generate = value == "true"; } "language.output_language" => { manager.set_output_language(value.to_string()); } "language.keep_types_english" => { manager.set_keep_types_english(value == "true"); } "language.keep_changelog_types_english" => { manager.set_keep_changelog_types_english(value == "true"); } _ => bail!("Unknown configuration key: {}", key), } manager.save()?; println!("{} Set {} = {}", "✓".green(), key.cyan(), value); Ok(()) } async fn get_value(&self, key: &str, config_path: &Option) -> Result<()> { let manager = self.get_manager(config_path)?; let config = manager.config(); let value = match key { "llm.provider" => config.llm.provider.clone(), "llm.model" => config.llm.model.clone(), "llm.base_url" => manager.llm_base_url(), "llm.max_tokens" => config.llm.max_tokens.to_string(), "llm.temperature" => config.llm.temperature.to_string(), "llm.timeout" => config.llm.timeout.to_string(), "llm.thinking_enabled" => config.llm.thinking_enabled.to_string(), "llm.thinking_budget_tokens" => config .llm .thinking_budget_tokens .map(|v| v.to_string()) .unwrap_or_else(|| "none".to_string()), "llm.api_key_storage" => config.llm.api_key_storage.clone(), "commit.format" => config.commit.format.to_string(), "commit.auto_generate" => config.commit.auto_generate.to_string(), "tag.version_prefix" => config.tag.version_prefix.clone(), "tag.auto_generate" => config.tag.auto_generate.to_string(), "changelog.path" => config.changelog.path.clone(), "changelog.auto_generate" => config.changelog.auto_generate.to_string(), "language.output_language" => config.language.output_language.clone(), "language.keep_types_english" => config.language.keep_types_english.to_string(), "language.keep_changelog_types_english" => { config.language.keep_changelog_types_english.to_string() } _ => bail!("Unknown configuration key: {}", key), }; println!("{}", value); Ok(()) } async fn set_llm( &self, provider: Option<&str>, model: Option<&str>, base_url: Option<&str>, api_key: Option<&str>, config_path: &Option, ) -> Result<()> { let mut manager = self.get_manager(config_path)?; let selected_provider = if let Some(p) = provider { let providers = get_supported_providers(); if !providers.contains(&p) { bail!( "Invalid provider: {}. Valid options: {}", p, providers.join(", ") ); } p.to_string() } else { println!("{}", "Select LLM Provider:".bold()); let provider_display_names = vec![ "Ollama (local)", "OpenAI", "Anthropic Claude", "Kimi (Moonshot AI)", "DeepSeek", "OpenRouter", ]; let provider_idx = Select::new() .items(&provider_display_names) .default(0) .interact()?; let providers = get_supported_providers(); providers[provider_idx].to_string() }; let keyring = manager.keyring(); let keyring_available = keyring.is_available(); if !keyring_available && provider_needs_api_key(&selected_provider) { println!( "\n{}", "⚠ Keyring is not available on this system.".yellow() ); println!("{}", keyring.get_status_message().yellow()); } let selected_model = if let Some(m) = model { m.to_string() } else { let default_model = get_default_model(&selected_provider); Input::new() .with_prompt("Model name") .default(default_model.to_string()) .interact_text()? }; let selected_base_url = if let Some(u) = base_url { Some(u.to_string()) } else if selected_provider == "ollama" { let url: String = Input::new() .with_prompt("Ollama server URL") .default("http://localhost:11434".to_string()) .interact_text()?; Some(url) } else { let use_custom = Confirm::new() .with_prompt("Use custom API base URL?") .default(false) .interact()?; if use_custom { let default_url = get_default_base_url(&selected_provider); let url: String = Input::new() .with_prompt("Base URL") .default(default_url.to_string()) .interact_text()?; Some(url) } else { None } }; let selected_api_key = if provider_needs_api_key(&selected_provider) { if let Some(k) = api_key { Some(k.to_string()) } else if keyring_available { let existing_key = manager.get_api_key(); if existing_key.is_some() { let overwrite = Confirm::new() .with_prompt("API key already exists. Update it?") .default(false) .interact()?; if overwrite { let key: String = Input::new().with_prompt("API key").interact_text()?; Some(key) } else { None } } else { let key: String = Input::new().with_prompt("API key").interact_text()?; Some(key) } } else { println!( "\n{}", "Please set the QUICOMMIT_API_KEY environment variable.".yellow() ); None } } else { None }; manager.set_llm_provider(selected_provider.clone()); manager.set_llm_model(selected_model); manager.set_llm_base_url(selected_base_url); if let Some(key) = selected_api_key { manager.set_api_key(&key)?; println!( "\n{} API key stored securely in system keyring", "✓".green() ); } manager.save()?; println!("\n{} LLM configuration updated", "✓".green()); println!(" Provider: {}", manager.llm_provider().cyan()); println!(" Model: {}", manager.llm_model().cyan()); println!(" Base URL: {}", manager.llm_base_url()); Ok(()) } async fn set_api_key(&self, key: &str, config_path: &Option) -> Result<()> { let mut manager = self.get_manager(config_path)?; let provider = manager.llm_provider().to_string(); if !provider_needs_api_key(&provider) { println!("{} {} does not require an API key", "ℹ".blue(), provider); return Ok(()); } let storage_method = manager.config().llm.api_key_storage.to_string(); match storage_method.as_str() { "keyring" => { if !manager.keyring().is_available() { bail!( "Keyring is not available. Set QUICOMMIT_API_KEY environment variable instead or change api_key_storage to 'config'." ); } manager.set_api_key(key)?; println!( "{} API key stored securely in system keyring for {}", "✓".green(), provider.cyan() ); } "config" => { // Store API key directly in config file manager.config_mut().llm.api_key = Some(key.to_string()); manager.save()?; println!( "{} API key stored in configuration file for {}", "✓".green(), provider.cyan() ); println!( "{} Note: API key is stored in plain text. Consider using 'keyring' storage for better security.", "⚠".yellow() ); } "environment" => { bail!( "API key storage set to 'environment'. Please set QUICOMMIT_{}_API_KEY environment variable.", provider.to_uppercase() ); } _ => { bail!("Invalid API key storage method: {}", storage_method); } } Ok(()) } async fn delete_api_key(&self, config_path: &Option) -> Result<()> { let mut manager = self.get_manager(config_path)?; let provider = manager.llm_provider().to_string(); let storage_method = manager.config().llm.api_key_storage.to_string(); match storage_method.as_str() { "keyring" => { if !manager.keyring().is_available() { bail!("Keyring is not available."); } manager.delete_api_key()?; println!( "{} API key deleted from system keyring for {}", "✓".green(), provider.cyan() ); } "config" => { // Remove API key from config file manager.config_mut().llm.api_key = None; manager.save()?; println!( "{} API key deleted from configuration file for {}", "✓".green(), provider.cyan() ); } "environment" => { println!( "{} API key storage set to 'environment'. Please remove QUICOMMIT_{}_API_KEY environment variable manually.", "ℹ".blue(), provider.to_uppercase() ); } _ => { bail!("Invalid API key storage method: {}", storage_method); } } Ok(()) } async fn set_commit_format(&self, format: &str, config_path: &Option) -> Result<()> { let mut manager = self.get_manager(config_path)?; let format = match format { "conventional" => CommitFormat::Conventional, "commitlint" => CommitFormat::Commitlint, _ => bail!("Invalid format: {}. Use: conventional, commitlint", format), }; manager.set_commit_format(format); manager.save()?; println!( "{} Commit format set to {}", "✓".green(), format.to_string().cyan() ); Ok(()) } async fn set_version_prefix(&self, prefix: &str, config_path: &Option) -> Result<()> { let mut manager = self.get_manager(config_path)?; manager.set_version_prefix(prefix.to_string()); manager.save()?; println!("{} Version prefix set to '{}'", "✓".green(), prefix.cyan()); Ok(()) } async fn set_changelog_path(&self, path: &str, config_path: &Option) -> Result<()> { let mut manager = self.get_manager(config_path)?; manager.set_changelog_path(path.to_string()); manager.save()?; println!("{} Changelog path set to {}", "✓".green(), path.cyan()); Ok(()) } async fn set_language( &self, language: Option<&str>, config_path: &Option, ) -> Result<()> { let mut manager = self.get_manager(config_path)?; let lang_code = if let Some(l) = language { l.to_string() } else { println!("{}", "Select Output Language:".bold()); let languages = [ ("en", "English"), ("zh", "中文"), ("ja", "日本語"), ("ko", "한국어"), ("es", "Español"), ("fr", "Français"), ("de", "Deutsch"), ]; let lang_names: Vec<&str> = languages.iter().map(|(_, n)| *n).collect(); let idx = Select::new().items(&lang_names).default(0).interact()?; languages[idx].0.to_string() }; manager.set_output_language(lang_code.clone()); manager.save()?; println!( "{} Output language set to {}", "✓".green(), lang_code.cyan() ); Ok(()) } async fn set_keep_types_english( &self, keep: bool, config_path: &Option, ) -> Result<()> { let mut manager = self.get_manager(config_path)?; manager.set_keep_types_english(keep); manager.save()?; println!( "{} Keep commit types in English: {}", "✓".green(), keep.to_string().cyan() ); Ok(()) } async fn set_keep_changelog_types_english( &self, keep: bool, config_path: &Option, ) -> Result<()> { let mut manager = self.get_manager(config_path)?; manager.set_keep_changelog_types_english(keep); manager.save()?; println!( "{} Keep changelog types in English: {}", "✓".green(), keep.to_string().cyan() ); Ok(()) } async fn reset(&self, force: bool, config_path: &Option) -> Result<()> { if !force { let confirm = Confirm::new() .with_prompt("Reset all configuration to defaults?") .default(false) .interact()?; if !confirm { println!("Reset cancelled."); return Ok(()); } } let mut manager = self.get_manager(config_path)?; manager.reset(); manager.save()?; println!("{} Configuration reset to defaults", "✓".green()); Ok(()) } async fn export_config( &self, output: Option<&str>, password: Option<&str>, config_path: &Option, ) -> Result<()> { let manager = self.get_manager(config_path)?; let toml = manager.export()?; let export_content = if let Some(_path) = output { let pwd = if let Some(p) = password { p.to_string() } else { let confirm = Confirm::new() .with_prompt("Encrypt the exported configuration?") .default(true) .interact()?; if confirm { let pwd1 = Password::new() .with_prompt("Enter encryption password") .interact()?; let pwd2 = Password::new() .with_prompt("Confirm encryption password") .interact()?; if pwd1 != pwd2 { bail!("Passwords do not match"); } pwd1 } else { String::new() } }; if pwd.is_empty() { let mut has_pats = false; for (profile_name, profile) in manager.config().profiles.iter() { for service in profile.tokens.keys() { if manager.has_pat_for_profile(profile_name, service) { has_pats = true; break; } } } if has_pats { println!( "{} {}", "⚠".yellow(), "WARNING: Exporting without encryption.".bold() ); println!( " {}", "Personal Access Tokens (PATs) stored in keyring will NOT be exported." .yellow() ); println!( " {}", "To export PATs securely, please enable encryption.".yellow() ); println!(); } toml } else { let mut encrypted_pats: Vec = Vec::new(); for (profile_name, profile) in manager.config().profiles.iter() { for service in profile.tokens.keys() { if let Ok(Some(pat_value)) = manager.get_pat_for_profile(profile_name, service) { let encrypted_token = encrypt(pat_value.as_bytes(), &pwd)?; encrypted_pats.push(EncryptedPat { profile_name: profile_name.clone(), service: service.clone(), user_email: profile.user_email.clone(), encrypted_token, }); } } } let export_data = ExportData::with_encrypted_pats(toml, encrypted_pats); let export_json = serde_json::to_string(&export_data) .context("Failed to serialize export data")?; let encrypted = encrypt(export_json.as_bytes(), &pwd)?; format!("ENCRYPTED:{}", encrypted) } } else { toml }; match output { Some(path) => { std::fs::write(path, &export_content)?; if export_content.starts_with("ENCRYPTED:") { println!( "{} Configuration encrypted and exported to {}", "✓".green(), path ); } else { println!("{} Configuration exported to {}", "✓".green(), path); } } None => { println!("{}", export_content); } } Ok(()) } async fn import_config( &self, file: &str, password: Option<&str>, config_path: &Option, ) -> Result<()> { let content = std::fs::read_to_string(file)?; let (config_content, encrypted_pats, pwd) = if content.starts_with("ENCRYPTED:") { let encrypted_data = content.strip_prefix("ENCRYPTED:").unwrap(); let pwd = if let Some(p) = password { p.to_string() } else { Password::new() .with_prompt("Enter decryption password") .interact()? }; let decrypted = match decrypt(encrypted_data, &pwd) { Ok(d) => d, Err(e) => { bail!( "Failed to decrypt configuration: {}. Please check your password.", e ); } }; let decrypted_str = String::from_utf8(decrypted) .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in decrypted content: {}", e))?; match serde_json::from_str::(&decrypted_str) { Ok(export_data) => ( export_data.config, Some(export_data.encrypted_pats), Some(pwd), ), Err(_) => (decrypted_str, None, Some(pwd)), } } else { (content, None, None) }; let mut manager = self.get_manager(config_path)?; manager.import(&config_content)?; manager.save()?; if let (Some(pats), Some(pwd)) = (encrypted_pats, pwd) && !pats.is_empty() { println!(); println!("{}", "Importing Personal Access Tokens...".bold()); let mut imported_count = 0; let mut failed_count = 0; for pat in pats { match decrypt(&pat.encrypted_token, &pwd) { Ok(token_bytes) => match String::from_utf8(token_bytes) { Ok(token_value) => { if manager.keyring().is_available() { match manager.store_pat_for_profile( &pat.profile_name, &pat.service, &token_value, ) { Ok(_) => { println!( " {} Token for {} ({}) imported to keyring", "✓".green(), pat.profile_name.cyan(), pat.service.yellow() ); imported_count += 1; } Err(e) => { println!( " {} Failed to store token for {} ({}): {}", "✗".red(), pat.profile_name, pat.service, e ); failed_count += 1; } } } else { println!( " {} Keyring not available, cannot store token for {} ({})", "⚠".yellow(), pat.profile_name, pat.service ); failed_count += 1; } } Err(e) => { println!( " {} Invalid token format for {} ({}): {}", "✗".red(), pat.profile_name, pat.service, e ); failed_count += 1; } }, Err(e) => { println!( " {} Failed to decrypt token for {} ({}): {}", "✗".red(), pat.profile_name, pat.service, e ); failed_count += 1; } } } println!(); if imported_count > 0 { println!( "{} {} token(s) imported to keyring", "✓".green(), imported_count ); } if failed_count > 0 { println!( "{} {} token(s) failed to import", "⚠".yellow(), failed_count ); } } println!("{} Configuration imported from {}", "✓".green(), file); Ok(()) } async fn list_models(&self, config_path: &Option) -> Result<()> { let manager = self.get_manager(config_path)?; let provider = manager.llm_provider(); println!("{}", "\nAvailable Models".bold()); println!("{}", "─".repeat(40)); match provider { "ollama" => { println!("Ollama models (local):"); println!(" llama2, llama2-uncensored, llama2:13b"); println!(" codellama, codellama:34b"); println!(" mistral, mixtral, phi, gemma"); println!("\nRun 'ollama list' to see installed models"); } "openai" => { println!("OpenAI models:"); println!(" o-series (reasoning): o4-mini, o3, o3-mini, o1, o1-mini, o1-pro"); println!(" GPT-4: gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, gpt-4o, gpt-4o-mini"); println!(" Legacy: gpt-4-turbo, gpt-4, gpt-3.5-turbo"); println!("\nUse --think/-t with o-series models for reasoning mode."); } "anthropic" => { println!("Anthropic Claude models:"); println!( " Claude 4 (thinking): claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5" ); println!( " Claude 3.5: claude-3-opus-20240229, claude-3-sonnet-20240229, claude-3-haiku-20240307" ); println!(" Legacy: claude-2.1, claude-2.0, claude-instant-1.2"); println!("\nUse --think/-t with Claude 4 models for extended thinking."); } "kimi" => { println!("Kimi (Moonshot AI) models:"); println!( " K2 (thinking): kimi-k2.6, kimi-k2.5, kimi-k2-thinking, kimi-k2-thinking-turbo" ); println!(" K2 instruct: kimi-k2-instruct, kimi-k2-instruct-0905"); println!(" Legacy: moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k"); println!("\nUse --think/-t with K2 models for thinking mode."); } "deepseek" => { println!("DeepSeek models:"); println!(" V4: deepseek-v4-flash, deepseek-v4-pro"); println!(" Legacy: deepseek-chat, deepseek-reasoner (deprecated 2026-07-24)"); println!("\nUse --think/-t for reasoning mode."); } "openrouter" => { println!("OpenRouter models (examples):"); println!(" openai/gpt-4, openai/gpt-3.5-turbo"); println!(" anthropic/claude-3-opus"); println!(" google/gemini-pro"); println!(" meta-llama/llama-2-70b-chat"); println!("\nSee https://openrouter.ai/models for full list"); } _ => { println!("Unknown provider: {}", provider); } } println!("\nCurrent model: {}", manager.llm_model().cyan()); Ok(()) } async fn test_llm(&self, config_path: &Option) -> Result<()> { let manager = self.get_manager(config_path)?; println!("{}", "\nTesting LLM Connection...".bold()); println!(" Provider: {}", manager.llm_provider().cyan()); println!(" Model: {}", manager.llm_model().cyan()); println!(" Base URL: {}", manager.llm_base_url()); let has_key = manager.has_api_key(); if provider_needs_api_key(manager.llm_provider()) { println!( " API Key: {}", if has_key { "✓ configured".green() } else { "✗ not set".red() } ); } println!("\n{}", "Sending test request...".dimmed()); match crate::llm::test_connection(&manager).await { Ok(response) => { println!("{} Connection successful!", "✓".green()); println!("\n{}", "Response:".bold()); println!(" {}", response); } Err(e) => { println!("{} Connection failed: {}", "✗".red(), e); return Err(e); } } Ok(()) } }