use std::path::{Path, PathBuf};
use std::time::SystemTime;
use walkdir::WalkDir;
pub async fn detect_json_log_location(
project_path: &Path,
cli_output: &str,
execution_start: SystemTime,
) -> Option<PathBuf> {
if let Some(path) = parse_log_location_from_output(cli_output) {
if path.exists() {
tracing::debug!("Found Claude JSON log via CLI output: {}", path.display());
return Some(path);
}
}
if let Some(path) = find_recent_log(execution_start).await {
tracing::debug!("Found Claude JSON log via file search: {}", path.display());
return Some(path);
}
if let Some(path) = infer_log_location(project_path) {
if path.exists() {
tracing::debug!(
"Found Claude JSON log via path inference: {}",
path.display()
);
return Some(path);
}
}
tracing::debug!("Could not detect Claude JSON log location");
None
}
fn parse_log_location_from_output(output: &str) -> Option<PathBuf> {
let patterns = [
"Session log: ",
"Log saved to: ",
"JSON log: ",
"Saving to: ",
];
for pattern in &patterns {
if let Some(start) = output.find(pattern) {
let path_start = start + pattern.len();
if let Some(end) = output[path_start..].find('\n') {
let path_str = output[path_start..path_start + end].trim();
return Some(PathBuf::from(path_str));
} else {
let path_str = output[path_start..].trim();
return Some(PathBuf::from(path_str));
}
}
}
None
}
fn infer_log_location(project_path: &Path) -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
let sanitized = sanitize_project_path(project_path);
let projects_dir = PathBuf::from(home).join(".claude/projects").join(sanitized);
if projects_dir.exists() {
if let Ok(entries) = std::fs::read_dir(&projects_dir) {
let mut files: Vec<_> = entries
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "jsonl"))
.collect();
files.sort_by_key(|e| {
e.metadata()
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
});
if let Some(most_recent) = files.last() {
return Some(most_recent.path());
}
}
}
None
}
pub fn sanitize_project_path(path: &Path) -> String {
path.to_string_lossy()
.replace('/', "-")
.trim_start_matches('-')
.to_string()
}
async fn find_recent_log(since: SystemTime) -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
let projects_dir = PathBuf::from(home).join(".claude/projects");
if !projects_dir.exists() {
return None;
}
let search_start = since.checked_sub(std::time::Duration::from_secs(1))?;
let mut candidates: Vec<_> = WalkDir::new(&projects_dir)
.max_depth(2) .into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "jsonl"))
.filter_map(|e| {
let modified = e.metadata().ok()?.modified().ok()?;
if modified >= search_start {
Some((e.path().to_path_buf(), modified))
} else {
None
}
})
.collect();
candidates.sort_by_key(|(_, modified)| *modified);
candidates.last().map(|(path, _)| path.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_log_location_from_output() {
let output = "Session log: /Users/test/.claude/projects/test/abc.jsonl\n";
let result = parse_log_location_from_output(output);
assert_eq!(
result,
Some(PathBuf::from("/Users/test/.claude/projects/test/abc.jsonl"))
);
}
#[test]
fn test_parse_log_location_no_newline() {
let output = "Log saved to: /Users/test/log.jsonl";
let result = parse_log_location_from_output(output);
assert_eq!(result, Some(PathBuf::from("/Users/test/log.jsonl")));
}
#[test]
fn test_parse_log_location_not_found() {
let output = "Some other output without log location";
let result = parse_log_location_from_output(output);
assert_eq!(result, None);
}
#[test]
fn test_sanitize_project_path() {
assert_eq!(
sanitize_project_path(&PathBuf::from("/Users/glen/prodigy")),
"Users-glen-prodigy"
);
}
#[test]
fn test_sanitize_project_path_no_leading_slash() {
assert_eq!(
sanitize_project_path(&PathBuf::from("Users/glen/prodigy")),
"Users-glen-prodigy"
);
}
#[test]
fn test_execution_result_with_json_log() {
use crate::cook::execution::ExecutionResult;
use std::collections::HashMap;
let result = ExecutionResult {
success: true,
stdout: String::new(),
stderr: String::new(),
exit_code: Some(0),
metadata: HashMap::new(),
}
.with_json_log_location(PathBuf::from("/test/log.jsonl"));
assert_eq!(result.json_log_location(), Some("/test/log.jsonl"));
}
}