chasm_cli/providers/
cursor.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Cursor IDE chat provider
4
5use super::{ChatProvider, ProviderType};
6use crate::models::ChatSession;
7use crate::storage::parse_session_json;
8use anyhow::Result;
9use std::path::PathBuf;
10
11/// Cursor IDE chat provider
12///
13/// Cursor stores chat sessions in a similar format to VS Code Copilot,
14/// located in the Cursor app data directory.
15pub struct CursorProvider {
16    /// Path to Cursor's workspace storage
17    storage_path: PathBuf,
18    /// Whether Cursor is installed and accessible
19    available: bool,
20}
21
22impl CursorProvider {
23    /// Discover Cursor installation and create provider
24    pub fn discover() -> Option<Self> {
25        let storage_path = Self::find_cursor_storage()?;
26
27        Some(Self {
28            available: storage_path.exists(),
29            storage_path,
30        })
31    }
32
33    /// Find Cursor's workspace storage directory
34    fn find_cursor_storage() -> Option<PathBuf> {
35        #[cfg(target_os = "windows")]
36        {
37            let appdata = dirs::data_dir()?;
38            let cursor_path = appdata.join("Cursor").join("User").join("workspaceStorage");
39            if cursor_path.exists() {
40                return Some(cursor_path);
41            }
42            // Also check Roaming
43            let roaming = std::env::var("APPDATA").ok()?;
44            let roaming_path = PathBuf::from(roaming)
45                .join("Cursor")
46                .join("User")
47                .join("workspaceStorage");
48            if roaming_path.exists() {
49                return Some(roaming_path);
50            }
51        }
52
53        #[cfg(target_os = "macos")]
54        {
55            let home = dirs::home_dir()?;
56            let cursor_path = home
57                .join("Library")
58                .join("Application Support")
59                .join("Cursor")
60                .join("User")
61                .join("workspaceStorage");
62            if cursor_path.exists() {
63                return Some(cursor_path);
64            }
65        }
66
67        #[cfg(target_os = "linux")]
68        {
69            let config = dirs::config_dir()?;
70            let cursor_path = config.join("Cursor").join("User").join("workspaceStorage");
71            if cursor_path.exists() {
72                return Some(cursor_path);
73            }
74        }
75
76        None
77    }
78
79    /// List all workspace directories with chat sessions
80    fn list_workspaces(&self) -> Result<Vec<PathBuf>> {
81        let mut workspaces = Vec::new();
82
83        if self.storage_path.exists() {
84            for entry in std::fs::read_dir(&self.storage_path)? {
85                let entry = entry?;
86                let path = entry.path();
87
88                if path.is_dir() {
89                    // Check for chat sessions directory
90                    let chat_path = path.join("chatSessions");
91                    if chat_path.exists() {
92                        workspaces.push(path);
93                    }
94                }
95            }
96        }
97
98        Ok(workspaces)
99    }
100}
101
102impl ChatProvider for CursorProvider {
103    fn provider_type(&self) -> ProviderType {
104        ProviderType::Cursor
105    }
106
107    fn name(&self) -> &str {
108        "Cursor"
109    }
110
111    fn is_available(&self) -> bool {
112        self.available
113    }
114
115    fn sessions_path(&self) -> Option<PathBuf> {
116        Some(self.storage_path.clone())
117    }
118
119    fn list_sessions(&self) -> Result<Vec<ChatSession>> {
120        let mut sessions = Vec::new();
121
122        for workspace in self.list_workspaces()? {
123            let chat_path = workspace.join("chatSessions");
124
125            if chat_path.exists() {
126                for entry in std::fs::read_dir(&chat_path)? {
127                    let entry = entry?;
128                    let path = entry.path();
129
130                    if path.extension().is_some_and(|e| e == "json") {
131                        if let Ok(content) = std::fs::read_to_string(&path) {
132                            if let Ok(session) = parse_session_json(&content) {
133                                sessions.push(session);
134                            }
135                        }
136                    }
137                }
138            }
139        }
140
141        Ok(sessions)
142    }
143
144    fn import_session(&self, session_id: &str) -> Result<ChatSession> {
145        // Search for the session file across all workspaces
146        for workspace in self.list_workspaces()? {
147            let session_path = workspace
148                .join("chatSessions")
149                .join(format!("{}.json", session_id));
150
151            if session_path.exists() {
152                let content = std::fs::read_to_string(&session_path)?;
153                let session: ChatSession = serde_json::from_str(&content)?;
154                return Ok(session);
155            }
156        }
157
158        anyhow::bail!("Session not found: {}", session_id)
159    }
160
161    fn export_session(&self, _session: &ChatSession) -> Result<()> {
162        // Cursor uses similar format to VS Code, so export is straightforward
163        // However, we need a target workspace
164        anyhow::bail!("Export to Cursor not yet implemented - use import instead")
165    }
166}