diff --git a/.env.example b/.env.example index a6ba55e28..10e58d677 100644 --- a/.env.example +++ b/.env.example @@ -12,10 +12,11 @@ API_KEY=your-api-key-here # ZEROCLAW_API_KEY=your-api-key-here -# Default provider/model (can be overridden by CLI flags) -PROVIDER=openrouter -# ZEROCLAW_PROVIDER=openrouter -# ZEROCLAW_MODEL=anthropic/claude-sonnet-4-6 +# Default provider/model (required - choose one) +# Options: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, and more +# PROVIDER=your-provider-here +# ZEROCLAW_PROVIDER=your-provider-here +# ZEROCLAW_MODEL=your-model-here # ZEROCLAW_TEMPERATURE=0.7 # Workspace directory override diff --git a/docker-compose.yml b/docker-compose.yml index b1e6fefc4..cfcda78eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,9 +21,9 @@ services: # Or use the prefixed version: # - ZEROCLAW_API_KEY=${ZEROCLAW_API_KEY:-} - # Optional: LLM provider (default: openrouter) - # Options: openrouter, openai, anthropic, ollama - - PROVIDER=${PROVIDER:-openrouter} + # Required: LLM provider (must be set) + # Options: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai + - PROVIDER=${PROVIDER:?PROVIDER is required - set to openrouter, openai, anthropic, gemini, ollama, etc} # Allow public bind inside Docker (required for container networking) - ZEROCLAW_ALLOW_PUBLIC_BIND=true diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 7a1b1001e..9852ac48c 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -14,16 +14,18 @@ For first-time setup and quick orientation. | Scenario | Command | |----------|---------| -| I have an API key, want fastest setup | `zeroclaw onboard --api-key sk-... --provider openrouter` | -| I want guided prompts | `zeroclaw onboard --interactive` | +| I have an API key, want fastest setup | `zeroclaw onboard --api-key sk-... --provider ` | +| I want guided prompts (TUI wizard) | `zeroclaw onboard --interactive` | | Config exists, just fix channels | `zeroclaw onboard --channels-only` | | Config exists, I intentionally want full overwrite | `zeroclaw onboard --force` | | Using OpenAI Codex subscription auth | See [OpenAI Codex OAuth Quick Setup](#openai-codex-oauth-quick-setup) | +**Supported providers:** openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, together-ai, fireworks, perplexity, cohere, and more. + ## Onboarding and Validation -- Quick onboarding: `zeroclaw onboard --api-key "sk-..." --provider openrouter` -- Interactive onboarding: `zeroclaw onboard --interactive` +- Quick onboarding: `zeroclaw onboard --api-key "sk-..." --provider ` +- Interactive onboarding (recommended): `zeroclaw onboard --interactive` - Existing config protection: reruns require explicit confirmation (or `--force` in non-interactive flows) - Ollama cloud models (`:cloud`) require a remote `api_url` and API key (for example `api_url = "https://ollama.com"`). - Validate environment: `zeroclaw status` + `zeroclaw doctor` diff --git a/docs/one-click-bootstrap.md b/docs/one-click-bootstrap.md index 9cd4ae5ae..ecc3ee33e 100644 --- a/docs/one-click-bootstrap.md +++ b/docs/one-click-bootstrap.md @@ -119,22 +119,24 @@ it pulls `ghcr.io/zeroclaw-labs/zeroclaw:latest` and tags it locally before runn ### Quick onboarding (non-interactive) ```bash -./bootstrap.sh --onboard --api-key "sk-..." --provider openrouter +# Replace with your choice: openrouter, openai, anthropic, gemini, ollama, etc. +./install.sh --onboard --api-key "sk-..." --provider ``` Or with environment variables: ```bash -ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="openrouter" ./bootstrap.sh --onboard +# Replace with your choice +ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="" ./install.sh --onboard ``` ### Interactive onboarding ```bash -./bootstrap.sh --interactive-onboard +./install.sh --interactive-onboard ``` -This launches the full-screen TUI onboarding flow (`zeroclaw onboard --interactive-ui`). +This launches the full-screen TUI onboarding flow (`zeroclaw onboard --interactive`) where you can select your provider interactively. ## Useful flags diff --git a/src/onboard/tui.rs b/src/onboard/tui.rs index 68dd5f8b6..0ef522ca9 100644 --- a/src/onboard/tui.rs +++ b/src/onboard/tui.rs @@ -26,7 +26,7 @@ use std::io::{self, IsTerminal}; use std::path::PathBuf; use std::time::Duration; -const PROVIDER_OPTIONS: [&str; 5] = ["openrouter", "openai", "anthropic", "gemini", "ollama"]; +const PROVIDER_OPTIONS: [&str; 6] = ["(select provider)", "openrouter", "openai", "anthropic", "gemini", "ollama"]; const MEMORY_OPTIONS: [&str; 4] = ["sqlite", "lucid", "markdown", "none"]; const TUNNEL_OPTIONS: [&str; 3] = ["none", "cloudflare", "ngrok"]; @@ -228,13 +228,12 @@ struct TuiOnboardPlan { impl TuiOnboardPlan { fn new(default_workspace: PathBuf, force: bool) -> Self { - let provider = PROVIDER_OPTIONS[0]; Self { workspace_path: default_workspace.display().to_string(), force_overwrite: force, - provider_idx: 0, + provider_idx: 0, // Start with placeholder - user must select api_key: String::new(), - model: provider_default_model(provider), + model: String::new(), // No default model until provider is selected memory_idx: 0, disable_totp: false, enable_telegram: false, @@ -254,7 +253,16 @@ impl TuiOnboardPlan { } fn provider(&self) -> &str { - PROVIDER_OPTIONS[self.provider_idx] + let idx = self.provider_idx; + if idx == 0 { + "(select provider)" + } else { + PROVIDER_OPTIONS[idx] + } + } + + fn provider_selected(&self) -> bool { + self.provider_idx > 0 } fn memory_backend(&self) -> &str { @@ -765,6 +773,9 @@ impl TuiState { Ok(()) } Step::Provider => { + if !self.plan.provider_selected() { + bail!("Provider selection is required. Use arrow keys to select a provider.") + } if self.plan.model.trim().is_empty() { bail!("Default model is required") } diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index 4314154ac..d140a4f22 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -477,8 +477,8 @@ fn apply_provider_update( // ── Quick setup (zero prompts) ─────────────────────────────────── /// Non-interactive setup: generates a sensible default config instantly. -/// Use `zeroclaw onboard` or `zeroclaw onboard --api-key sk-... --provider openrouter --memory sqlite|lucid|cortex-mem`. -/// Use `zeroclaw onboard --interactive` for the full wizard. +/// Use `zeroclaw onboard --api-key sk-... --provider --memory sqlite|lucid|cortex-mem`. +/// Use `zeroclaw onboard --interactive` for the full wizard with provider selection. fn backend_key_from_choice(choice: usize) -> &'static str { selectable_memory_backends() .get(choice) @@ -741,7 +741,12 @@ async fn run_quick_setup_with_home( .await .context("Failed to create workspace directory")?; - let provider_name = provider.unwrap_or("openrouter").to_string(); + let provider_name = provider + .ok_or_else(|| anyhow::anyhow!( + "Provider is required. Use --provider or run with --interactive for the TUI wizard.\n\ + \nSupported providers: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, and more." + ))? + .to_string(); let model = model_override .map(str::to_string) .unwrap_or_else(|| default_model_for_provider(&provider_name)); @@ -2200,11 +2205,15 @@ pub async fn run_models_refresh( provider_override: Option<&str>, force: bool, ) -> Result<()> { - let provider_name = provider_override + let provider_name = match provider_override .or(config.default_provider.as_deref()) - .unwrap_or("openrouter") - .trim() - .to_string(); + { + Some(p) => p.trim().to_string(), + None => anyhow::bail!( + "No provider configured. Run `zeroclaw onboard --interactive` or set a provider with `--provider `.\n\ + \nSupported providers: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, and more." + ), + }; if provider_name.is_empty() { anyhow::bail!("Provider name cannot be empty"); @@ -2284,11 +2293,17 @@ pub async fn run_models_refresh( } pub async fn run_models_list(config: &Config, provider_override: Option<&str>) -> Result<()> { - let provider_name = provider_override + let provider_name = match provider_override .or(config.default_provider.as_deref()) - .unwrap_or("openrouter"); + { + Some(p) => p.to_string(), + None => anyhow::bail!( + "No provider configured. Run `zeroclaw onboard --interactive` or specify `--provider `.\n\ + \nSupported providers: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, and more." + ), + }; - let cached = load_any_cached_models_for_provider(&config.workspace_dir, provider_name).await?; + let cached = load_any_cached_models_for_provider(&config.workspace_dir, &provider_name).await?; let Some(cached) = cached else { println!(); @@ -2336,7 +2351,7 @@ pub async fn run_models_set(config: &Config, model: &str) -> Result<()> { } pub async fn run_models_status(config: &Config) -> Result<()> { - let provider = config.default_provider.as_deref().unwrap_or("openrouter"); + let provider = config.default_provider.as_deref().unwrap_or("(not set)"); let model = config.default_model.as_deref().unwrap_or("(not set)"); println!(); @@ -6794,7 +6809,7 @@ fn print_summary(config: &Config) { println!( " {} Provider: {}", style("🤖").cyan(), - config.default_provider.as_deref().unwrap_or("openrouter") + config.default_provider.as_deref().unwrap_or("(not set)") ); println!( " {} Model: {}", @@ -6914,57 +6929,59 @@ fn print_summary(config: &Config) { let mut step = 1u8; - let provider = config.default_provider.as_deref().unwrap_or("openrouter"); - let canonical_provider = canonical_provider_name(provider); - if config.api_key.is_none() && !provider_supports_keyless_local_usage(provider) { - if canonical_provider == "copilot" { - println!( - " {} Authenticate GitHub Copilot:", - style(format!("{step}.")).cyan().bold() - ); - println!(" {}", style("zeroclaw agent -m \"Hello!\"").yellow()); - println!( - " {}", - style("(device/OAuth prompt appears automatically on first run)").dim() - ); - } else if canonical_provider == "openai-codex" { - println!( - " {} Authenticate OpenAI Codex:", - style(format!("{step}.")).cyan().bold() - ); - println!( - " {}", - style("zeroclaw auth login --provider openai-codex --device-code").yellow() - ); - } else if canonical_provider == "anthropic" { - println!( - " {} Configure Anthropic auth:", - style(format!("{step}.")).cyan().bold() - ); - println!( - " {}", - style("export ANTHROPIC_API_KEY=\"sk-ant-...\"").yellow() - ); - println!( - " {}", - style( - "or: zeroclaw auth paste-token --provider anthropic --auth-kind authorization" - ) - .yellow() - ); - } else { - let env_var = provider_env_var(provider); - println!( - " {} Set your API key:", - style(format!("{step}.")).cyan().bold() - ); - println!( - " {}", - style(format!("export {env_var}=\"sk-...\"")).yellow() - ); + let provider = config.default_provider.as_deref(); + let canonical_provider = provider.map(canonical_provider_name); + if let (Some(p), Some(cp)) = (provider, canonical_provider) { + if config.api_key.is_none() && !provider_supports_keyless_local_usage(p) { + if cp == "copilot" { + println!( + " {} Authenticate GitHub Copilot:", + style(format!("{step}.")).cyan().bold() + ); + println!(" {}", style("zeroclaw agent -m \"Hello!\"").yellow()); + println!( + " {}", + style("(device/OAuth prompt appears automatically on first run)").dim() + ); + } else if cp == "openai-codex" { + println!( + " {} Authenticate OpenAI Codex:", + style(format!("{step}.")).cyan().bold() + ); + println!( + " {}", + style("zeroclaw auth login --provider openai-codex --device-code").yellow() + ); + } else if cp == "anthropic" { + println!( + " {} Configure Anthropic auth:", + style(format!("{step}.")).cyan().bold() + ); + println!( + " {}", + style("export ANTHROPIC_API_KEY=\"sk-ant-...\"").yellow() + ); + println!( + " {}", + style( + "or: zeroclaw auth paste-token --provider anthropic --auth-kind authorization" + ) + .yellow() + ); + } else { + let env_var = provider_env_var(p); + println!( + " {} Set your API key:", + style(format!("{step}.")).cyan().bold() + ); + println!( + " {}", + style(format!("export {env_var}=\"sk-...\"")).yellow() + ); + } + println!(); + step += 1; } - println!(); - step += 1; } // If channels are configured, show channel start as the primary next step diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index b28420322..b5208f0ac 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -1,5 +1,5 @@ import { useLocation } from 'react-router-dom'; -import { LogOut, Menu, PanelLeftClose, PanelLeftOpen } from 'lucide-react'; +import { LogOut, Menu } from 'lucide-react'; import { t, LANGUAGE_BUTTON_LABELS, LANGUAGE_SWITCH_ORDER } from '@/lib/i18n'; import { useLocaleContext } from '@/App'; import { useAuth } from '@/hooks/useAuth'; @@ -21,16 +21,10 @@ const routeTitles: Record = { const languageSummary = 'English · 简体中文 · 日本語 · Русский · Français · Tiếng Việt · Ελληνικά'; interface HeaderProps { - isSidebarCollapsed: boolean; onToggleSidebar: () => void; - onToggleSidebarCollapse: () => void; } -export default function Header({ - isSidebarCollapsed, - onToggleSidebar, - onToggleSidebarCollapse, -}: HeaderProps) { +export default function Header({ onToggleSidebar }: HeaderProps) { const location = useLocation(); const { logout } = useAuth(); const { locale, setAppLocale } = useLocaleContext(); @@ -64,22 +58,12 @@ export default function Header({ {pageTitle}

- Electric dashboard + ZeroClaw dashboard

- -