Skip to main content

lore_cli/capture/watchers/
common.rs

1//! Common utilities shared across watcher implementations.
2//!
3//! This module provides helper functions for parsing timestamps, roles, and UUIDs
4//! that are used by multiple watcher implementations. It also provides the
5//! platform-specific path to VS Code's global storage directory.
6
7use chrono::{DateTime, TimeZone, Utc};
8use std::path::PathBuf;
9use uuid::Uuid;
10
11use crate::storage::models::MessageRole;
12
13/// Returns the platform-specific path to VS Code's global storage directory.
14///
15/// This is where VS Code extensions store their data:
16/// - macOS: `~/Library/Application Support/Code/User/globalStorage`
17/// - Linux: `~/.config/Code/User/globalStorage`
18/// - Windows: `%APPDATA%/Code/User/globalStorage`
19pub fn vscode_global_storage() -> PathBuf {
20    #[cfg(target_os = "macos")]
21    {
22        dirs::home_dir()
23            .unwrap_or_else(|| PathBuf::from("."))
24            .join("Library/Application Support/Code/User/globalStorage")
25    }
26    #[cfg(target_os = "linux")]
27    {
28        dirs::config_dir()
29            .unwrap_or_else(|| PathBuf::from("."))
30            .join("Code/User/globalStorage")
31    }
32    #[cfg(target_os = "windows")]
33    {
34        dirs::config_dir()
35            .unwrap_or_else(|| PathBuf::from("."))
36            .join("Code/User/globalStorage")
37    }
38    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
39    {
40        dirs::config_dir()
41            .unwrap_or_else(|| PathBuf::from("."))
42            .join("Code/User/globalStorage")
43    }
44}
45
46/// Parses a role string into a MessageRole.
47///
48/// Handles common role names used across different AI tools:
49/// - "user", "human" -> MessageRole::User
50/// - "assistant" -> MessageRole::Assistant
51/// - "system" -> MessageRole::System
52///
53/// Returns None for unrecognized roles.
54pub fn parse_role(role: &str) -> Option<MessageRole> {
55    match role {
56        "user" | "human" => Some(MessageRole::User),
57        "assistant" => Some(MessageRole::Assistant),
58        "system" => Some(MessageRole::System),
59        _ => None,
60    }
61}
62
63/// Parses a timestamp from milliseconds since Unix epoch.
64///
65/// Returns None if the timestamp is invalid or out of range.
66pub fn parse_timestamp_millis(ms: i64) -> Option<DateTime<Utc>> {
67    Utc.timestamp_millis_opt(ms).single()
68}
69
70/// Parses a timestamp from an RFC3339 formatted string.
71///
72/// Returns None if the string cannot be parsed.
73pub fn parse_timestamp_rfc3339(s: &str) -> Option<DateTime<Utc>> {
74    DateTime::parse_from_rfc3339(s)
75        .ok()
76        .map(|dt| dt.with_timezone(&Utc))
77}
78
79/// Parses a string as a UUID, or generates a new one if parsing fails.
80///
81/// This is useful when importing sessions from tools that may use non-UUID
82/// identifiers. The generated UUID is random and not deterministic.
83#[allow(dead_code)]
84pub fn parse_uuid_or_generate(s: &str) -> Uuid {
85    Uuid::parse_str(s).unwrap_or_else(|_| Uuid::new_v4())
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_vscode_global_storage_returns_valid_path() {
94        let path = vscode_global_storage();
95        // Should end with globalStorage
96        assert!(path.to_string_lossy().contains("globalStorage"));
97    }
98
99    #[test]
100    fn test_parse_role_user() {
101        assert_eq!(parse_role("user"), Some(MessageRole::User));
102    }
103
104    #[test]
105    fn test_parse_role_human() {
106        assert_eq!(parse_role("human"), Some(MessageRole::User));
107    }
108
109    #[test]
110    fn test_parse_role_assistant() {
111        assert_eq!(parse_role("assistant"), Some(MessageRole::Assistant));
112    }
113
114    #[test]
115    fn test_parse_role_system() {
116        assert_eq!(parse_role("system"), Some(MessageRole::System));
117    }
118
119    #[test]
120    fn test_parse_role_unknown() {
121        assert_eq!(parse_role("unknown"), None);
122        assert_eq!(parse_role(""), None);
123        assert_eq!(parse_role("thinking"), None);
124        assert_eq!(parse_role("tool"), None);
125    }
126
127    #[test]
128    fn test_parse_timestamp_millis_valid() {
129        let ts = parse_timestamp_millis(1704067200000);
130        assert!(ts.is_some());
131        let dt = ts.unwrap();
132        assert_eq!(dt.timestamp_millis(), 1704067200000);
133    }
134
135    #[test]
136    fn test_parse_timestamp_millis_zero() {
137        let ts = parse_timestamp_millis(0);
138        assert!(ts.is_some());
139        assert_eq!(ts.unwrap().timestamp(), 0);
140    }
141
142    #[test]
143    fn test_parse_timestamp_rfc3339_valid() {
144        let ts = parse_timestamp_rfc3339("2025-01-15T10:00:00.000Z");
145        assert!(ts.is_some());
146        let dt = ts.unwrap();
147        assert!(dt.to_rfc3339().contains("2025-01-15"));
148    }
149
150    #[test]
151    fn test_parse_timestamp_rfc3339_with_offset() {
152        let ts = parse_timestamp_rfc3339("2025-01-15T10:00:00-05:00");
153        assert!(ts.is_some());
154    }
155
156    #[test]
157    fn test_parse_timestamp_rfc3339_invalid() {
158        assert!(parse_timestamp_rfc3339("not a timestamp").is_none());
159        assert!(parse_timestamp_rfc3339("").is_none());
160        assert!(parse_timestamp_rfc3339("2025-01-15").is_none());
161    }
162
163    #[test]
164    fn test_parse_uuid_or_generate_valid_uuid() {
165        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
166        let uuid = parse_uuid_or_generate(uuid_str);
167        assert_eq!(uuid.to_string(), uuid_str);
168    }
169
170    #[test]
171    fn test_parse_uuid_or_generate_invalid_generates_new() {
172        let uuid = parse_uuid_or_generate("not-a-uuid");
173        assert!(!uuid.is_nil());
174        // Should be a valid UUID v4
175        assert_eq!(uuid.get_version_num(), 4);
176    }
177
178    #[test]
179    fn test_parse_uuid_or_generate_empty_generates_new() {
180        let uuid = parse_uuid_or_generate("");
181        assert!(!uuid.is_nil());
182    }
183}