feat: interactive provider wizard with fast model selection

Adds interactive provider setup wizard.
This commit is contained in:
TheArchitectit
2026-05-24 21:24:20 -05:00
committed by GitHub
parent bc1b3c837a
commit cef45efc16
2 changed files with 373 additions and 0 deletions

View File

@@ -592,6 +592,92 @@ pub fn default_config_home() -> PathBuf {
.unwrap_or_else(|| PathBuf::from(".claw"))
}
/// Save provider settings to the user-level `~/.claw/settings.json`.
/// Creates the file and directory if they don't exist. Sets file permissions
/// to `0o600` (owner read/write only) to protect stored API keys.
pub fn save_user_provider_settings(
kind: &str,
api_key: &str,
base_url: Option<&str>,
model: Option<&str>,
) -> Result<(), ConfigError> {
let config_home = default_config_home();
fs::create_dir_all(&config_home).map_err(ConfigError::Io)?;
let settings_path = config_home.join("settings.json");
let mut root = read_settings_root(&settings_path);
let mut provider = serde_json::Map::new();
provider.insert("kind".to_string(), serde_json::Value::String(kind.to_string()));
provider.insert("apiKey".to_string(), serde_json::Value::String(api_key.to_string()));
if let Some(base_url) = base_url {
provider.insert("baseUrl".to_string(), serde_json::Value::String(base_url.to_string()));
} else {
provider.remove("baseUrl");
}
root.insert("provider".to_string(), serde_json::Value::Object(provider));
if let Some(model) = model {
root.insert("model".to_string(), serde_json::Value::String(model.to_string()));
} else {
root.remove("model");
}
write_settings_root(&settings_path, &root)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
fs::set_permissions(&settings_path, perms).map_err(ConfigError::Io)?;
}
Ok(())
}
/// Remove the `provider` section from the user-level `~/.claw/settings.json`.
pub fn clear_user_provider_settings() -> Result<(), ConfigError> {
let config_home = default_config_home();
let settings_path = config_home.join("settings.json");
if !settings_path.exists() {
return Ok(());
}
let mut root = read_settings_root(&settings_path);
if root.remove("provider").is_none() {
return Ok(());
}
root.remove("model");
write_settings_root(&settings_path, &root)?;
Ok(())
}
fn read_settings_root(path: &Path) -> serde_json::Map<String, serde_json::Value> {
match fs::read_to_string(path) {
Ok(contents) if !contents.trim().is_empty() => {
serde_json::from_str::<serde_json::Value>(&contents)
.ok()
.and_then(|v| v.as_object().cloned())
.unwrap_or_default()
}
_ => serde_json::Map::new(),
}
}
fn write_settings_root(
path: &Path,
root: &serde_json::Map<String, serde_json::Value>,
) -> Result<(), ConfigError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(ConfigError::Io)?;
}
let rendered = serde_json::to_string_pretty(&serde_json::Value::Object(root.clone()))
.map_err(|e| ConfigError::Parse(e.to_string()))?;
fs::write(path, format!("{rendered}\n")).map_err(ConfigError::Io)
}
impl RuntimeHookConfig {
#[must_use]
pub fn new(