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