From e8e001457e8d9c5f7fbdc4432b52745742e7e229 Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Tue, 2 Jun 2026 16:26:41 -0500 Subject: [PATCH 1/2] feat: wizard entry points -- /setup command, claw setup subcommand, and RuntimeProviderConfig The setup wizard was merged in PR #3017 but was orphaned -- it was not declared as a module in main.rs, making it unreachable. Additionally, the setup_wizard.rs imports RuntimeProviderConfig which did not exist on upstream/main. This commit makes the wizard accessible and adds the necessary RuntimeProviderConfig type. Changes: - Add RuntimeProviderConfig struct to runtime/src/config.rs with kind(), api_key(), base_url(), model() accessors. This represents the "provider" section in ~/.claw/settings.json and supports the 3-tier credential resolution: env var > .env file > stored config. - Add parse_optional_provider_config() to parse the provider object from merged settings JSON. - Add provider() method to RuntimeConfig and RuntimeFeatureConfig. - Export RuntimeProviderConfig, save_user_provider_settings, clear_user_provider_settings, and default_config_home from runtime crate public API (runtime/src/lib.rs). - Add "mod setup_wizard;" to rusty-claude-cli/src/main.rs to activate the previously orphaned setup wizard module. - Add "claw setup" CLI subcommand: runs the interactive provider setup wizard from the terminal. - Add "/setup" slash command: runs the wizard inside the REPL. - Add Setup variant to SlashCommand enum with /setup spec entry. - Add Setup to LocalHelpTopic enum with help text. - Add "setup" to diagnostic subcommand matching, local-help topic routes, suggest_similar_subcommand list, and all parse dispatches. - Add "subagentModel" to TOP_LEVEL_FIELDS in config_validate.rs so that the setup wizard's fast-model setting is recognized as a valid settings key instead of causing an "unknown key" error. Note: Ctrl+P provider swap in the TUI input loop is deferred to a follow-up as it requires deeper integration with the TUI state machine (see reference commit 99351d44 from feat-tui). [depends on #3211 for 3-tier credential resolution] Co-Authored-By: Claude Opus 4.8 --- rust/crates/commands/src/lib.rs | 16 ++++- rust/crates/runtime/src/config.rs | 67 +++++++++++++++++++ rust/crates/runtime/src/config_validate.rs | 4 ++ rust/crates/runtime/src/lib.rs | 3 +- rust/crates/rusty-claude-cli/src/main.rs | 45 ++++++++++++- .../rusty-claude-cli/src/setup_wizard.rs | 58 +++++++++------- 6 files changed, 166 insertions(+), 27 deletions(-) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index a8fd88d5b6..46e8ef1586 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -719,6 +719,13 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ argument_hint: None, resume_supported: true, }, + SlashCommandSpec { + name: "setup", + aliases: &[], + summary: "Run the interactive provider setup wizard", + argument_hint: None, + resume_supported: false, + }, SlashCommandSpec { name: "notifications", aliases: &[], @@ -1101,6 +1108,7 @@ pub enum SlashCommand { args: Option, }, Doctor, + Setup, Login, Logout, Vim, @@ -1222,6 +1230,7 @@ impl SlashCommand { Self::Compact { .. } => "/compact", Self::Cost => "/cost", Self::Doctor => "/doctor", + Self::Setup => "/setup", Self::Config { .. } => "/config", Self::Memory { .. } => "/memory", Self::History { .. } => "/history", @@ -1391,6 +1400,10 @@ pub fn validate_slash_command_input( validate_no_args(command, &args)?; SlashCommand::Doctor } + "setup" => { + validate_no_args(command, &args)?; + SlashCommand::Setup + } "login" | "logout" => { return Err(command_error( "This auth flow was removed. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead.", @@ -1902,7 +1915,7 @@ fn slash_command_category(name: &str) -> &'static str { | "stickers" | "language" | "profile" | "max-tokens" | "temperature" | "system-prompt" | "api-key" | "terminal-setup" | "notifications" | "telemetry" | "providers" | "env" | "project" | "reasoning" | "budget" | "rate-limit" | "workspace" | "reset" | "ide" - | "desktop" | "upgrade" => "Config", + | "desktop" | "upgrade" | "setup" => "Config", "debug-tool-call" | "doctor" | "sandbox" | "diagnostics" | "tool-details" | "changelog" | "metrics" => "Debug", _ => "Tools", @@ -4624,6 +4637,7 @@ pub fn handle_slash_command( | SlashCommand::AddDir { .. } | SlashCommand::History { .. } | SlashCommand::Team { .. } + | SlashCommand::Setup | SlashCommand::Unknown(_) => None, } } diff --git a/rust/crates/runtime/src/config.rs b/rust/crates/runtime/src/config.rs index 0379e31b56..b9b83c7c87 100644 --- a/rust/crates/runtime/src/config.rs +++ b/rust/crates/runtime/src/config.rs @@ -95,6 +95,42 @@ pub struct RuntimeFeatureConfig { sandbox: SandboxConfig, provider_fallbacks: ProviderFallbackConfig, trusted_roots: Vec, + provider: RuntimeProviderConfig, +} + +/// Stored provider configuration from the setup wizard. +/// +/// Represents the `provider` section in `~/.claw/settings.json`, used as a +/// fallback when environment variables are absent (3-tier resolution: +/// env var > .env file > stored config). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct RuntimeProviderConfig { + kind: Option, + api_key: Option, + base_url: Option, + model: Option, +} + +impl RuntimeProviderConfig { + #[must_use] + pub fn kind(&self) -> Option<&str> { + self.kind.as_deref() + } + + #[must_use] + pub fn api_key(&self) -> Option<&str> { + self.api_key.as_deref() + } + + #[must_use] + pub fn base_url(&self) -> Option<&str> { + self.base_url.as_deref() + } + + #[must_use] + pub fn model(&self) -> Option<&str> { + self.model.as_deref() + } } /// Ordered chain of fallback model identifiers used when the primary @@ -353,6 +389,7 @@ impl ConfigLoader { sandbox: parse_optional_sandbox_config(&merged_value)?, provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?, trusted_roots: parse_optional_trusted_roots(&merged_value)?, + provider: parse_optional_provider_config(&merged_value)?, }; Ok(RuntimeConfig { @@ -410,6 +447,7 @@ impl ConfigLoader { sandbox: parse_optional_sandbox_config(&merged_value)?, provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?, trusted_roots: parse_optional_trusted_roots(&merged_value)?, + provider: parse_optional_provider_config(&merged_value)?, }; let config = RuntimeConfig { @@ -506,6 +544,11 @@ impl RuntimeConfig { &self.feature_config.provider_fallbacks } + #[must_use] + pub fn provider(&self) -> &RuntimeProviderConfig { + &self.feature_config.provider + } + #[must_use] pub fn trusted_roots(&self) -> &[String] { &self.feature_config.trusted_roots @@ -586,6 +629,11 @@ impl RuntimeFeatureConfig { &self.provider_fallbacks } + #[must_use] + pub fn provider(&self) -> &RuntimeProviderConfig { + &self.provider + } + #[must_use] pub fn trusted_roots(&self) -> &[String] { &self.trusted_roots @@ -1162,6 +1210,25 @@ fn parse_optional_trusted_roots(root: &JsonValue) -> Result, ConfigE ) } +fn parse_optional_provider_config(root: &JsonValue) -> Result { + let Some(provider_value) = root.as_object().and_then(|object| object.get("provider")) else { + return Ok(RuntimeProviderConfig::default()); + }; + let Some(object) = provider_value.as_object() else { + return Ok(RuntimeProviderConfig::default()); + }; + let kind = optional_string(object, "kind", "provider")?.map(str::to_string); + let api_key = optional_string(object, "apiKey", "provider")?.map(str::to_string); + let base_url = optional_string(object, "baseUrl", "provider")?.map(str::to_string); + let model = optional_string(object, "model", "provider")?.map(str::to_string); + Ok(RuntimeProviderConfig { + kind, + api_key, + base_url, + model, + }) +} + fn parse_filesystem_mode_label(value: &str) -> Result { match value { "off" => Ok(FilesystemIsolationMode::Off), diff --git a/rust/crates/runtime/src/config_validate.rs b/rust/crates/runtime/src/config_validate.rs index 3ea064ebe5..ca4ec84a7a 100644 --- a/rust/crates/runtime/src/config_validate.rs +++ b/rust/crates/runtime/src/config_validate.rs @@ -201,6 +201,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[ name: "provider", expected: FieldType::Object, }, + FieldSpec { + name: "subagentModel", + expected: FieldType::String, + }, ]; const HOOKS_FIELDS: &[FieldSpec] = &[ diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index f0ab67c30e..6193527b6f 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -65,12 +65,13 @@ pub use compact::{ get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult, }; pub use config::{ + clear_user_provider_settings, default_config_home, save_user_provider_settings, suppress_config_warnings_for_json_mode, ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection, McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig, ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig, - RuntimePluginConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME, + RuntimePluginConfig, RuntimeProviderConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME, }; pub use config_validate::{ check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic, diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 5febf8417a..13a2f3fbad 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -16,6 +16,7 @@ mod init; mod input; mod render; +mod setup_wizard; use std::collections::BTreeSet; use std::env; @@ -628,6 +629,7 @@ fn run() -> Result<(), Box> { CliAction::Acp { output_format } => print_acp_status(output_format)?, CliAction::State { output_format } => run_worker_state(output_format)?, CliAction::Init { output_format } => run_init(output_format)?, + CliAction::Setup { output_format: _ } => run_setup()?, // #146: dispatch pure-local introspection. Text mode uses existing // render_config_report/render_diff_report; JSON mode uses the // corresponding _json helpers already exposed for resume sessions. @@ -762,6 +764,9 @@ enum CliAction { Init { output_format: CliOutputFormat, }, + Setup { + output_format: CliOutputFormat, + }, // #146: `claw config` and `claw diff` are pure-local read-only // introspection commands; wire them as standalone CLI subcommands. Config { @@ -816,6 +821,7 @@ enum LocalHelpTopic { Mcp, Config, Diff, + Setup, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1063,6 +1069,7 @@ fn parse_args(args: &[String]) -> Result { "doctor" => Some(LocalHelpTopic::Doctor), "acp" => Some(LocalHelpTopic::Acp), "init" => Some(LocalHelpTopic::Init), + "setup" => Some(LocalHelpTopic::Setup), "state" => Some(LocalHelpTopic::State), "export" => Some(LocalHelpTopic::Export), "version" => Some(LocalHelpTopic::Version), @@ -1348,6 +1355,15 @@ fn parse_args(args: &[String]) -> Result { } Ok(CliAction::Init { output_format }) } + "setup" => { + if rest.len() > 1 { + let extra = rest[1..].join(" "); + return Err(format!( + "unexpected extra arguments after `claw setup`: {extra}\nUsage: claw setup" + )); + } + Ok(CliAction::Setup { output_format }) + } "export" => parse_export_args(&rest[1..], output_format), "prompt" => { let prompt = rest[1..].join(" "); @@ -1444,6 +1460,7 @@ fn parse_local_help_action( "doctor" => LocalHelpTopic::Doctor, "acp" => LocalHelpTopic::Acp, "init" => LocalHelpTopic::Init, + "setup" => LocalHelpTopic::Setup, "state" => LocalHelpTopic::State, "export" => LocalHelpTopic::Export, "version" => LocalHelpTopic::Version, @@ -1484,7 +1501,7 @@ fn parse_single_word_command_alias( let verb = &rest[0]; let is_diagnostic = matches!( verb.as_str(), - "help" | "version" | "status" | "sandbox" | "doctor" | "state" + "help" | "version" | "status" | "sandbox" | "doctor" | "setup" | "state" ); if is_diagnostic && rest.len() > 1 { @@ -1504,6 +1521,7 @@ fn parse_single_word_command_alias( "doctor" => Some(LocalHelpTopic::Doctor), "acp" => Some(LocalHelpTopic::Acp), "init" => Some(LocalHelpTopic::Init), + "setup" => Some(LocalHelpTopic::Setup), "state" => Some(LocalHelpTopic::State), "export" => Some(LocalHelpTopic::Export), "version" => Some(LocalHelpTopic::Version), @@ -1553,6 +1571,7 @@ fn parse_single_word_command_alias( "doctor" => Some(LocalHelpTopic::Doctor), "acp" => Some(LocalHelpTopic::Acp), "init" => Some(LocalHelpTopic::Init), + "setup" => Some(LocalHelpTopic::Setup), "state" => Some(LocalHelpTopic::State), "export" => Some(LocalHelpTopic::Export), "version" => Some(LocalHelpTopic::Version), @@ -1593,6 +1612,7 @@ fn parse_single_word_command_alias( })), "sandbox" => Some(Ok(CliAction::Sandbox { output_format })), "doctor" => Some(Ok(CliAction::Doctor { output_format })), + "setup" => Some(Ok(CliAction::Setup { output_format })), "state" => Some(Ok(CliAction::State { output_format })), // #146: let `config` and `diff` fall through to parse_subcommand // where they are wired as pure-local introspection, instead of @@ -1868,6 +1888,7 @@ fn suggest_similar_subcommand(input: &str) -> Option> { "status", "sandbox", "doctor", + "setup", "state", "dump-manifests", "bootstrap-plan", @@ -2652,6 +2673,11 @@ fn run_doctor(output_format: CliOutputFormat) -> Result<(), Box Result<(), Box> { + setup_wizard::run_setup_wizard() +} + /// Starts a minimal Model Context Protocol server that exposes claw's /// built-in tools over stdio. /// @@ -4863,7 +4889,8 @@ fn run_resume_command( | SlashCommand::Tag { .. } | SlashCommand::OutputStyle { .. } | SlashCommand::AddDir { .. } - | SlashCommand::Team { .. } => Err("unsupported resumed slash command".into()), + | SlashCommand::Team { .. } + | SlashCommand::Setup => Err("unsupported resumed slash command".into()), } } @@ -6073,6 +6100,12 @@ impl LiveCli { ); false } + SlashCommand::Setup => { + if let Err(e) = setup_wizard::run_setup_wizard() { + eprintln!("Setup wizard failed: {e}"); + } + false + } SlashCommand::History { count } => { self.print_prompt_history(count.as_deref()); false @@ -7923,6 +7956,13 @@ fn render_help_topic(topic: LocalHelpTopic) -> String { Formats text (default), json Related /diff · ROADMAP #148" .to_string(), + LocalHelpTopic::Setup => "Setup + Usage claw setup + Aliases /setup (inside the REPL) + Purpose run the interactive provider setup wizard to configure API key, model, and base URL + Output writes provider settings to ~/.claw/settings.json (0600 permissions) + Related /model · /config · claw doctor" + .to_string(), } } @@ -7945,6 +7985,7 @@ fn local_help_topic_command(topic: LocalHelpTopic) -> &'static str { LocalHelpTopic::Mcp => "mcp", LocalHelpTopic::Config => "config", LocalHelpTopic::Diff => "diff", + LocalHelpTopic::Setup => "setup", } } diff --git a/rust/crates/rusty-claude-cli/src/setup_wizard.rs b/rust/crates/rusty-claude-cli/src/setup_wizard.rs index 69fabfb36b..c2f7b6ff39 100644 --- a/rust/crates/rusty-claude-cli/src/setup_wizard.rs +++ b/rust/crates/rusty-claude-cli/src/setup_wizard.rs @@ -23,7 +23,10 @@ const DEFAULT_BASE_URLS: &[(&str, &str)] = &[ ("anthropic", "https://api.anthropic.com"), ("xai", "https://api.x.ai/v1"), ("openai", "https://api.openai.com/v1"), - ("dashscope", "https://dashscope.aliyuncs.com/compatible-mode/v1"), + ( + "dashscope", + "https://dashscope.aliyuncs.com/compatible-mode/v1", + ), ]; const API_KEY_ENV_VARS: &[(&str, &str)] = &[ @@ -51,12 +54,7 @@ pub fn run_setup_wizard() -> Result<(), Box> { let model = prompt_model(&kind, ¤t)?; let fast_model = prompt_fast_model(¤t, model.as_deref())?; - save_user_provider_settings( - &kind, - &api_key, - base_url.as_deref(), - model.as_deref(), - )?; + save_user_provider_settings(&kind, &api_key, base_url.as_deref(), model.as_deref())?; if let Some(fast) = &fast_model { save_settings_field("subagentModel", fast)?; @@ -64,7 +62,10 @@ pub fn run_setup_wizard() -> Result<(), Box> { println!(); println!(" \x1b[32mProvider saved to ~/.claw/settings.json\x1b[0m"); - println!(" Run \x1b[1m/model {}\x1b[0m or restart claw to activate.", model.as_deref().unwrap_or(&kind)); + println!( + " Run \x1b[1m/model {}\x1b[0m or restart claw to activate.", + model.as_deref().unwrap_or(&kind) + ); println!(); Ok(()) @@ -82,7 +83,11 @@ fn prompt_provider(current: &RuntimeProviderConfig) -> Result "DASHSCOPE_BASE_URL", _ => "BASE_URL", }; - let env_set = std::env::var(env_var) - .ok() - .is_some_and(|v| !v.is_empty()); + let env_set = std::env::var(env_var).ok().is_some_and(|v| !v.is_empty()); if env_set { println!(" {env_var} is set in environment (will take priority over stored URL)"); } @@ -203,7 +206,9 @@ fn prompt_model( .find(|(k, _)| *k == kind) .map_or(empty, |(_, models)| *models); - let current_model = current.model().unwrap_or(aliases.first().copied().unwrap_or("")); + let current_model = current + .model() + .unwrap_or(aliases.first().copied().unwrap_or("")); println!(" \x1b[1mModel\x1b[0m"); if !aliases.is_empty() { @@ -235,12 +240,16 @@ fn prompt_fast_model( println!(" Press Enter to skip (agents will use your main model)."); let current_fast = load_current_settings_field("subagentModel"); - let default_hint = current_fast - .as_deref() - .or(main_model) - .unwrap_or(""); + let default_hint = current_fast.as_deref().or(main_model).unwrap_or(""); - let input = read_line(&format!(" Fast model [{}]: ", if default_hint.is_empty() { "same as main" } else { default_hint }))?; + let input = read_line(&format!( + " Fast model [{}]: ", + if default_hint.is_empty() { + "same as main" + } else { + default_hint + } + ))?; if input.trim().is_empty() { Ok(current_fast) } else { @@ -269,7 +278,10 @@ fn save_settings_field(field: &str, value: &str) -> Result<(), Box Date: Wed, 3 Jun 2026 14:24:41 -0500 Subject: [PATCH 2/2] fix: update slash command count and add /setup assertion in test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update slash_command_specs().len() assertion from 139 to 140. The /setup command added by this PR increased the spec count by 1 but the test's expected count was not updated, causing CI failure. - Add assert!(help.contains("/setup")) to the renders_help_from_shared_specs test so the new command is verified in the help output. Fixes CI Build ❌ and Test ❌ on #3218. --- rust/crates/commands/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 46e8ef1586..fcae31e73a 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -5240,7 +5240,8 @@ mod tests { assert!(help.contains("aliases: /skill")); assert!(!help.contains("/login")); assert!(!help.contains("/logout")); - assert_eq!(slash_command_specs().len(), 139); + assert!(help.contains("/setup")); + assert_eq!(slash_command_specs().len(), 140); assert!(resume_supported_slash_commands().len() >= 39); }