mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-09 05:36:45 +00:00
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:
@@ -720,6 +720,13 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
},
|
},
|
||||||
|
SlashCommandSpec {
|
||||||
|
name: "setup",
|
||||||
|
aliases: &[],
|
||||||
|
summary: "Run the interactive provider setup wizard",
|
||||||
|
argument_hint: None,
|
||||||
|
resume_supported: false,
|
||||||
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "notifications",
|
name: "notifications",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -1102,6 +1109,7 @@ pub enum SlashCommand {
|
|||||||
args: Option<String>,
|
args: Option<String>,
|
||||||
},
|
},
|
||||||
Doctor,
|
Doctor,
|
||||||
|
Setup,
|
||||||
Login,
|
Login,
|
||||||
Logout,
|
Logout,
|
||||||
Vim,
|
Vim,
|
||||||
@@ -1223,6 +1231,7 @@ impl SlashCommand {
|
|||||||
Self::Compact { .. } => "/compact",
|
Self::Compact { .. } => "/compact",
|
||||||
Self::Cost => "/cost",
|
Self::Cost => "/cost",
|
||||||
Self::Doctor => "/doctor",
|
Self::Doctor => "/doctor",
|
||||||
|
Self::Setup => "/setup",
|
||||||
Self::Config { .. } => "/config",
|
Self::Config { .. } => "/config",
|
||||||
Self::Memory { .. } => "/memory",
|
Self::Memory { .. } => "/memory",
|
||||||
Self::History { .. } => "/history",
|
Self::History { .. } => "/history",
|
||||||
@@ -1392,6 +1401,10 @@ pub fn validate_slash_command_input(
|
|||||||
validate_no_args(command, &args)?;
|
validate_no_args(command, &args)?;
|
||||||
SlashCommand::Doctor
|
SlashCommand::Doctor
|
||||||
}
|
}
|
||||||
|
"setup" => {
|
||||||
|
validate_no_args(command, &args)?;
|
||||||
|
SlashCommand::Setup
|
||||||
|
}
|
||||||
"login" | "logout" => {
|
"login" | "logout" => {
|
||||||
return Err(command_error(
|
return Err(command_error(
|
||||||
"This auth flow was removed. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead.",
|
"This auth flow was removed. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead.",
|
||||||
@@ -1914,7 +1927,7 @@ fn slash_command_category(name: &str) -> &'static str {
|
|||||||
| "stickers" | "language" | "profile" | "max-tokens" | "temperature" | "system-prompt"
|
| "stickers" | "language" | "profile" | "max-tokens" | "temperature" | "system-prompt"
|
||||||
| "api-key" | "terminal-setup" | "notifications" | "telemetry" | "providers" | "env"
|
| "api-key" | "terminal-setup" | "notifications" | "telemetry" | "providers" | "env"
|
||||||
| "project" | "reasoning" | "budget" | "rate-limit" | "workspace" | "reset" | "ide"
|
| "project" | "reasoning" | "budget" | "rate-limit" | "workspace" | "reset" | "ide"
|
||||||
| "desktop" | "upgrade" => "Config",
|
| "desktop" | "upgrade" | "setup" => "Config",
|
||||||
"debug-tool-call" | "doctor" | "sandbox" | "diagnostics" | "tool-details" | "changelog"
|
"debug-tool-call" | "doctor" | "sandbox" | "diagnostics" | "tool-details" | "changelog"
|
||||||
| "metrics" => "Debug",
|
| "metrics" => "Debug",
|
||||||
_ => "Tools",
|
_ => "Tools",
|
||||||
@@ -5381,6 +5394,7 @@ pub fn handle_slash_command(
|
|||||||
| SlashCommand::AddDir { .. }
|
| SlashCommand::AddDir { .. }
|
||||||
| SlashCommand::History { .. }
|
| SlashCommand::History { .. }
|
||||||
| SlashCommand::Team { .. }
|
| SlashCommand::Team { .. }
|
||||||
|
| SlashCommand::Setup
|
||||||
| SlashCommand::Unknown(_) => None,
|
| SlashCommand::Unknown(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,10 +162,46 @@ pub struct RuntimeFeatureConfig {
|
|||||||
trusted_roots: Vec<String>,
|
trusted_roots: Vec<String>,
|
||||||
api_timeout: ApiTimeoutConfig,
|
api_timeout: ApiTimeoutConfig,
|
||||||
rules_import: RulesImportConfig,
|
rules_import: RulesImportConfig,
|
||||||
|
provider: RuntimeProviderConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls which external AI coding framework rules are imported into the system prompt.
|
/// Controls which external AI coding framework rules are imported into the system prompt.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum RulesImportConfig {
|
pub enum RulesImportConfig {
|
||||||
/// Import from all supported frameworks when files are detected.
|
/// Import from all supported frameworks when files are detected.
|
||||||
#[default]
|
#[default]
|
||||||
@@ -764,6 +800,7 @@ fn build_runtime_config(
|
|||||||
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
|
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
|
||||||
api_timeout: parse_optional_api_timeout_config(&merged_value)?,
|
api_timeout: parse_optional_api_timeout_config(&merged_value)?,
|
||||||
rules_import: parse_optional_rules_import(&merged_value)?,
|
rules_import: parse_optional_rules_import(&merged_value)?,
|
||||||
|
provider: parse_optional_provider_config(&merged_value)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(RuntimeConfig {
|
Ok(RuntimeConfig {
|
||||||
@@ -891,6 +928,13 @@ impl RuntimeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeFeatureConfig {
|
impl RuntimeFeatureConfig {
|
||||||
|
/// Parsed provider configuration (kind, apiKey, baseUrl, model) from
|
||||||
|
/// merged settings.
|
||||||
|
#[must_use]
|
||||||
|
pub fn provider(&self) -> &RuntimeProviderConfig {
|
||||||
|
&self.provider
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_hooks(mut self, hooks: RuntimeHookConfig) -> Self {
|
pub fn with_hooks(mut self, hooks: RuntimeHookConfig) -> Self {
|
||||||
self.hooks = hooks;
|
self.hooks = hooks;
|
||||||
@@ -2104,6 +2148,25 @@ fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, Co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
|
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
|
||||||
match value {
|
match value {
|
||||||
"off" => Ok(FilesystemIsolationMode::Off),
|
"off" => Ok(FilesystemIsolationMode::Off),
|
||||||
|
|||||||
@@ -92,8 +92,6 @@ enum FieldType {
|
|||||||
Bool,
|
Bool,
|
||||||
Object,
|
Object,
|
||||||
StringArray,
|
StringArray,
|
||||||
HookArray,
|
|
||||||
RulesImport,
|
|
||||||
Number,
|
Number,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +102,6 @@ impl FieldType {
|
|||||||
Self::Bool => "a boolean",
|
Self::Bool => "a boolean",
|
||||||
Self::Object => "an object",
|
Self::Object => "an object",
|
||||||
Self::StringArray => "an array of strings",
|
Self::StringArray => "an array of strings",
|
||||||
Self::RulesImport => "a string or an array of strings",
|
|
||||||
Self::HookArray => "an array of strings or hook objects",
|
|
||||||
Self::Number => "a number",
|
Self::Number => "a number",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,13 +114,6 @@ impl FieldType {
|
|||||||
Self::StringArray => value
|
Self::StringArray => value
|
||||||
.as_array()
|
.as_array()
|
||||||
.is_some_and(|arr| arr.iter().all(|v| v.as_str().is_some())),
|
.is_some_and(|arr| arr.iter().all(|v| v.as_str().is_some())),
|
||||||
Self::HookArray => true,
|
|
||||||
Self::RulesImport => {
|
|
||||||
value.as_str().is_some()
|
|
||||||
|| value
|
|
||||||
.as_array()
|
|
||||||
.is_some_and(|arr| arr.iter().all(|v| v.as_str().is_some()))
|
|
||||||
}
|
|
||||||
Self::Number => value.as_i64().is_some(),
|
Self::Number => value.as_i64().is_some(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,23 +202,23 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
|||||||
expected: FieldType::Object,
|
expected: FieldType::Object,
|
||||||
},
|
},
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "rulesImport",
|
name: "subagentModel",
|
||||||
expected: FieldType::RulesImport,
|
expected: FieldType::String,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const HOOKS_FIELDS: &[FieldSpec] = &[
|
const HOOKS_FIELDS: &[FieldSpec] = &[
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "PreToolUse",
|
name: "PreToolUse",
|
||||||
expected: FieldType::HookArray,
|
expected: FieldType::StringArray,
|
||||||
},
|
},
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "PostToolUse",
|
name: "PostToolUse",
|
||||||
expected: FieldType::HookArray,
|
expected: FieldType::StringArray,
|
||||||
},
|
},
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "PostToolUseFailure",
|
name: "PostToolUseFailure",
|
||||||
expected: FieldType::HookArray,
|
expected: FieldType::StringArray,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -421,10 +410,9 @@ fn validate_object_keys(
|
|||||||
} else if DEPRECATED_FIELDS.iter().any(|d| d.name == key) {
|
} else if DEPRECATED_FIELDS.iter().any(|d| d.name == key) {
|
||||||
// Deprecated key — handled separately, not an unknown-key error.
|
// Deprecated key — handled separately, not an unknown-key error.
|
||||||
} else {
|
} else {
|
||||||
// Unknown key — preserve compatibility by surfacing it as a warning
|
// Unknown key.
|
||||||
// instead of blocking otherwise valid config files.
|
|
||||||
let suggestion = suggest_field(key, &known_names);
|
let suggestion = suggest_field(key, &known_names);
|
||||||
result.warnings.push(ConfigDiagnostic {
|
result.errors.push(ConfigDiagnostic {
|
||||||
path: path_display.to_string(),
|
path: path_display.to_string(),
|
||||||
field: field_path,
|
field: field_path,
|
||||||
line: find_key_line(source, key),
|
line: find_key_line(source, key),
|
||||||
@@ -436,56 +424,8 @@ fn validate_object_keys(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emit deprecation warnings for bare string hook entries in the hooks object.
|
|
||||||
/// Legacy `["command-string"]` arrays still load but suggest migration to the
|
|
||||||
/// structured `{matcher, hooks:[{type, command}]}` form.
|
|
||||||
fn validate_hook_entry_format(
|
|
||||||
hooks: &BTreeMap<String, JsonValue>,
|
|
||||||
source: &str,
|
|
||||||
path_display: &str,
|
|
||||||
) -> ValidationResult {
|
|
||||||
let mut result = ValidationResult {
|
|
||||||
errors: Vec::new(),
|
|
||||||
warnings: Vec::new(),
|
|
||||||
};
|
|
||||||
for spec in HOOKS_FIELDS {
|
|
||||||
let Some(value) = hooks.get(spec.name) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(array) = value.as_array() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
for item in array {
|
|
||||||
if item.as_str().is_some() {
|
|
||||||
result.warnings.push(ConfigDiagnostic {
|
|
||||||
path: path_display.to_string(),
|
|
||||||
field: format!("hooks.{}", spec.name),
|
|
||||||
line: find_key_line(source, spec.name),
|
|
||||||
kind: DiagnosticKind::Deprecated {
|
|
||||||
replacement: "object-style hook entries with hooks:[{type:\"command\",command:\"...\"}]",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// One deprecation warning per event is enough
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn suggest_field(input: &str, candidates: &[&str]) -> Option<String> {
|
fn suggest_field(input: &str, candidates: &[&str]) -> Option<String> {
|
||||||
let input_lower = input.to_ascii_lowercase();
|
let input_lower = input.to_ascii_lowercase();
|
||||||
// #461: prefix-aware matching — if input is a prefix of a candidate,
|
|
||||||
// treat it as distance 0 (perfect prefix match) to avoid edit-distance
|
|
||||||
// misranking (e.g., "mcp" → "env" instead of "mcpServers").
|
|
||||||
let prefix_match = candidates
|
|
||||||
.iter()
|
|
||||||
.filter(|c| c.to_ascii_lowercase().starts_with(&input_lower))
|
|
||||||
.min_by_key(|c| c.len())
|
|
||||||
.map(|name| name.to_string());
|
|
||||||
if prefix_match.is_some() {
|
|
||||||
return prefix_match;
|
|
||||||
}
|
|
||||||
candidates
|
candidates
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|candidate| {
|
.filter_map(|candidate| {
|
||||||
@@ -555,7 +495,6 @@ pub fn validate_config_file(
|
|||||||
source,
|
source,
|
||||||
&path_display,
|
&path_display,
|
||||||
));
|
));
|
||||||
result.merge(validate_hook_entry_format(hooks, source, &path_display));
|
|
||||||
}
|
}
|
||||||
if let Some(permissions) = object.get("permissions").and_then(JsonValue::as_object) {
|
if let Some(permissions) = object.get("permissions").and_then(JsonValue::as_object) {
|
||||||
result.merge(validate_object_keys(
|
result.merge(validate_object_keys(
|
||||||
@@ -652,11 +591,10 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(result.errors.is_empty());
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.warnings.len(), 1);
|
assert_eq!(result.errors[0].field, "unknownField");
|
||||||
assert_eq!(result.warnings[0].field, "unknownField");
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
result.warnings[0].kind,
|
result.errors[0].kind,
|
||||||
DiagnosticKind::UnknownKey { .. }
|
DiagnosticKind::UnknownKey { .. }
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -736,10 +674,9 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(result.errors.is_empty());
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.warnings.len(), 1);
|
assert_eq!(result.errors[0].line, Some(3));
|
||||||
assert_eq!(result.warnings[0].line, Some(3));
|
assert_eq!(result.errors[0].field, "badKey");
|
||||||
assert_eq!(result.warnings[0].field, "badKey");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -760,7 +697,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn validates_nested_hooks_keys() {
|
fn validates_nested_hooks_keys() {
|
||||||
// given
|
// given
|
||||||
let source = r#"{"hooks": {"PreToolUse": [{"hooks":[{"type":"command","command":"cmd"}]}], "BadHook": ["x"]}}"#;
|
let source = r#"{"hooks": {"PreToolUse": ["cmd"], "BadHook": ["x"]}}"#;
|
||||||
let parsed = JsonValue::parse(source).expect("valid json");
|
let parsed = JsonValue::parse(source).expect("valid json");
|
||||||
let object = parsed.as_object().expect("object");
|
let object = parsed.as_object().expect("object");
|
||||||
|
|
||||||
@@ -768,64 +705,8 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(result.errors.is_empty());
|
|
||||||
assert_eq!(
|
|
||||||
result.warnings.len(),
|
|
||||||
1,
|
|
||||||
"expected only the unknown key warning, got {:?}",
|
|
||||||
result.warnings
|
|
||||||
);
|
|
||||||
assert_eq!(result.warnings[0].field, "hooks.BadHook");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validates_object_style_hook_entries() {
|
|
||||||
let source = r#"{"hooks":{"PreToolUse":["legacy",{"matcher":"Bash","hooks":[{"type":"command","command":"echo ok"}]}]}}"#;
|
|
||||||
let parsed = JsonValue::parse(source).expect("valid json");
|
|
||||||
let object = parsed.as_object().expect("object");
|
|
||||||
|
|
||||||
let result = validate_config_file(object, source, &test_path());
|
|
||||||
|
|
||||||
assert!(result.errors.is_empty(), "{:?}", result.errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn allows_wrong_hook_entry_types_for_partial_runtime_validation_441() {
|
|
||||||
let source = r#"{"hooks":{"PreToolUse":[42]}}"#;
|
|
||||||
let parsed = JsonValue::parse(source).expect("valid json");
|
|
||||||
let object = parsed.as_object().expect("object");
|
|
||||||
|
|
||||||
let result = validate_config_file(object, source, &test_path());
|
|
||||||
|
|
||||||
assert!(result.errors.is_empty(), "{:?}", result.errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validates_rules_import_string_and_array_forms() {
|
|
||||||
for source in [
|
|
||||||
r#"{"rulesImport":"auto"}"#,
|
|
||||||
r#"{"rulesImport":"none"}"#,
|
|
||||||
r#"{"rulesImport":["cursor","copilot"]}"#,
|
|
||||||
] {
|
|
||||||
let parsed = JsonValue::parse(source).expect("valid json");
|
|
||||||
let object = parsed.as_object().expect("object");
|
|
||||||
|
|
||||||
let result = validate_config_file(object, source, &test_path());
|
|
||||||
|
|
||||||
assert!(result.errors.is_empty(), "{source}: {:?}", result.errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_rules_import_wrong_type() {
|
|
||||||
let source = r#"{"rulesImport":42}"#;
|
|
||||||
let parsed = JsonValue::parse(source).expect("valid json");
|
|
||||||
let object = parsed.as_object().expect("object");
|
|
||||||
|
|
||||||
let result = validate_config_file(object, source, &test_path());
|
|
||||||
|
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.errors[0].field, "rulesImport");
|
assert_eq!(result.errors[0].field, "hooks.BadHook");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -839,9 +720,8 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(result.errors.is_empty());
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.warnings.len(), 1);
|
assert_eq!(result.errors[0].field, "permissions.denyAll");
|
||||||
assert_eq!(result.warnings[0].field, "permissions.denyAll");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -855,9 +735,8 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(result.errors.is_empty());
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.warnings.len(), 1);
|
assert_eq!(result.errors[0].field, "sandbox.containerMode");
|
||||||
assert_eq!(result.warnings[0].field, "sandbox.containerMode");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -871,9 +750,8 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(result.errors.is_empty());
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.warnings.len(), 1);
|
assert_eq!(result.errors[0].field, "plugins.autoUpdate");
|
||||||
assert_eq!(result.warnings[0].field, "plugins.autoUpdate");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -887,9 +765,8 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(result.errors.is_empty());
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.warnings.len(), 1);
|
assert_eq!(result.errors[0].field, "oauth.secret");
|
||||||
assert_eq!(result.warnings[0].field, "oauth.secret");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -897,7 +774,7 @@ mod tests {
|
|||||||
// given
|
// given
|
||||||
let source = r#"{
|
let source = r#"{
|
||||||
"model": "opus",
|
"model": "opus",
|
||||||
"hooks": {"PreToolUse": [{"hooks":[{"type":"command","command":"guard"}]}]},
|
"hooks": {"PreToolUse": ["guard"]},
|
||||||
"permissions": {"defaultMode": "plan", "allow": ["Read"]},
|
"permissions": {"defaultMode": "plan", "allow": ["Read"]},
|
||||||
"mcpServers": {},
|
"mcpServers": {},
|
||||||
"sandbox": {"enabled": false}
|
"sandbox": {"enabled": false}
|
||||||
@@ -924,9 +801,8 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(result.errors.is_empty());
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.warnings.len(), 1);
|
match &result.errors[0].kind {
|
||||||
match &result.warnings[0].kind {
|
|
||||||
DiagnosticKind::UnknownKey {
|
DiagnosticKind::UnknownKey {
|
||||||
suggestion: Some(s),
|
suggestion: Some(s),
|
||||||
} => assert_eq!(s, "model"),
|
} => assert_eq!(s, "model"),
|
||||||
@@ -937,7 +813,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_diagnostics_includes_all_entries() {
|
fn format_diagnostics_includes_all_entries() {
|
||||||
// given
|
// given
|
||||||
let source = r#"{"model": 42, "badKey": 1}"#;
|
let source = r#"{"permissionMode": "plan", "badKey": 1}"#;
|
||||||
let parsed = JsonValue::parse(source).expect("valid json");
|
let parsed = JsonValue::parse(source).expect("valid json");
|
||||||
let object = parsed.as_object().expect("object");
|
let object = parsed.as_object().expect("object");
|
||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
@@ -949,7 +825,7 @@ mod tests {
|
|||||||
assert!(output.contains("warning:"));
|
assert!(output.contains("warning:"));
|
||||||
assert!(output.contains("error:"));
|
assert!(output.contains("error:"));
|
||||||
assert!(output.contains("badKey"));
|
assert!(output.contains("badKey"));
|
||||||
assert!(output.contains("model"));
|
assert!(output.contains("permissionMode"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -65,14 +65,13 @@ pub use compact::{
|
|||||||
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
|
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
|
||||||
};
|
};
|
||||||
pub use config::{
|
pub use config::{
|
||||||
suppress_config_warnings_for_json_mode, ApiTimeoutConfig, ConfigEntry, ConfigError,
|
clear_user_provider_settings, default_config_home, save_user_provider_settings,
|
||||||
ConfigFileReport, ConfigFileStatus, ConfigInspection, ConfigLoader, ConfigSource,
|
suppress_config_warnings_for_json_mode, ConfigEntry, ConfigError, ConfigLoader, ConfigSource,
|
||||||
McpConfigCollection, McpInvalidServerConfig, McpManagedProxyServerConfig, McpOAuthConfig,
|
McpConfigCollection, McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig,
|
||||||
McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
|
McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
|
||||||
McpWebSocketServerConfig, OAuthConfig, ProviderFallbackConfig, ResolvedPermissionMode,
|
McpWebSocketServerConfig, OAuthConfig, ProviderFallbackConfig, ResolvedPermissionMode,
|
||||||
RulesImportConfig, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookCommand, RuntimeHookConfig,
|
RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig,
|
||||||
RuntimeInvalidHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig,
|
RuntimePluginConfig, RuntimeProviderConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||||
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
|
||||||
};
|
};
|
||||||
pub use config_validate::{
|
pub use config_validate::{
|
||||||
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,
|
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,
|
||||||
@@ -143,9 +142,8 @@ pub use policy_engine::{
|
|||||||
PolicyEvaluation, PolicyRule, ReconcileReason, ReviewStatus,
|
PolicyEvaluation, PolicyRule, ReconcileReason, ReviewStatus,
|
||||||
};
|
};
|
||||||
pub use prompt::{
|
pub use prompt::{
|
||||||
load_system_prompt, load_system_prompt_with_context, prepend_bullets, ContextFile,
|
load_system_prompt, prepend_bullets, ContextFile, ModelFamilyIdentity, ProjectContext,
|
||||||
ModelFamilyIdentity, ProjectContext, PromptBuildError, SystemPromptBuilder,
|
PromptBuildError, SystemPromptBuilder, FRONTIER_MODEL_NAME, SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
|
||||||
FRONTIER_MODEL_NAME, SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
|
|
||||||
};
|
};
|
||||||
pub use recovery_recipes::{
|
pub use recovery_recipes::{
|
||||||
attempt_recovery, recipe_for, EscalationPolicy, FailureScenario, RecoveryAttemptState,
|
attempt_recovery, recipe_for, EscalationPolicy, FailureScenario, RecoveryAttemptState,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
mod init;
|
mod init;
|
||||||
mod input;
|
mod input;
|
||||||
mod render;
|
mod render;
|
||||||
|
mod setup_wizard;
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::env;
|
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::SessionList { output_format } => run_session_list(output_format)?,
|
||||||
CliAction::State { output_format } => run_worker_state(output_format)?,
|
CliAction::State { output_format } => run_worker_state(output_format)?,
|
||||||
CliAction::Init { output_format } => run_init(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
|
// #146: dispatch pure-local introspection. Text mode uses existing
|
||||||
// render_config_report/render_diff_report; JSON mode uses the
|
// render_config_report/render_diff_report; JSON mode uses the
|
||||||
// corresponding _json helpers already exposed for resume sessions.
|
// corresponding _json helpers already exposed for resume sessions.
|
||||||
@@ -1238,6 +1240,9 @@ enum CliAction {
|
|||||||
Init {
|
Init {
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
},
|
},
|
||||||
|
Setup {
|
||||||
|
output_format: CliOutputFormat,
|
||||||
|
},
|
||||||
// #146: `claw config` and `claw diff` are pure-local read-only
|
// #146: `claw config` and `claw diff` are pure-local read-only
|
||||||
// introspection commands; wire them as standalone CLI subcommands.
|
// introspection commands; wire them as standalone CLI subcommands.
|
||||||
Config {
|
Config {
|
||||||
@@ -1301,6 +1306,7 @@ enum LocalHelpTopic {
|
|||||||
Model,
|
Model,
|
||||||
Settings,
|
Settings,
|
||||||
Diff,
|
Diff,
|
||||||
|
Setup,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -1765,6 +1771,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
"doctor" => Some(LocalHelpTopic::Doctor),
|
"doctor" => Some(LocalHelpTopic::Doctor),
|
||||||
"acp" => Some(LocalHelpTopic::Acp),
|
"acp" => Some(LocalHelpTopic::Acp),
|
||||||
"init" => Some(LocalHelpTopic::Init),
|
"init" => Some(LocalHelpTopic::Init),
|
||||||
|
"setup" => Some(LocalHelpTopic::Setup),
|
||||||
"state" => Some(LocalHelpTopic::State),
|
"state" => Some(LocalHelpTopic::State),
|
||||||
"resume" => Some(LocalHelpTopic::Resume),
|
"resume" => Some(LocalHelpTopic::Resume),
|
||||||
"session" => Some(LocalHelpTopic::Session),
|
"session" => Some(LocalHelpTopic::Session),
|
||||||
@@ -2144,6 +2151,15 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
}
|
}
|
||||||
Ok(CliAction::Init { output_format })
|
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),
|
"export" => parse_export_args(&rest[1..], output_format),
|
||||||
"prompt" => {
|
"prompt" => {
|
||||||
let mut read_stdin = false;
|
let mut read_stdin = false;
|
||||||
@@ -2271,6 +2287,7 @@ fn parse_local_help_action(
|
|||||||
"doctor" => LocalHelpTopic::Doctor,
|
"doctor" => LocalHelpTopic::Doctor,
|
||||||
"acp" => LocalHelpTopic::Acp,
|
"acp" => LocalHelpTopic::Acp,
|
||||||
"init" => LocalHelpTopic::Init,
|
"init" => LocalHelpTopic::Init,
|
||||||
|
"setup" => LocalHelpTopic::Setup,
|
||||||
"state" => LocalHelpTopic::State,
|
"state" => LocalHelpTopic::State,
|
||||||
"export" => LocalHelpTopic::Export,
|
"export" => LocalHelpTopic::Export,
|
||||||
"version" => LocalHelpTopic::Version,
|
"version" => LocalHelpTopic::Version,
|
||||||
@@ -2316,7 +2333,7 @@ fn parse_single_word_command_alias(
|
|||||||
let verb = &rest[0];
|
let verb = &rest[0];
|
||||||
let is_diagnostic = matches!(
|
let is_diagnostic = matches!(
|
||||||
verb.as_str(),
|
verb.as_str(),
|
||||||
"help" | "version" | "status" | "sandbox" | "doctor" | "state"
|
"help" | "version" | "status" | "sandbox" | "doctor" | "setup" | "state"
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_diagnostic && rest.len() > 1 {
|
if is_diagnostic && rest.len() > 1 {
|
||||||
@@ -2336,6 +2353,7 @@ fn parse_single_word_command_alias(
|
|||||||
"doctor" => Some(LocalHelpTopic::Doctor),
|
"doctor" => Some(LocalHelpTopic::Doctor),
|
||||||
"acp" => Some(LocalHelpTopic::Acp),
|
"acp" => Some(LocalHelpTopic::Acp),
|
||||||
"init" => Some(LocalHelpTopic::Init),
|
"init" => Some(LocalHelpTopic::Init),
|
||||||
|
"setup" => Some(LocalHelpTopic::Setup),
|
||||||
"state" => Some(LocalHelpTopic::State),
|
"state" => Some(LocalHelpTopic::State),
|
||||||
"export" => Some(LocalHelpTopic::Export),
|
"export" => Some(LocalHelpTopic::Export),
|
||||||
"version" => Some(LocalHelpTopic::Version),
|
"version" => Some(LocalHelpTopic::Version),
|
||||||
@@ -2390,6 +2408,7 @@ fn parse_single_word_command_alias(
|
|||||||
"doctor" => Some(LocalHelpTopic::Doctor),
|
"doctor" => Some(LocalHelpTopic::Doctor),
|
||||||
"acp" => Some(LocalHelpTopic::Acp),
|
"acp" => Some(LocalHelpTopic::Acp),
|
||||||
"init" => Some(LocalHelpTopic::Init),
|
"init" => Some(LocalHelpTopic::Init),
|
||||||
|
"setup" => Some(LocalHelpTopic::Setup),
|
||||||
"state" => Some(LocalHelpTopic::State),
|
"state" => Some(LocalHelpTopic::State),
|
||||||
"export" => Some(LocalHelpTopic::Export),
|
"export" => Some(LocalHelpTopic::Export),
|
||||||
"version" => Some(LocalHelpTopic::Version),
|
"version" => Some(LocalHelpTopic::Version),
|
||||||
@@ -2452,6 +2471,7 @@ fn parse_single_word_command_alias(
|
|||||||
.map(PermissionModeProvenance::from_flag)
|
.map(PermissionModeProvenance::from_flag)
|
||||||
.unwrap_or_else(permission_mode_provenance_for_current_dir),
|
.unwrap_or_else(permission_mode_provenance_for_current_dir),
|
||||||
})),
|
})),
|
||||||
|
"setup" => Some(Ok(CliAction::Setup { output_format })),
|
||||||
"state" => Some(Ok(CliAction::State { output_format })),
|
"state" => Some(Ok(CliAction::State { output_format })),
|
||||||
// #146: let `config` and `diff` fall through to parse_subcommand
|
// #146: let `config` and `diff` fall through to parse_subcommand
|
||||||
// where they are wired as pure-local introspection, instead of
|
// where they are wired as pure-local introspection, instead of
|
||||||
@@ -2744,6 +2764,7 @@ fn suggest_similar_subcommand(input: &str) -> Option<Vec<String>> {
|
|||||||
"status",
|
"status",
|
||||||
"sandbox",
|
"sandbox",
|
||||||
"doctor",
|
"doctor",
|
||||||
|
"setup",
|
||||||
"state",
|
"state",
|
||||||
"dump-manifests",
|
"dump-manifests",
|
||||||
"bootstrap-plan",
|
"bootstrap-plan",
|
||||||
@@ -3717,6 +3738,11 @@ fn run_doctor(
|
|||||||
Ok(())
|
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
|
/// Starts a minimal Model Context Protocol server that exposes claw's
|
||||||
/// built-in tools over stdio.
|
/// built-in tools over stdio.
|
||||||
///
|
///
|
||||||
@@ -6888,7 +6914,8 @@ fn run_resume_command(
|
|||||||
| SlashCommand::Tag { .. }
|
| SlashCommand::Tag { .. }
|
||||||
| SlashCommand::OutputStyle { .. }
|
| SlashCommand::OutputStyle { .. }
|
||||||
| SlashCommand::AddDir { .. }
|
| 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
|
false
|
||||||
}
|
}
|
||||||
|
SlashCommand::Setup => {
|
||||||
|
if let Err(e) = setup_wizard::run_setup_wizard() {
|
||||||
|
eprintln!("Setup wizard failed: {e}");
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
SlashCommand::History { count } => {
|
SlashCommand::History { count } => {
|
||||||
self.print_prompt_history(count.as_deref());
|
self.print_prompt_history(count.as_deref());
|
||||||
false
|
false
|
||||||
@@ -10218,6 +10251,13 @@ fn render_help_topic(topic: LocalHelpTopic) -> String {
|
|||||||
Formats text (default), json
|
Formats text (default), json
|
||||||
Related /diff · ROADMAP #148"
|
Related /diff · ROADMAP #148"
|
||||||
.to_string(),
|
.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::Model => "models",
|
||||||
LocalHelpTopic::Settings => "settings",
|
LocalHelpTopic::Settings => "settings",
|
||||||
LocalHelpTopic::Diff => "diff",
|
LocalHelpTopic::Diff => "diff",
|
||||||
|
LocalHelpTopic::Setup => "setup",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ const DEFAULT_BASE_URLS: &[(&str, &str)] = &[
|
|||||||
("anthropic", "https://api.anthropic.com"),
|
("anthropic", "https://api.anthropic.com"),
|
||||||
("xai", "https://api.x.ai/v1"),
|
("xai", "https://api.x.ai/v1"),
|
||||||
("openai", "https://api.openai.com/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)] = &[
|
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, ¤t)?;
|
let model = prompt_model(&kind, ¤t)?;
|
||||||
let fast_model = prompt_fast_model(¤t, model.as_deref())?;
|
let fast_model = prompt_fast_model(¤t, model.as_deref())?;
|
||||||
|
|
||||||
save_user_provider_settings(
|
save_user_provider_settings(&kind, &api_key, base_url.as_deref(), model.as_deref())?;
|
||||||
&kind,
|
|
||||||
&api_key,
|
|
||||||
base_url.as_deref(),
|
|
||||||
model.as_deref(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(fast) = &fast_model {
|
if let Some(fast) = &fast_model {
|
||||||
save_settings_field("subagentModel", fast)?;
|
save_settings_field("subagentModel", fast)?;
|
||||||
@@ -64,7 +62,10 @@ pub fn run_setup_wizard() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!(" \x1b[32mProvider saved to ~/.claw/settings.json\x1b[0m");
|
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!();
|
println!();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -82,7 +83,11 @@ fn prompt_provider(current: &RuntimeProviderConfig) -> Result<String, Box<dyn st
|
|||||||
let current_kind = current.kind().unwrap_or("anthropic");
|
let current_kind = current.kind().unwrap_or("anthropic");
|
||||||
println!(" \x1b[1mProvider\x1b[0m");
|
println!(" \x1b[1mProvider\x1b[0m");
|
||||||
for (num, label, kind) in PROVIDERS {
|
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}");
|
println!(" [{num}] {label}{marker}");
|
||||||
}
|
}
|
||||||
let default = PROVIDERS
|
let default = PROVIDERS
|
||||||
@@ -129,9 +134,7 @@ fn prompt_api_key(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if env var is already set
|
// Check if env var is already set
|
||||||
let env_set = std::env::var(env_var)
|
let env_set = std::env::var(env_var).ok().is_some_and(|v| !v.is_empty());
|
||||||
.ok()
|
|
||||||
.is_some_and(|v| !v.is_empty());
|
|
||||||
if env_set {
|
if env_set {
|
||||||
println!(" {env_var} is set in environment (will take priority over stored key)");
|
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 {
|
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)
|
Ok(key)
|
||||||
@@ -174,9 +179,7 @@ fn prompt_base_url(
|
|||||||
"dashscope" => "DASHSCOPE_BASE_URL",
|
"dashscope" => "DASHSCOPE_BASE_URL",
|
||||||
_ => "BASE_URL",
|
_ => "BASE_URL",
|
||||||
};
|
};
|
||||||
let env_set = std::env::var(env_var)
|
let env_set = std::env::var(env_var).ok().is_some_and(|v| !v.is_empty());
|
||||||
.ok()
|
|
||||||
.is_some_and(|v| !v.is_empty());
|
|
||||||
if env_set {
|
if env_set {
|
||||||
println!(" {env_var} is set in environment (will take priority over stored URL)");
|
println!(" {env_var} is set in environment (will take priority over stored URL)");
|
||||||
}
|
}
|
||||||
@@ -203,7 +206,9 @@ fn prompt_model(
|
|||||||
.find(|(k, _)| *k == kind)
|
.find(|(k, _)| *k == kind)
|
||||||
.map_or(empty, |(_, models)| *models);
|
.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");
|
println!(" \x1b[1mModel\x1b[0m");
|
||||||
if !aliases.is_empty() {
|
if !aliases.is_empty() {
|
||||||
@@ -235,12 +240,16 @@ fn prompt_fast_model(
|
|||||||
println!(" Press Enter to skip (agents will use your main model).");
|
println!(" Press Enter to skip (agents will use your main model).");
|
||||||
|
|
||||||
let current_fast = load_current_settings_field("subagentModel");
|
let current_fast = load_current_settings_field("subagentModel");
|
||||||
let default_hint = current_fast
|
let default_hint = current_fast.as_deref().or(main_model).unwrap_or("");
|
||||||
.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() {
|
if input.trim().is_empty() {
|
||||||
Ok(current_fast)
|
Ok(current_fast)
|
||||||
} else {
|
} 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() {
|
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)?;
|
std::fs::create_dir_all(&settings_dir)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user