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.
- 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.
- Add claw setup CLI subcommand.
- Add /setup slash command.
- Add Setup variant to SlashCommand enum.
- Add Setup to LocalHelpTopic enum.
- Add setup to diagnostic subcommand matching.
- Add subagentModel to TOP_LEVEL_FIELDS in config_validate.rs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-06-04 21:52:48 -05:00
parent 503d515f38
commit 3845040b9d
6 changed files with 192 additions and 188 deletions

View File

@@ -17,6 +17,7 @@
mod init;
mod input;
mod render;
mod setup_wizard;
use std::collections::BTreeSet;
use std::env;
@@ -1095,6 +1096,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
CliAction::SessionList { output_format } => run_session_list(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.
@@ -1238,6 +1240,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 {
@@ -1301,6 +1306,7 @@ enum LocalHelpTopic {
Model,
Settings,
Diff,
Setup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1765,6 +1771,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
"doctor" => Some(LocalHelpTopic::Doctor),
"acp" => Some(LocalHelpTopic::Acp),
"init" => Some(LocalHelpTopic::Init),
"setup" => Some(LocalHelpTopic::Setup),
"state" => Some(LocalHelpTopic::State),
"resume" => Some(LocalHelpTopic::Resume),
"session" => Some(LocalHelpTopic::Session),
@@ -2144,6 +2151,15 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}
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 mut read_stdin = false;
@@ -2271,6 +2287,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,
@@ -2316,7 +2333,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 {
@@ -2336,6 +2353,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),
@@ -2390,6 +2408,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),
@@ -2452,6 +2471,7 @@ fn parse_single_word_command_alias(
.map(PermissionModeProvenance::from_flag)
.unwrap_or_else(permission_mode_provenance_for_current_dir),
})),
"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
@@ -2744,6 +2764,7 @@ fn suggest_similar_subcommand(input: &str) -> Option<Vec<String>> {
"status",
"sandbox",
"doctor",
"setup",
"state",
"dump-manifests",
"bootstrap-plan",
@@ -3717,6 +3738,11 @@ fn run_doctor(
Ok(())
}
/// Run the interactive setup wizard to configure provider, API key, and model.
fn run_setup() -> Result<(), Box<dyn std::error::Error>> {
setup_wizard::run_setup_wizard()
}
/// Starts a minimal Model Context Protocol server that exposes claw's
/// built-in tools over stdio.
///
@@ -6888,7 +6914,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()),
}
}
@@ -8149,6 +8176,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
@@ -10218,6 +10251,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(),
}
}
@@ -10245,6 +10285,7 @@ fn local_help_topic_command(topic: LocalHelpTopic) -> &'static str {
LocalHelpTopic::Model => "models",
LocalHelpTopic::Settings => "settings",
LocalHelpTopic::Diff => "diff",
LocalHelpTopic::Setup => "setup",
}
}

View File

@@ -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<dyn std::error::Error>> {
let model = prompt_model(&kind, &current)?;
let fast_model = prompt_fast_model(&current, 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<dyn std::error::Error>> {
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<String, Box<dyn st
let current_kind = current.kind().unwrap_or("anthropic");
println!(" \x1b[1mProvider\x1b[0m");
for (num, label, kind) in PROVIDERS {
let marker = if *kind == current_kind { " (current)" } else { "" };
let marker = if *kind == current_kind {
" (current)"
} else {
""
};
println!(" [{num}] {label}{marker}");
}
let default = PROVIDERS
@@ -129,9 +134,7 @@ fn prompt_api_key(
};
// Check if env var is already set
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 key)");
}
@@ -144,7 +147,9 @@ fn prompt_api_key(
};
if key.is_empty() && !env_set {
eprintln!(" \x1b[33mWarning: no API key configured. Set {env_var} or re-run setup.\x1b[0m");
eprintln!(
" \x1b[33mWarning: no API key configured. Set {env_var} or re-run setup.\x1b[0m"
);
}
Ok(key)
@@ -174,9 +179,7 @@ fn prompt_base_url(
"dashscope" => "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<dyn std::erro
};
if let Some(obj) = settings.as_object_mut() {
obj.insert(field.to_string(), serde_json::Value::String(value.to_string()));
obj.insert(
field.to_string(),
serde_json::Value::String(value.to_string()),
);
}
std::fs::create_dir_all(&settings_dir)?;