Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions rust/crates/commands/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,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: &[],
Expand Down Expand Up @@ -1102,6 +1109,7 @@ pub enum SlashCommand {
args: Option<String>,
},
Doctor,
Setup,
Login,
Logout,
Vim,
Expand Down Expand Up @@ -1223,6 +1231,7 @@ impl SlashCommand {
Self::Compact { .. } => "/compact",
Self::Cost => "/cost",
Self::Doctor => "/doctor",
Self::Setup => "/setup",
Self::Config { .. } => "/config",
Self::Memory { .. } => "/memory",
Self::History { .. } => "/history",
Expand Down Expand Up @@ -1392,6 +1401,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.",
Expand Down Expand Up @@ -1911,7 +1924,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",
Expand Down Expand Up @@ -5114,6 +5127,7 @@ pub fn handle_slash_command(
| SlashCommand::AddDir { .. }
| SlashCommand::History { .. }
| SlashCommand::Team { .. }
| SlashCommand::Setup
| SlashCommand::Unknown(_) => None,
}
}
Expand Down Expand Up @@ -5730,7 +5744,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);
}

Expand Down
66 changes: 66 additions & 0 deletions rust/crates/runtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,45 @@ pub struct RuntimeFeatureConfig {
sandbox: SandboxConfig,
provider_fallbacks: ProviderFallbackConfig,
trusted_roots: Vec<String>,
provider: RuntimeProviderConfig,
rules_import: RulesImportConfig,
}

/// 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<String>,
api_key: Option<String>,
base_url: Option<String>,
model: Option<String>,
}

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()
}
}

/// Controls which external AI coding framework rules are imported into the system prompt.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum RulesImportConfig {
Expand Down Expand Up @@ -729,6 +765,7 @@ fn build_runtime_config(
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)?,
rules_import: parse_optional_rules_import(&merged_value)?,
};

Expand Down Expand Up @@ -834,6 +871,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
Expand Down Expand Up @@ -919,6 +961,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
Expand Down Expand Up @@ -1794,6 +1841,25 @@ fn parse_optional_trusted_roots(root: &JsonValue) -> Result<Vec<String>, ConfigE
)
}

fn parse_optional_provider_config(root: &JsonValue) -> Result<RuntimeProviderConfig, ConfigError> {
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_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, ConfigError> {
let Some(object) = root.as_object() else {
return Ok(RulesImportConfig::default());
Expand Down
10 changes: 10 additions & 0 deletions rust/crates/runtime/src/config_validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ struct DeprecatedField {
replacement: &'static str,
}

/// Canonical list of valid top-level settings keys for `settings.json`.
///
/// This array is the single source of truth used by config validation; every
/// supported top-level key (including `subagentModel`, `provider`,
/// `trustedRoots`, `rulesImport`, etc.) must appear here with its expected
/// type. Add new top-level settings keys to this list.
const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
FieldSpec {
name: "$schema",
Expand Down Expand Up @@ -215,6 +221,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
name: "provider",
expected: FieldType::Object,
},
FieldSpec {
name: "subagentModel",
expected: FieldType::String,
},
FieldSpec {
name: "rulesImport",
expected: FieldType::RulesImport,
Expand Down
3 changes: 2 additions & 1 deletion rust/crates/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,14 @@ 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, ConfigFileReport,
ConfigFileStatus, ConfigInspection, ConfigLoader, ConfigSource, McpConfigCollection,
McpInvalidServerConfig, McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig,
McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
McpWebSocketServerConfig, OAuthConfig, ProviderFallbackConfig, ResolvedPermissionMode,
RulesImportConfig, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookCommand, RuntimeHookConfig,
RuntimePermissionRuleConfig, RuntimePluginConfig, ScopedMcpServerConfig,
RuntimePermissionRuleConfig, RuntimePluginConfig, RuntimeProviderConfig, ScopedMcpServerConfig,
CLAW_SETTINGS_SCHEMA_NAME,
};
pub use config_validate::{
Expand Down
45 changes: 43 additions & 2 deletions rust/crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
mod init;
mod input;
mod render;
mod setup_wizard;

use std::collections::BTreeSet;
use std::env;
Expand Down Expand Up @@ -1086,6 +1087,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
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.
Expand Down Expand Up @@ -1225,6 +1227,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 {
Expand Down Expand Up @@ -1288,6 +1293,7 @@ enum LocalHelpTopic {
Model,
Settings,
Diff,
Setup,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -1697,6 +1703,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),
Expand Down Expand Up @@ -2050,6 +2057,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;
Expand Down Expand Up @@ -2177,6 +2193,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,
Expand Down Expand Up @@ -2222,7 +2239,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 {
Expand All @@ -2242,6 +2259,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),
Expand Down Expand Up @@ -2296,6 +2314,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),
Expand Down Expand Up @@ -2348,6 +2367,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
Expand Down Expand Up @@ -2640,6 +2660,7 @@ fn suggest_similar_subcommand(input: &str) -> Option<Vec<String>> {
"status",
"sandbox",
"doctor",
"setup",
"state",
"dump-manifests",
"bootstrap-plan",
Expand Down Expand Up @@ -3573,6 +3594,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.
///
Expand Down Expand Up @@ -6380,7 +6406,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()),
}
}

Expand Down Expand Up @@ -7594,6 +7621,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
Expand Down Expand Up @@ -9570,6 +9603,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(),
}
}

Expand Down Expand Up @@ -9597,6 +9637,7 @@ fn local_help_topic_command(topic: LocalHelpTopic) -> &'static str {
LocalHelpTopic::Model => "models",
LocalHelpTopic::Settings => "settings",
LocalHelpTopic::Diff => "diff",
LocalHelpTopic::Setup => "setup",
}
}

Expand Down
Loading
Loading