Skip to content

Commit 15fd8dc

Browse files
authored
feat(parser): Add Cursor Client parser for IDE chat history (#111)
* feat(parser): Add Cursor Client parser for IDE chat history - Add CursorClientParser to parse state.vscdb SQLite files - Support composerData and bubbleId key extraction - Auto-detect macOS/Windows/Linux storage paths - Scan both global and workspace storage locations - Add Provider::CursorClient enum variant - Integrate with CLI import command (--provider cursor-client) - Add Tauri GUI import support - Add Cursor button to frontend import dialog * fix: Apply cargo fmt and fix clippy warnings - Use #[derive(Default)] instead of manual impl for StorageMode - Use next_back() instead of last() for DoubleEndedIterator - Collapse nested if statements - Convert extract_text_recursive to associated function
1 parent bbe54a3 commit 15fd8dc

File tree

10 files changed

+1329
-7
lines changed

10 files changed

+1329
-7
lines changed

crates/retrochat-cli/src/commands/help.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ fn format_provider_arg(p: &Provider) -> String {
266266
Provider::ClaudeCode => "claude".to_string(),
267267
Provider::GeminiCLI => "gemini".to_string(),
268268
Provider::Codex => "codex".to_string(),
269+
Provider::CursorClient => "cursor-client".to_string(),
269270
Provider::Other(name) => name.clone(),
270271
}
271272
}

crates/retrochat-cli/src/commands/import.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,29 @@ async fn import_providers(providers: Vec<Provider>, overwrite: bool) -> Result<(
9797
}
9898
println!();
9999
}
100+
Provider::CursorClient => {
101+
println!("Importing from Cursor Client...");
102+
if let Some(workspace_path) =
103+
retrochat_core::parsers::CursorClientParser::get_default_workspace_path()
104+
{
105+
if let Some(parent) = workspace_path.parent() {
106+
let global_db = parent.join("globalStorage/state.vscdb");
107+
if global_db.exists() {
108+
if let Err(e) =
109+
import_file(global_db.to_string_lossy().to_string(), overwrite)
110+
.await
111+
{
112+
eprintln!("Error importing Cursor global storage: {e}");
113+
} else {
114+
imported_any = true;
115+
}
116+
}
117+
}
118+
} else {
119+
eprintln!("Could not find Cursor workspace storage path");
120+
}
121+
println!();
122+
}
100123
Provider::Other(name) => {
101124
eprintln!("Unknown provider: {name}");
102125
}

crates/retrochat-core/src/models/provider/enum.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ pub enum Provider {
88
ClaudeCode,
99
GeminiCLI,
1010
Codex,
11+
/// Cursor App (VSCode-based IDE)
12+
CursorClient,
1113
Other(String),
1214
}
1315

1416
// Implement ValueEnum manually because we need to exclude Other variant
1517
impl ValueEnum for Provider {
1618
fn value_variants<'a>() -> &'a [Self] {
17-
&[Self::All, Self::ClaudeCode, Self::GeminiCLI, Self::Codex]
19+
&[
20+
Self::All,
21+
Self::ClaudeCode,
22+
Self::GeminiCLI,
23+
Self::Codex,
24+
Self::CursorClient,
25+
]
1826
}
1927

2028
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
@@ -23,6 +31,7 @@ impl ValueEnum for Provider {
2331
Self::ClaudeCode => Some(clap::builder::PossibleValue::new("claude")),
2432
Self::GeminiCLI => Some(clap::builder::PossibleValue::new("gemini")),
2533
Self::Codex => Some(clap::builder::PossibleValue::new("codex")),
34+
Self::CursorClient => Some(clap::builder::PossibleValue::new("cursor-client")),
2635
Self::Other(_) => None,
2736
}
2837
}
@@ -35,6 +44,7 @@ impl std::fmt::Display for Provider {
3544
Provider::ClaudeCode => write!(f, "Claude Code"),
3645
Provider::GeminiCLI => write!(f, "Gemini CLI"),
3746
Provider::Codex => write!(f, "Codex"),
47+
Provider::CursorClient => write!(f, "Cursor Client"),
3848
Provider::Other(name) => write!(f, "{name}"),
3949
}
4050
}
@@ -49,6 +59,7 @@ impl std::str::FromStr for Provider {
4959
"Claude Code" | "claude" => Ok(Provider::ClaudeCode),
5060
"Gemini CLI" | "gemini" => Ok(Provider::GeminiCLI),
5161
"Codex" | "codex" => Ok(Provider::Codex),
62+
"Cursor Client" | "cursor-client" => Ok(Provider::CursorClient),
5263
_ => Ok(Provider::Other(s.to_string())),
5364
}
5465
}
@@ -57,7 +68,12 @@ impl std::str::FromStr for Provider {
5768
impl Provider {
5869
/// Get all concrete provider variants (excluding All and Other)
5970
pub fn all_concrete() -> Vec<Self> {
60-
vec![Self::ClaudeCode, Self::GeminiCLI, Self::Codex]
71+
vec![
72+
Self::ClaudeCode,
73+
Self::GeminiCLI,
74+
Self::Codex,
75+
Self::CursorClient,
76+
]
6177
}
6278

6379
/// Check if this is a concrete provider (not All or Other)

crates/retrochat-core/src/models/provider/registry.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ impl ProviderRegistry {
3737
Provider::ClaudeCode => ClaudeCodeConfig::create(),
3838
Provider::GeminiCLI => GeminiCliConfig::create(),
3939
Provider::Codex => CodexConfig::create(),
40-
Provider::All => continue, // Skip aggregate
41-
Provider::Other(_) => continue, // Skip unknown providers
40+
Provider::CursorClient => continue, // Skip for now - uses directory-based detection
41+
Provider::All => continue, // Skip aggregate
42+
Provider::Other(_) => continue, // Skip unknown providers
4243
};
4344
self.providers.insert(provider, config);
4445
}

0 commit comments

Comments
 (0)