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:
97
sunbeam/src/code/mod.rs
Normal file
97
sunbeam/src/code/mod.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod project;
|
||||
pub mod tools;
|
||||
|
||||
use clap::Subcommand;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum CodeCommand {
|
||||
/// Start a coding session (default — can omit subcommand)
|
||||
Start {
|
||||
/// Model override (e.g., devstral-2)
|
||||
#[arg(long)]
|
||||
model: Option<String>,
|
||||
/// Sol gRPC endpoint (default: from sunbeam config)
|
||||
#[arg(long)]
|
||||
endpoint: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn cmd_code(cmd: Option<CodeCommand>) -> sunbeam_sdk::error::Result<()> {
|
||||
cmd_code_inner(cmd).await.map_err(|e| sunbeam_sdk::error::SunbeamError::Other(e.to_string()))
|
||||
}
|
||||
|
||||
async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
let cmd = cmd.unwrap_or(CodeCommand::Start {
|
||||
model: None,
|
||||
endpoint: None,
|
||||
});
|
||||
|
||||
match cmd {
|
||||
CodeCommand::Start { model, endpoint } => {
|
||||
let endpoint = endpoint.unwrap_or_else(|| "http://127.0.0.1:50051".into());
|
||||
|
||||
// Discover project context
|
||||
let project = project::discover_project(".")?;
|
||||
info!(
|
||||
project = project.name.as_str(),
|
||||
path = project.path.as_str(),
|
||||
branch = project.git_branch.as_deref().unwrap_or("?"),
|
||||
"Discovered project"
|
||||
);
|
||||
|
||||
// Load project config
|
||||
let cfg = config::load_project_config(&project.path);
|
||||
|
||||
let model = model
|
||||
.or(cfg.model_name.clone())
|
||||
.unwrap_or_else(|| "devstral-small-2506".into());
|
||||
|
||||
// Connect to Sol
|
||||
let mut session = client::connect(&endpoint, &project, &cfg, &model).await?;
|
||||
|
||||
info!(
|
||||
session_id = session.session_id.as_str(),
|
||||
room_id = session.room_id.as_str(),
|
||||
model = session.model.as_str(),
|
||||
"Connected to Sol"
|
||||
);
|
||||
|
||||
// For now, simple stdin loop (ratatui TUI in Phase 4)
|
||||
println!("sunbeam code · {} · {}", project.name, model);
|
||||
println!("connected to Sol (session: {})", &session.session_id[..8]);
|
||||
println!("type a message, /quit to exit\n");
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let reader = tokio::io::BufReader::new(stdin);
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
let mut lines = reader.lines();
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let line = line.trim().to_string();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if line == "/quit" {
|
||||
session.end().await?;
|
||||
println!("session ended.");
|
||||
break;
|
||||
}
|
||||
|
||||
print!("> ");
|
||||
match session.chat(&line).await {
|
||||
Ok(response) => {
|
||||
println!("\n{}\n", response);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user