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