use std::path::Path;
use anyhow::{Context, Result};
use claude_wrapper::{Claude, QueryCommand};
pub async fn generate(prompt: String, model: Option<&str>, call_name: &str) -> Result<String> {
run_generation(prompt, model, call_name, &[], None).await
}
pub async fn generate_inspecting(
prompt: String,
model: Option<&str>,
call_name: &str,
) -> Result<String> {
run_generation(
prompt,
model,
call_name,
&["Read", "Glob", "Grep"],
Some(20),
)
.await
}
async fn run_generation(
prompt: String,
model: Option<&str>,
call_name: &str,
allowed_tools: &[&str],
max_turns: Option<u32>,
) -> Result<String> {
let claude = Claude::builder().build()?;
let mut cmd = QueryCommand::new(prompt)
.name(call_name)
.prompt_via_stdin(true)
.no_session_persistence();
if !allowed_tools.is_empty() {
let tools: Vec<String> = allowed_tools.iter().map(|s| s.to_string()).collect();
cmd = cmd.allowed_tools(tools);
}
if let Some(n) = max_turns {
cmd = cmd.max_turns(n);
}
if let Some(model) = model {
cmd = cmd.model(model.to_string());
}
let result = cmd.execute_json(&claude).await?;
Ok(result.result)
}
pub fn strip_code_fences(s: &str) -> String {
let trimmed = s.trim();
if !trimmed.starts_with("```") {
return trimmed.to_string();
}
let mut lines: Vec<&str> = trimmed.lines().collect();
lines.remove(0); if lines
.last()
.is_some_and(|l| l.trim_start().starts_with("```"))
{
lines.pop(); }
lines.join("\n")
}
pub fn append_block(path: &Path, block: &str) -> Result<()> {
use std::io::Write;
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)
.with_context(|| format!("creating parent directory for {}", path.display()))?;
}
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.with_context(|| format!("opening {} for append", path.display()))?;
write!(file, "\n{block}").with_context(|| format!("writing to {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_code_fences_unwraps_toml_fence() {
let raw = "```toml\n[alias.x]\ndescription = \"hi\"\n```";
assert_eq!(strip_code_fences(raw), "[alias.x]\ndescription = \"hi\"");
}
#[test]
fn strip_code_fences_leaves_plain_input() {
let raw = "[alias.x]\ndescription = \"hi\"\n";
assert_eq!(strip_code_fences(raw), "[alias.x]\ndescription = \"hi\"");
}
#[test]
fn append_block_creates_and_appends() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nested").join("roba.toml");
append_block(&path, "[profile.x]\nreadonly = true\n").unwrap();
let text = std::fs::read_to_string(&path).unwrap();
assert_eq!(text, "\n[profile.x]\nreadonly = true\n");
append_block(&path, "[profile.y]\nwritable = true\n").unwrap();
let text = std::fs::read_to_string(&path).unwrap();
assert!(
text.contains("[profile.x]") && text.contains("[profile.y]"),
"{text}"
);
}
}