1use anyhow::Result;
2use std::path::{Path, PathBuf};
3
4pub struct Config {
5 pub claude_dir: PathBuf,
6}
7
8impl Config {
9 pub fn new(path_override: Option<&str>) -> Result<Self> {
10 let claude_dir = if let Some(p) = path_override {
11 PathBuf::from(p)
12 } else {
13 dirs_fallback()
14 };
15
16 anyhow::ensure!(
17 claude_dir.exists(),
18 "Claude projects directory not found at {}",
19 claude_dir.display()
20 );
21
22 Ok(Config { claude_dir })
23 }
24
25 pub fn discover_jsonl_files(&self) -> Result<Vec<SessionFile>> {
26 let mut files = Vec::new();
27 let projects_dir = &self.claude_dir;
28
29 if !projects_dir.is_dir() {
30 return Ok(files);
31 }
32
33 for entry in std::fs::read_dir(projects_dir)? {
34 let entry = entry?;
35 let project_dir = entry.path();
36 if !project_dir.is_dir() {
37 continue;
38 }
39
40 let project_name = extract_project_name(entry.file_name().to_str().unwrap_or(""));
41
42 for file_entry in std::fs::read_dir(&project_dir)? {
43 let file_entry = file_entry?;
44 let path = file_entry.path();
45 if path.extension().map_or(false, |e| e == "jsonl") && path.is_file() {
46 let session_id = path
47 .file_stem()
48 .and_then(|s| s.to_str())
49 .unwrap_or("")
50 .to_string();
51
52 let metadata = std::fs::metadata(&path)?;
53
54 files.push(SessionFile {
55 path,
56 session_id,
57 project_name: project_name.clone(),
58 size_bytes: metadata.len(),
59 });
60 }
61 }
62
63 let subagents_dir = project_dir.join("subagents");
65 if subagents_dir.is_dir() {
66 }
68 }
69
70 files.sort_by(|a, b| b.size_bytes.cmp(&a.size_bytes));
71 Ok(files)
72 }
73}
74
75fn dirs_fallback() -> PathBuf {
76 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
77 Path::new(&home).join(".claude").join("projects")
78}
79
80pub fn extract_project_name(dir_name: &str) -> String {
81 let parts: Vec<&str> = dir_name.split('-').collect();
83
84 if let Some(pos) = parts.iter().position(|&p| p == "GitHub") {
86 let project_parts: Vec<&str> = parts[pos + 1..].iter().copied().collect();
87 if project_parts.is_empty() {
88 dir_name.to_string()
89 } else {
90 project_parts.join("/")
91 }
92 } else {
93 parts
95 .iter()
96 .filter(|p| !p.is_empty() && *p != &"Users")
97 .last()
98 .unwrap_or(&dir_name)
99 .to_string()
100 }
101}
102
103#[derive(Debug, Clone)]
104pub struct SessionFile {
105 pub path: PathBuf,
106 pub session_id: String,
107 pub project_name: String,
108 pub size_bytes: u64,
109}
110
111impl SessionFile {
112 pub fn size_human(&self) -> String {
113 let bytes = self.size_bytes;
114 if bytes < 1024 {
115 format!("{}B", bytes)
116 } else if bytes < 1024 * 1024 {
117 format!("{:.1}KB", bytes as f64 / 1024.0)
118 } else {
119 format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
120 }
121 }
122}