Files
cli/sunbeam/src/code/config.rs
Sienna Meridian Satterwhite 02e4d7fb37 feat(code): CLI client with gRPC connection + local tools
phase 3 client core:
- sunbeam code subcommand with project discovery, config loading
- gRPC client connects to Sol, starts bidirectional session
- 7 client-side tool executors: file_read, file_write, search_replace,
  grep, bash, list_directory
- project context: .sunbeam/prompt.md, .sunbeam/config.toml, git info
- tool permission config (always/ask/never per tool)
- simple stdin loop (ratatui TUI in phase 4)
- aligned sunbeam-proto to tonic 0.14
2026-03-23 11:57:24 +00:00

118 lines
3.3 KiB
Rust

use serde::Deserialize;
/// Project-level configuration from .sunbeam/config.toml.
#[derive(Debug, Default, Deserialize)]
pub struct ProjectConfig {
#[serde(default)]
pub model: Option<ModelConfig>,
#[serde(default)]
pub tools: Option<ToolPermissions>,
}
#[derive(Debug, Deserialize)]
pub struct ModelConfig {
pub name: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
pub struct ToolPermissions {
#[serde(default)]
pub file_read: Option<String>,
#[serde(default)]
pub file_write: Option<String>,
#[serde(default)]
pub search_replace: Option<String>,
#[serde(default)]
pub grep: Option<String>,
#[serde(default)]
pub bash: Option<String>,
#[serde(default)]
pub list_directory: Option<String>,
}
/// Convenience wrapper with flattened fields.
pub struct LoadedConfig {
pub model_name: Option<String>,
pub file_read_perm: String,
pub file_write_perm: String,
pub search_replace_perm: String,
pub grep_perm: String,
pub bash_perm: String,
pub list_directory_perm: String,
}
impl Default for LoadedConfig {
fn default() -> Self {
Self {
model_name: None,
file_read_perm: "always".into(),
file_write_perm: "ask".into(),
search_replace_perm: "ask".into(),
grep_perm: "always".into(),
bash_perm: "ask".into(),
list_directory_perm: "always".into(),
}
}
}
/// Load project config from .sunbeam/config.toml.
pub fn load_project_config(project_path: &str) -> LoadedConfig {
let config_path = std::path::Path::new(project_path)
.join(".sunbeam")
.join("config.toml");
let raw = match std::fs::read_to_string(&config_path) {
Ok(s) => s,
Err(_) => return LoadedConfig::default(),
};
let parsed: ProjectConfig = match toml::from_str(&raw) {
Ok(c) => c,
Err(e) => {
eprintln!("warning: failed to parse .sunbeam/config.toml: {e}");
return LoadedConfig::default();
}
};
let tools = parsed.tools.unwrap_or_default();
LoadedConfig {
model_name: parsed.model.and_then(|m| m.name),
file_read_perm: tools.file_read.unwrap_or_else(|| "always".into()),
file_write_perm: tools.file_write.unwrap_or_else(|| "ask".into()),
search_replace_perm: tools.search_replace.unwrap_or_else(|| "ask".into()),
grep_perm: tools.grep.unwrap_or_else(|| "always".into()),
bash_perm: tools.bash.unwrap_or_else(|| "ask".into()),
list_directory_perm: tools.list_directory.unwrap_or_else(|| "always".into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = LoadedConfig::default();
assert_eq!(cfg.file_read_perm, "always");
assert_eq!(cfg.file_write_perm, "ask");
assert_eq!(cfg.bash_perm, "ask");
assert!(cfg.model_name.is_none());
}
#[test]
fn test_parse_config() {
let toml = r#"
[model]
name = "devstral-2"
[tools]
file_read = "always"
bash = "never"
"#;
let parsed: ProjectConfig = toml::from_str(toml).unwrap();
assert_eq!(parsed.model.unwrap().name.unwrap(), "devstral-2");
assert_eq!(parsed.tools.unwrap().bash.unwrap(), "never");
}
}