use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use crate::parser::{parse_jsonl_file, ToolCall};
use super::mapping::{canonical, ToolNameMapping};
use super::traits::{Agent, ExecutionConfig, RawExecutionResult};
pub struct ClaudeAdapter {
mapping: ToolNameMapping,
}
impl ClaudeAdapter {
pub fn new() -> Self {
let mut mapping = ToolNameMapping::new();
mapping
.add("Read", canonical::READ)
.add("Write", canonical::WRITE)
.add("Edit", canonical::EDIT)
.add("Bash", canonical::BASH)
.add("Grep", canonical::GREP)
.add("Glob", canonical::GLOB)
.add("LS", canonical::LIST_DIRECTORY)
.add("AskUserQuestion", canonical::ASK_USER)
.add("Task", canonical::TASK)
.add("WebFetch", canonical::WEB_FETCH)
.add("WebSearch", canonical::WEB_SEARCH)
.add("NotebookEdit", canonical::NOTEBOOK_EDIT)
.add("TodoWrite", canonical::TODO_WRITE)
.add("KillShell", canonical::KILL_SHELL)
.add("TaskOutput", canonical::TASK_OUTPUT)
.add("Skill", canonical::SKILL);
Self { mapping }
}
}
impl Default for ClaudeAdapter {
fn default() -> Self {
Self::new()
}
}
impl Agent for ClaudeAdapter {
fn name(&self) -> &'static str {
"claude"
}
fn execute(&self, prompt: &str, config: &ExecutionConfig) -> Result<RawExecutionResult> {
let claude_dir = get_claude_projects_dir()?;
let project_dir = get_project_dir_for_workdir(&claude_dir, &config.working_dir)?;
let existing_sessions = list_session_files(&project_dir)?;
let mut cmd = Command::new("claude");
cmd.arg("--print").arg(prompt).stdin(Stdio::null());
if let Some(dir) = &config.working_dir {
cmd.current_dir(dir);
}
for arg in &config.extra_args {
cmd.arg(arg);
}
let output = cmd.output().context("Failed to execute claude command")?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stdout = if stdout.is_empty() { None } else { Some(stdout) };
let session_log_path = find_new_session(&project_dir, &existing_sessions)?;
Ok(RawExecutionResult {
session_log_path: Some(session_log_path),
stdout,
})
}
fn parse_session(&self, result: &RawExecutionResult) -> Result<Vec<ToolCall>> {
let path = result
.session_log_path
.as_ref()
.context("Claude requires session log path")?;
parse_jsonl_file(path)
}
fn tool_mapping(&self) -> &ToolNameMapping {
&self.mapping
}
fn is_available(&self) -> bool {
Command::new("claude")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
}
fn get_claude_projects_dir() -> Result<PathBuf> {
let home = dirs::home_dir().context("Could not find home directory")?;
let claude_dir = home.join(".claude").join("projects");
if !claude_dir.exists() {
anyhow::bail!(
"Claude projects directory not found at {:?}. Is Claude Code installed?",
claude_dir
);
}
Ok(claude_dir)
}
fn get_project_dir_for_workdir(
claude_dir: &PathBuf,
working_dir: &Option<PathBuf>,
) -> Result<PathBuf> {
let workdir = match working_dir {
Some(dir) => dir
.canonicalize()
.context("Failed to canonicalize working directory")?,
None => std::env::current_dir().context("Failed to get current directory")?,
};
let path_str = workdir.to_string_lossy();
let project_name = path_str.replace('/', "-");
let project_dir = claude_dir.join(&project_name);
if !project_dir.exists() {
return Ok(claude_dir.clone());
}
Ok(project_dir)
}
fn list_session_files(claude_dir: &PathBuf) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
if claude_dir.exists() {
for entry in walkdir::WalkDir::new(claude_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.to_string_lossy().contains("/subagents/") {
continue;
}
if path.extension().map_or(false, |ext| ext == "jsonl") {
files.push(path.to_path_buf());
}
}
}
Ok(files)
}
fn find_new_session(claude_dir: &PathBuf, existing: &[PathBuf]) -> Result<PathBuf> {
let current = list_session_files(claude_dir)?;
for path in ¤t {
if !existing.contains(path) {
return Ok(path.clone());
}
}
let mut newest: Option<(PathBuf, std::time::SystemTime)> = None;
for path in current {
if let Ok(metadata) = path.metadata() {
if let Ok(modified) = metadata.modified() {
match &newest {
None => newest = Some((path, modified)),
Some((_, newest_time)) if modified > *newest_time => {
newest = Some((path, modified));
}
_ => {}
}
}
}
}
newest
.map(|(path, _)| path)
.context("Could not find session log file")
}