Skip to main content

toolpath_claude/
io.rs

1use crate::error::Result;
2use crate::paths::PathResolver;
3use crate::reader::ConversationReader;
4use crate::types::{Conversation, ConversationMetadata, HistoryEntry};
5use std::path::PathBuf;
6
7#[derive(Debug, Clone)]
8pub struct ConvoIO {
9    resolver: PathResolver,
10}
11
12impl ConvoIO {
13    pub fn new() -> Self {
14        Self {
15            resolver: PathResolver::new(),
16        }
17    }
18
19    pub fn with_resolver(resolver: PathResolver) -> Self {
20        Self { resolver }
21    }
22
23    pub fn resolver(&self) -> &PathResolver {
24        &self.resolver
25    }
26
27    pub fn read_conversation(&self, project_path: &str, session_id: &str) -> Result<Conversation> {
28        let path = self.resolver.conversation_file(project_path, session_id)?;
29        ConversationReader::read_conversation(&path)
30    }
31
32    pub fn read_conversation_metadata(
33        &self,
34        project_path: &str,
35        session_id: &str,
36    ) -> Result<ConversationMetadata> {
37        let path = self.resolver.conversation_file(project_path, session_id)?;
38        ConversationReader::read_conversation_metadata(&path)
39    }
40
41    pub fn list_conversations(&self, project_path: &str) -> Result<Vec<String>> {
42        self.resolver.list_conversations(project_path)
43    }
44
45    pub fn list_conversation_metadata(
46        &self,
47        project_path: &str,
48    ) -> Result<Vec<ConversationMetadata>> {
49        let sessions = self.list_conversations(project_path)?;
50        let mut metadata = Vec::new();
51
52        for session_id in sessions {
53            match self.read_conversation_metadata(project_path, &session_id) {
54                Ok(meta) => metadata.push(meta),
55                Err(e) => {
56                    eprintln!("Warning: Failed to read metadata for {}: {}", session_id, e);
57                }
58            }
59        }
60
61        metadata.sort_by(|a, b| b.last_activity.cmp(&a.last_activity));
62        Ok(metadata)
63    }
64
65    pub fn list_projects(&self) -> Result<Vec<String>> {
66        self.resolver.list_project_dirs()
67    }
68
69    pub fn read_history(&self) -> Result<Vec<HistoryEntry>> {
70        let path = self.resolver.history_file()?;
71        ConversationReader::read_history(&path)
72    }
73
74    pub fn exists(&self) -> bool {
75        self.resolver.exists()
76    }
77
78    pub fn claude_dir_path(&self) -> Result<PathBuf> {
79        self.resolver.claude_dir()
80    }
81
82    pub fn conversation_exists(&self, project_path: &str, session_id: &str) -> Result<bool> {
83        let path = self.resolver.conversation_file(project_path, session_id)?;
84        Ok(path.exists())
85    }
86
87    pub fn project_exists(&self, project_path: &str) -> bool {
88        self.resolver
89            .project_dir(project_path)
90            .map(|p| p.exists())
91            .unwrap_or(false)
92    }
93}
94
95impl Default for ConvoIO {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::fs;
105    use tempfile::TempDir;
106
107    fn setup_io() -> (TempDir, ConvoIO) {
108        let temp = TempDir::new().unwrap();
109        let claude_dir = temp.path().join(".claude");
110        let project_dir = claude_dir.join("projects/-test-project");
111        fs::create_dir_all(&project_dir).unwrap();
112
113        let entry1 = r#"{"type":"user","uuid":"u1","timestamp":"2024-01-01T00:00:00Z","cwd":"/test/project","message":{"role":"user","content":"Hello"}}"#;
114        let entry2 = r#"{"type":"assistant","uuid":"u2","timestamp":"2024-01-01T00:01:00Z","message":{"role":"assistant","content":"Hi"}}"#;
115        fs::write(
116            project_dir.join("session-1.jsonl"),
117            format!("{}\n{}\n", entry1, entry2),
118        )
119        .unwrap();
120
121        let resolver = PathResolver::new().with_claude_dir(&claude_dir);
122        let io = ConvoIO::with_resolver(resolver);
123        (temp, io)
124    }
125
126    #[test]
127    fn test_default() {
128        let _io = ConvoIO::default();
129    }
130
131    #[test]
132    fn test_read_conversation() {
133        let (_temp, io) = setup_io();
134        let convo = io.read_conversation("/test/project", "session-1").unwrap();
135        assert_eq!(convo.entries.len(), 2);
136    }
137
138    #[test]
139    fn test_read_conversation_metadata() {
140        let (_temp, io) = setup_io();
141        let meta = io
142            .read_conversation_metadata("/test/project", "session-1")
143            .unwrap();
144        assert_eq!(meta.message_count, 2);
145    }
146
147    #[test]
148    fn test_list_conversations() {
149        let (_temp, io) = setup_io();
150        let sessions = io.list_conversations("/test/project").unwrap();
151        assert_eq!(sessions.len(), 1);
152    }
153
154    #[test]
155    fn test_list_conversation_metadata() {
156        let (_temp, io) = setup_io();
157        let meta = io.list_conversation_metadata("/test/project").unwrap();
158        assert_eq!(meta.len(), 1);
159        assert_eq!(meta[0].message_count, 2);
160    }
161
162    #[test]
163    fn test_list_projects() {
164        let (_temp, io) = setup_io();
165        let projects = io.list_projects().unwrap();
166        assert_eq!(projects.len(), 1);
167    }
168
169    #[test]
170    fn test_exists() {
171        let (_temp, io) = setup_io();
172        assert!(io.exists());
173    }
174
175    #[test]
176    fn test_claude_dir_path() {
177        let (_temp, io) = setup_io();
178        let path = io.claude_dir_path().unwrap();
179        assert!(path.exists());
180    }
181
182    #[test]
183    fn test_conversation_exists() {
184        let (_temp, io) = setup_io();
185        assert!(
186            io.conversation_exists("/test/project", "session-1")
187                .unwrap()
188        );
189        assert!(
190            !io.conversation_exists("/test/project", "nonexistent")
191                .unwrap()
192        );
193    }
194
195    #[test]
196    fn test_project_exists() {
197        let (_temp, io) = setup_io();
198        assert!(io.project_exists("/test/project"));
199        assert!(!io.project_exists("/nonexistent"));
200    }
201
202    #[test]
203    fn test_read_history_no_file() {
204        let (_temp, io) = setup_io();
205        let history = io.read_history().unwrap();
206        assert!(history.is_empty());
207    }
208
209    #[test]
210    fn test_resolver_accessor() {
211        let (_temp, io) = setup_io();
212        assert!(io.resolver().exists());
213    }
214}