lore_cli/capture/watchers/
mod.rs

1//! Watchers for different AI coding tools.
2//!
3//! Each watcher module provides functions to discover and parse session
4//! files from a specific AI coding tool. Watchers convert tool-specific
5//! formats into Lore's internal session and message models.
6//!
7//! The [`Watcher`] trait defines the common interface for all tool watchers.
8//! Use the [`WatcherRegistry`] to manage multiple watchers and query their
9//! availability.
10
11use anyhow::Result;
12use std::path::{Path, PathBuf};
13
14use crate::storage::models::{Message, Session};
15
16/// Aider session parser for markdown chat history files.
17pub mod aider;
18
19/// Amp CLI session parser for JSON files.
20pub mod amp;
21
22/// Claude Code session parser for JSONL files.
23pub mod claude_code;
24
25/// Cline (Claude Dev) session parser for VS Code extension storage.
26pub mod cline;
27
28/// Codex CLI session parser for JSONL files.
29pub mod codex;
30
31/// Continue.dev session parser for JSON session files.
32pub mod continue_dev;
33
34/// Gemini CLI session parser for JSON files.
35pub mod gemini;
36
37/// Kilo Code session parser for VS Code extension storage.
38pub mod kilo_code;
39
40/// OpenCode CLI session parser for multi-file JSON storage.
41pub mod opencode;
42
43/// Roo Code session parser for VS Code extension storage.
44pub mod roo_code;
45
46/// Information about a tool that can be watched for sessions.
47///
48/// Contains metadata about the watcher including its name, description,
49/// and default file system paths to search for sessions.
50#[derive(Debug, Clone)]
51pub struct WatcherInfo {
52    /// Short identifier for the watcher (e.g., "claude-code", "cursor").
53    pub name: &'static str,
54
55    /// Human-readable description of what this watcher handles.
56    #[allow(dead_code)]
57    pub description: &'static str,
58
59    /// Default file system paths where this tool stores sessions.
60    #[allow(dead_code)]
61    pub default_paths: Vec<PathBuf>,
62}
63
64/// A watcher for AI tool sessions.
65///
66/// Implementations of this trait can discover and parse session files from
67/// a specific AI coding tool. The trait is object-safe to allow storing
68/// multiple watcher implementations in a registry.
69///
70/// # Example
71///
72/// ```no_run
73/// use lore_cli::capture::watchers::default_registry;
74///
75/// let registry = default_registry();
76/// for watcher in registry.available_watchers() {
77///     println!("{}: {}", watcher.info().name, watcher.info().description);
78/// }
79/// ```
80pub trait Watcher: Send + Sync {
81    /// Returns information about this watcher.
82    fn info(&self) -> WatcherInfo;
83
84    /// Checks if this watcher is available.
85    ///
86    /// A watcher is available if the tool it watches is installed and its
87    /// session storage location exists on this system.
88    fn is_available(&self) -> bool;
89
90    /// Finds all session sources (files or directories) to import.
91    ///
92    /// Returns paths to individual session files or databases that can be
93    /// passed to [`parse_source`](Self::parse_source).
94    fn find_sources(&self) -> Result<Vec<PathBuf>>;
95
96    /// Parses a session source and returns sessions with their messages.
97    ///
98    /// Each session is returned with its associated messages as a tuple.
99    /// A single source file may contain multiple sessions.
100    fn parse_source(&self, path: &Path) -> Result<Vec<(Session, Vec<Message>)>>;
101
102    /// Returns paths to watch for changes.
103    ///
104    /// Used by the daemon file watcher to monitor for new or modified sessions.
105    fn watch_paths(&self) -> Vec<PathBuf>;
106}
107
108/// Registry of available session watchers.
109///
110/// The registry maintains a collection of watcher implementations and
111/// provides methods to query their availability and retrieve watchers by name.
112pub struct WatcherRegistry {
113    watchers: Vec<Box<dyn Watcher>>,
114}
115
116impl Default for WatcherRegistry {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl WatcherRegistry {
123    /// Creates an empty watcher registry.
124    pub fn new() -> Self {
125        Self {
126            watchers: Vec::new(),
127        }
128    }
129
130    /// Registers a new watcher with the registry.
131    pub fn register(&mut self, watcher: Box<dyn Watcher>) {
132        self.watchers.push(watcher);
133    }
134
135    /// Returns all registered watchers.
136    pub fn all_watchers(&self) -> Vec<&dyn Watcher> {
137        self.watchers.iter().map(|w| w.as_ref()).collect()
138    }
139
140    /// Returns only watchers that are currently available.
141    ///
142    /// A watcher is available if the tool it watches is installed
143    /// and configured on this system.
144    pub fn available_watchers(&self) -> Vec<&dyn Watcher> {
145        self.watchers
146            .iter()
147            .filter(|w| w.is_available())
148            .map(|w| w.as_ref())
149            .collect()
150    }
151
152    /// Returns watchers that are both available and enabled in config.
153    ///
154    /// Only watchers whose names appear in the `enabled_watchers` list
155    /// and are also available on the system are returned.
156    ///
157    /// This method is intended for use by the import command and daemon
158    /// to filter which watchers actively scan for sessions.
159    pub fn enabled_watchers(&self, enabled_watchers: &[String]) -> Vec<&dyn Watcher> {
160        self.watchers
161            .iter()
162            .filter(|w| {
163                w.is_available() && enabled_watchers.iter().any(|name| name == w.info().name)
164            })
165            .map(|w| w.as_ref())
166            .collect()
167    }
168
169    /// Retrieves a watcher by its name.
170    ///
171    /// Returns `None` if no watcher with the given name is registered.
172    #[allow(dead_code)]
173    pub fn get_watcher(&self, name: &str) -> Option<&dyn Watcher> {
174        self.watchers
175            .iter()
176            .find(|w| w.info().name == name)
177            .map(|w| w.as_ref())
178    }
179
180    /// Returns all paths that should be watched for changes.
181    ///
182    /// Collects watch paths from all available watchers into a single list.
183    pub fn all_watch_paths(&self) -> Vec<PathBuf> {
184        self.available_watchers()
185            .iter()
186            .flat_map(|w| w.watch_paths())
187            .collect()
188    }
189}
190
191/// Creates the default registry with all built-in watchers.
192///
193/// This includes watchers for:
194/// - Aider (markdown files in project directories)
195/// - Amp CLI (JSON files in ~/.local/share/amp/threads/)
196/// - Claude Code (JSONL files in ~/.claude/projects/)
197/// - Cline (JSON files in VS Code extension storage)
198/// - Codex CLI (JSONL files in ~/.codex/sessions/)
199/// - Continue.dev (JSON files in ~/.continue/sessions/)
200/// - Gemini CLI (JSON files in ~/.gemini/tmp/)
201/// - Kilo Code (JSON files in VS Code extension storage)
202/// - OpenCode CLI (JSON files in ~/.local/share/opencode/storage/)
203/// - Roo Code (JSON files in VS Code extension storage)
204pub fn default_registry() -> WatcherRegistry {
205    let mut registry = WatcherRegistry::new();
206    registry.register(Box::new(aider::AiderWatcher));
207    registry.register(Box::new(amp::AmpWatcher));
208    registry.register(Box::new(claude_code::ClaudeCodeWatcher));
209    registry.register(Box::new(cline::ClineWatcher));
210    registry.register(Box::new(codex::CodexWatcher));
211    registry.register(Box::new(continue_dev::ContinueDevWatcher));
212    registry.register(Box::new(gemini::GeminiWatcher));
213    registry.register(Box::new(kilo_code::KiloCodeWatcher));
214    registry.register(Box::new(opencode::OpenCodeWatcher));
215    registry.register(Box::new(roo_code::RooCodeWatcher));
216    registry
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    /// A test watcher implementation for unit testing the registry.
224    struct TestWatcher {
225        name: &'static str,
226        available: bool,
227    }
228
229    impl Watcher for TestWatcher {
230        fn info(&self) -> WatcherInfo {
231            WatcherInfo {
232                name: self.name,
233                description: "Test watcher",
234                default_paths: vec![PathBuf::from("/test")],
235            }
236        }
237
238        fn is_available(&self) -> bool {
239            self.available
240        }
241
242        fn find_sources(&self) -> Result<Vec<PathBuf>> {
243            Ok(vec![])
244        }
245
246        fn parse_source(&self, _path: &Path) -> Result<Vec<(Session, Vec<Message>)>> {
247            Ok(vec![])
248        }
249
250        fn watch_paths(&self) -> Vec<PathBuf> {
251            vec![PathBuf::from("/test")]
252        }
253    }
254
255    #[test]
256    fn test_registry_new_is_empty() {
257        let registry = WatcherRegistry::new();
258        assert!(registry.all_watchers().is_empty());
259    }
260
261    #[test]
262    fn test_registry_register_and_retrieve() {
263        let mut registry = WatcherRegistry::new();
264        registry.register(Box::new(TestWatcher {
265            name: "test-watcher",
266            available: true,
267        }));
268
269        assert_eq!(registry.all_watchers().len(), 1);
270        assert!(registry.get_watcher("test-watcher").is_some());
271        assert!(registry.get_watcher("nonexistent").is_none());
272    }
273
274    #[test]
275    fn test_registry_available_watchers_filters() {
276        let mut registry = WatcherRegistry::new();
277        registry.register(Box::new(TestWatcher {
278            name: "available",
279            available: true,
280        }));
281        registry.register(Box::new(TestWatcher {
282            name: "unavailable",
283            available: false,
284        }));
285
286        assert_eq!(registry.all_watchers().len(), 2);
287        assert_eq!(registry.available_watchers().len(), 1);
288        assert_eq!(registry.available_watchers()[0].info().name, "available");
289    }
290
291    #[test]
292    fn test_registry_all_watch_paths() {
293        let mut registry = WatcherRegistry::new();
294        registry.register(Box::new(TestWatcher {
295            name: "watcher1",
296            available: true,
297        }));
298        registry.register(Box::new(TestWatcher {
299            name: "watcher2",
300            available: true,
301        }));
302        registry.register(Box::new(TestWatcher {
303            name: "watcher3",
304            available: false,
305        }));
306
307        let paths = registry.all_watch_paths();
308        // Only available watchers contribute paths
309        assert_eq!(paths.len(), 2);
310    }
311
312    #[test]
313    fn test_default_registry_contains_builtin_watchers() {
314        let registry = default_registry();
315        let watchers = registry.all_watchers();
316
317        // Should have all built-in watchers
318        assert!(watchers.len() >= 10);
319
320        // Check that all watchers are registered
321        assert!(registry.get_watcher("aider").is_some());
322        assert!(registry.get_watcher("amp").is_some());
323        assert!(registry.get_watcher("claude-code").is_some());
324        assert!(registry.get_watcher("cline").is_some());
325        assert!(registry.get_watcher("codex").is_some());
326        assert!(registry.get_watcher("continue").is_some());
327        assert!(registry.get_watcher("gemini").is_some());
328        assert!(registry.get_watcher("kilo-code").is_some());
329        assert!(registry.get_watcher("opencode").is_some());
330        assert!(registry.get_watcher("roo-code").is_some());
331    }
332
333    #[test]
334    fn test_watcher_info_fields() {
335        let watcher = TestWatcher {
336            name: "test",
337            available: true,
338        };
339        let info = watcher.info();
340
341        assert_eq!(info.name, "test");
342        assert_eq!(info.description, "Test watcher");
343        assert!(!info.default_paths.is_empty());
344    }
345
346    #[test]
347    fn test_registry_enabled_watchers() {
348        let mut registry = WatcherRegistry::new();
349        registry.register(Box::new(TestWatcher {
350            name: "watcher-a",
351            available: true,
352        }));
353        registry.register(Box::new(TestWatcher {
354            name: "watcher-b",
355            available: true,
356        }));
357        registry.register(Box::new(TestWatcher {
358            name: "watcher-c",
359            available: false,
360        }));
361
362        // Test filtering by enabled list
363        let enabled = vec!["watcher-a".to_string(), "watcher-c".to_string()];
364        let watchers = registry.enabled_watchers(&enabled);
365
366        // Only watcher-a should be returned (available and enabled)
367        // watcher-b is available but not enabled
368        // watcher-c is enabled but not available
369        assert_eq!(watchers.len(), 1);
370        assert_eq!(watchers[0].info().name, "watcher-a");
371    }
372
373    #[test]
374    fn test_registry_enabled_watchers_empty_list() {
375        let mut registry = WatcherRegistry::new();
376        registry.register(Box::new(TestWatcher {
377            name: "watcher",
378            available: true,
379        }));
380
381        // Empty enabled list should return empty
382        let enabled: Vec<String> = vec![];
383        let watchers = registry.enabled_watchers(&enabled);
384        assert!(watchers.is_empty());
385    }
386}