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
This commit is contained in:
117
sunbeam/src/code/config.rs
Normal file
117
sunbeam/src/code/config.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user