#![allow(clippy::collapsible_if)] // TODO: Remove once Rust 2024 let-chains are available //! OWLEN CLI - Chat TUI client mod bootstrap; mod commands; mod mcp; use anyhow::Result; use clap::{Parser, Subcommand}; use commands::{ cloud::{CloudCommand, run_cloud_command}, providers::{ModelsArgs, ProvidersCommand, run_models_command, run_providers_command}, }; use mcp::{McpCommand, run_mcp_command}; use owlen_core::config as core_config; use owlen_core::config::McpMode; use owlen_core::mode::Mode; use owlen_tui::config; /// Owlen - Terminal UI for LLM chat #[derive(Parser, Debug)] #[command(name = "owlen")] #[command(about = "Terminal UI for LLM chat via MCP", long_about = None)] struct Args { /// Start in code mode (enables all tools) #[arg(long, short = 'c')] code: bool, #[command(subcommand)] command: Option, } #[derive(Debug, Subcommand)] enum OwlenCommand { /// Inspect or upgrade configuration files #[command(subcommand)] Config(ConfigCommand), /// Manage Ollama Cloud credentials #[command(subcommand)] Cloud(CloudCommand), /// Manage model providers #[command(subcommand)] Providers(ProvidersCommand), /// List models exposed by configured providers Models(ModelsArgs), /// Manage MCP server registrations #[command(subcommand)] Mcp(McpCommand), /// Show manual steps for updating Owlen to the latest revision Upgrade, } #[derive(Debug, Subcommand)] enum ConfigCommand { /// Automatically upgrade legacy configuration values and ensure validity Doctor, /// Print the resolved configuration file path Path, } async fn run_command(command: OwlenCommand) -> Result<()> { match command { OwlenCommand::Config(config_cmd) => run_config_command(config_cmd), OwlenCommand::Cloud(cloud_cmd) => run_cloud_command(cloud_cmd).await, OwlenCommand::Providers(provider_cmd) => run_providers_command(provider_cmd).await, OwlenCommand::Models(args) => run_models_command(args).await, OwlenCommand::Mcp(mcp_cmd) => run_mcp_command(mcp_cmd), OwlenCommand::Upgrade => { println!( "To update Owlen from source:\n git pull\n cargo install --path crates/owlen-cli --force" ); println!( "If you installed from the AUR, use your package manager (e.g., yay -S owlen-git)." ); Ok(()) } } } fn run_config_command(command: ConfigCommand) -> Result<()> { match command { ConfigCommand::Doctor => run_config_doctor(), ConfigCommand::Path => { let path = core_config::default_config_path(); println!("{}", path.display()); Ok(()) } } } fn run_config_doctor() -> Result<()> { let config_path = core_config::default_config_path(); let existed = config_path.exists(); let mut config = config::try_load_config().unwrap_or_default(); let _ = config.refresh_mcp_servers(None); let mut changes = Vec::new(); if !existed { changes.push("created configuration file from defaults".to_string()); } if config.provider(&config.general.default_provider).is_none() { config.general.default_provider = "ollama_local".to_string(); changes.push("default provider missing; reset to 'ollama_local'".to_string()); } for key in ["ollama_local", "ollama_cloud", "openai", "anthropic"] { if !config.providers.contains_key(key) { core_config::ensure_provider_config_mut(&mut config, key); changes.push(format!("added default configuration for provider '{key}'")); } } if let Some(entry) = config.providers.get_mut("ollama_local") { if entry.provider_type.trim().is_empty() || entry.provider_type != "ollama" { entry.provider_type = "ollama".to_string(); changes.push("normalised providers.ollama_local.provider_type to 'ollama'".to_string()); } } let mut ensure_default_enabled = true; if !config.providers.values().any(|cfg| cfg.enabled) { let entry = core_config::ensure_provider_config_mut(&mut config, "ollama_local"); if !entry.enabled { entry.enabled = true; changes.push("no providers were enabled; enabled 'ollama_local'".to_string()); } if config.general.default_provider != "ollama_local" { config.general.default_provider = "ollama_local".to_string(); changes.push( "default provider reset to 'ollama_local' because no providers were enabled" .to_string(), ); } ensure_default_enabled = false; } if ensure_default_enabled { let default_id = config.general.default_provider.clone(); if let Some(default_cfg) = config.providers.get(&default_id) { if !default_cfg.enabled { if let Some(new_default) = config .providers .iter() .filter(|(id, cfg)| cfg.enabled && *id != &default_id) .map(|(id, _)| id.clone()) .min() { config.general.default_provider = new_default.clone(); changes.push(format!( "default provider '{default_id}' was disabled; switched default to '{new_default}'" )); } else { let entry = core_config::ensure_provider_config_mut(&mut config, "ollama_local"); if !entry.enabled { entry.enabled = true; changes.push( "enabled 'ollama_local' because default provider was disabled" .to_string(), ); } if config.general.default_provider != "ollama_local" { config.general.default_provider = "ollama_local".to_string(); changes.push( "default provider reset to 'ollama_local' because previous default was disabled" .to_string(), ); } } } } } match config.mcp.mode { McpMode::Legacy => { config.mcp.mode = McpMode::LocalOnly; config.mcp.warn_on_legacy = true; changes.push("converted [mcp].mode = 'legacy' to 'local_only'".to_string()); } McpMode::RemoteOnly if config.effective_mcp_servers().is_empty() => { config.mcp.mode = McpMode::RemotePreferred; config.mcp.allow_fallback = true; changes.push( "downgraded remote-only configuration to remote_preferred because no servers are defined" .to_string(), ); } McpMode::RemotePreferred if !config.mcp.allow_fallback && config.effective_mcp_servers().is_empty() => { config.mcp.allow_fallback = true; changes.push( "enabled [mcp].allow_fallback because no remote servers are configured".to_string(), ); } _ => {} } config.validate()?; config::save_config(&config)?; if changes.is_empty() { println!( "Configuration already up to date: {}", config_path.display() ); } else { println!("Updated {}:", config_path.display()); for change in changes { println!(" - {change}"); } } Ok(()) } #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { // Parse command-line arguments let Args { code, command } = Args::parse(); if let Some(command) = command { return run_command(command).await; } let initial_mode = if code { Mode::Code } else { Mode::Chat }; bootstrap::launch(initial_mode).await }