#![allow(dead_code)]
use anyhow::{anyhow, Context, Result};
use serde_json::Value;
use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Duration;
pub struct ClaudeCli {
bin: String,
}
impl ClaudeCli {
pub fn new(bin: impl Into<String>) -> Self {
Self { bin: bin.into() }
}
pub fn ask(&self, prompt: &str, timeout: Duration) -> Result<String> {
let mut child = Command::new(&self.bin)
.args(["-p", "--output-format", "text"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("spawn `{}` (is claude CLI installed?)", self.bin))?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(prompt.as_bytes())
.context("write prompt to claude stdin")?;
}
let start = std::time::Instant::now();
let exit = loop {
match child.try_wait()? {
Some(s) => break s,
None => {
if start.elapsed() > timeout {
let _ = child.kill();
return Err(anyhow!("claude CLI timed out after {:?}", timeout));
}
std::thread::sleep(Duration::from_millis(100));
}
}
};
let mut stdout = String::new();
if let Some(mut s) = child.stdout.take() {
use std::io::Read;
let _ = s.read_to_string(&mut stdout);
}
let mut stderr = String::new();
if let Some(mut s) = child.stderr.take() {
use std::io::Read;
let _ = s.read_to_string(&mut stderr);
}
if !exit.success() {
return Err(anyhow!(
"claude CLI exited {} — stderr: {}",
exit.code().unwrap_or(-1),
stderr.trim()
));
}
Ok(stdout)
}
pub fn ask_json(&self, prompt: &str, timeout: Duration) -> Result<Value> {
let raw = self.ask(prompt, timeout)?;
let cleaned = strip_fences(&raw);
serde_json::from_str(&cleaned).with_context(|| format!("parse JSON from: {}", cleaned))
}
pub fn ask_json_in_dir(
&self,
prompt: &str,
workdir: &std::path::Path,
timeout: Duration,
) -> Result<Value> {
let mut child = Command::new(&self.bin)
.args([
"-p",
"--output-format",
"text",
"--permission-mode",
"bypassPermissions",
])
.current_dir(workdir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| {
format!(
"spawn `{}` in {} (claude CLI installed?)",
self.bin,
workdir.display()
)
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(prompt.as_bytes())?;
}
let start = std::time::Instant::now();
let exit = loop {
match child.try_wait()? {
Some(s) => break s,
None => {
if start.elapsed() > timeout {
let _ = child.kill();
return Err(anyhow!("claude CLI timed out after {:?}", timeout));
}
std::thread::sleep(Duration::from_millis(200));
}
}
};
let mut stdout = String::new();
if let Some(mut s) = child.stdout.take() {
use std::io::Read;
let _ = s.read_to_string(&mut stdout);
}
let mut stderr = String::new();
if let Some(mut s) = child.stderr.take() {
use std::io::Read;
let _ = s.read_to_string(&mut stderr);
}
if !exit.success() {
return Err(anyhow!(
"claude CLI exited {} in {} — stderr: {}",
exit.code().unwrap_or(-1),
workdir.display(),
stderr.trim()
));
}
let cleaned = strip_fences(&stdout);
serde_json::from_str(&cleaned).with_context(|| format!("parse JSON from: {}", cleaned))
}
}
fn strip_fences(s: &str) -> String {
let trimmed = s.trim();
if let Some(rest) = trimmed.strip_prefix("```json") {
if let Some(end) = rest.rfind("```") {
return rest[..end].trim().to_string();
}
}
if let Some(rest) = trimmed.strip_prefix("```") {
if let Some(end) = rest.rfind("```") {
return rest[..end].trim().to_string();
}
}
trimmed.to_string()
}
pub fn is_available(bin: &str) -> bool {
Command::new(bin)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}