use std::process::Stdio;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct ClaudeCall<'a> {
pub system_prompt: &'a str,
pub user_prompt: &'a str,
pub json_schema: &'a str,
pub model: Option<&'a str>,
pub cli_command: &'a str,
pub max_budget_usd: f64,
}
impl<'a> ClaudeCall<'a> {
#[must_use]
pub fn new(system_prompt: &'a str, user_prompt: &'a str, json_schema: &'a str) -> Self {
Self {
system_prompt,
user_prompt,
json_schema,
model: None,
cli_command: "claude",
max_budget_usd: 0.20,
}
}
}
pub fn run_structured(call: ClaudeCall<'_>) -> Result<Value, String> {
let mut cmd = std::process::Command::new(call.cli_command);
cmd.arg("-p")
.arg(call.user_prompt)
.arg("--system-prompt")
.arg(call.system_prompt)
.arg("--output-format")
.arg("json")
.arg("--no-session-persistence")
.arg("--permission-mode")
.arg("dontAsk")
.arg("--allowedTools")
.arg("");
if !call.json_schema.is_empty() {
cmd.arg("--json-schema").arg(call.json_schema);
}
if let Some(m) = call.model {
cmd.arg("--model").arg(m);
}
if call.max_budget_usd > 0.0 {
cmd.arg("--max-budget-usd")
.arg(format!("{:.2}", call.max_budget_usd));
}
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd
.output()
.map_err(|e| format!("spawn {}: {e}", call.cli_command))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let truncated = if stderr.len() > 600 {
format!("{}…", &stderr[..600])
} else {
stderr.into_owned()
};
return Err(format!(
"{} -p exited with {}: {truncated}",
call.cli_command,
output.status.code().unwrap_or(-1)
));
}
let stdout = String::from_utf8(output.stdout)
.map_err(|e| format!("non-utf8 from {}: {e}", call.cli_command))?;
let envelope: Value = serde_json::from_str(stdout.trim()).map_err(|e| {
format!(
"parse {} json envelope: {e}\noutput: {stdout}",
call.cli_command
)
})?;
let structured = envelope
.get("structured_output")
.or_else(|| envelope.get("result"))
.cloned()
.ok_or_else(|| {
format!(
"{} response missing structured_output / result field: {envelope}",
call.cli_command
)
})?;
match structured {
Value::String(s) => serde_json::from_str(&s)
.map_err(|e| format!("parse structured_output string: {e}\nvalue: {s}")),
v => Ok(v),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_call_carries_safe_defaults() {
let call = ClaudeCall::new("sys", "user", r#"{"type":"object"}"#);
assert_eq!(call.cli_command, "claude");
assert!(call.model.is_none());
assert!((call.max_budget_usd - 0.20).abs() < f64::EPSILON);
}
}