mod native;
pub use native::NativeClaudeConnector;
use crate::model::Session;
use anyhow::Result;
use async_trait::async_trait;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub enum ConnectorStatus {
Available {
path: PathBuf,
sessions_estimate: Option<usize>,
},
NotFound,
Error(String),
}
impl ConnectorStatus {
pub fn is_available(&self) -> bool {
matches!(self, Self::Available { .. })
}
}
#[derive(Debug, Clone, Default)]
pub struct ImportOptions {
pub path: Option<PathBuf>,
pub since: Option<jiff::Timestamp>,
pub until: Option<jiff::Timestamp>,
pub limit: Option<usize>,
pub incremental: bool,
}
impl ImportOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_path(mut self, path: PathBuf) -> Self {
self.path = Some(path);
self
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn incremental(mut self) -> Self {
self.incremental = true;
self
}
}
#[async_trait]
pub trait SessionConnector: Send + Sync {
fn source_id(&self) -> &str;
fn display_name(&self) -> &str;
fn detect(&self) -> ConnectorStatus;
fn default_path(&self) -> Option<PathBuf>;
async fn import(&self, options: &ImportOptions) -> Result<Vec<Session>>;
}
pub struct ConnectorRegistry {
connectors: Vec<Box<dyn SessionConnector>>,
}
impl ConnectorRegistry {
#[must_use]
#[allow(clippy::vec_init_then_push)] pub fn new() -> Self {
let mut connectors: Vec<Box<dyn SessionConnector>> = Vec::new();
connectors.push(Box::new(NativeClaudeConnector));
#[cfg(feature = "terraphim-session-analyzer")]
{
connectors.push(Box::new(crate::cla::ClaClaudeConnector::default()));
#[cfg(feature = "tsa-full")]
connectors.push(Box::new(crate::cla::ClaCursorConnector::default()));
}
Self { connectors }
}
#[must_use]
pub fn connectors(&self) -> &[Box<dyn SessionConnector>] {
&self.connectors
}
#[must_use]
pub fn get(&self, source_id: &str) -> Option<&dyn SessionConnector> {
self.connectors
.iter()
.find(|c| c.source_id() == source_id)
.map(|c| c.as_ref())
}
pub fn detect_all(&self) -> Vec<(&str, ConnectorStatus)> {
self.connectors
.iter()
.map(|c| (c.source_id(), c.detect()))
.collect()
}
pub fn available(&self) -> Vec<&dyn SessionConnector> {
self.connectors
.iter()
.filter(|c| c.detect().is_available())
.map(|c| c.as_ref())
.collect()
}
pub async fn import_all(&self, options: &ImportOptions) -> Result<Vec<Session>> {
let mut all_sessions = Vec::new();
for connector in self.available() {
match connector.import(options).await {
Ok(mut sessions) => {
all_sessions.append(&mut sessions);
}
Err(e) => {
tracing::warn!("Failed to import from {}: {}", connector.display_name(), e);
}
}
if let Some(limit) = options.limit {
if all_sessions.len() >= limit {
all_sessions.truncate(limit);
break;
}
}
}
Ok(all_sessions)
}
}
impl Default for ConnectorRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connector_registry_creation() {
let registry = ConnectorRegistry::new();
assert!(!registry.connectors().is_empty());
}
#[test]
fn test_import_options_builder() {
let options = ImportOptions::new()
.with_path(PathBuf::from("/test"))
.with_limit(10)
.incremental();
assert_eq!(options.path, Some(PathBuf::from("/test")));
assert_eq!(options.limit, Some(10));
assert!(options.incremental);
}
}