Skip to main content

smc_cli_cc/
config.rs

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            // Also check subagents directory
64            let subagents_dir = project_dir.join("subagents");
65            if subagents_dir.is_dir() {
66                // We skip subagent files from top-level discovery but could add them later
67            }
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    // Format: -Users-travis-GitHub-ProjectName or -Users-travis-GitHub-misc-subproject
82    let parts: Vec<&str> = dir_name.split('-').collect();
83
84    // Find "GitHub" and take everything after it
85    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        // Fallback: take last meaningful segment
94        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}