//! Resolves the on-disk path to a Claude Code session transcript JSONL file. use std::path::{Path, PathBuf}; /// Resolve the path to a Claude Code session transcript. /// /// Claude Code stores each session's transcript at /// `$HOME/.claude/projects//.jsonl`, where /// `` is the absolute working directory with every `/` and `.` /// replaced by `-` (e.g. `/workspace/.huskies/worktrees/1186` becomes /// `-workspace--huskies-worktrees-1186`). pub fn transcript_path(cwd: &Path, session_id: &str) -> PathBuf { let home = std::env::var("HOME").unwrap_or_else(|_| "/home/huskies".to_string()); transcript_path_under_home(Path::new(&home), cwd, session_id) } /// Same as [`transcript_path`] but takes an explicit `$HOME` directory, /// keeping the path-joining logic testable without mutating process env vars. fn transcript_path_under_home(home: &Path, cwd: &Path, session_id: &str) -> PathBuf { let mangled = mangle_cwd(&cwd.to_string_lossy()); home.join(".claude") .join("projects") .join(mangled) .join(format!("{session_id}.jsonl")) } /// Replace every `/` and `.` in an absolute path string with `-`, matching /// the directory-naming convention Claude Code uses under `~/.claude/projects/`. fn mangle_cwd(cwd: &str) -> String { cwd.chars() .map(|c| if c == '/' || c == '.' { '-' } else { c }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn mangles_worktree_path_matching_observed_convention() { // Verified against this very worktree's own transcript directory. assert_eq!( mangle_cwd("/workspace/.huskies/worktrees/1186"), "-workspace--huskies-worktrees-1186" ); } #[test] fn mangles_simple_home_path() { assert_eq!(mangle_cwd("/home/huskies"), "-home-huskies"); } #[test] fn transcript_path_joins_home_projects_dir_and_session_file() { let path = transcript_path_under_home( Path::new("/home/testuser"), Path::new("/workspace/proj"), "abc-123", ); assert_eq!( path, PathBuf::from("/home/testuser/.claude/projects/-workspace-proj/abc-123.jsonl") ); } }