mod native;
#[cfg(feature = "aider-connector")]
mod aider;
#[cfg(feature = "cline-connector")]
mod cline;
#[cfg(feature = "opencode-connector")]
mod opencode;
#[cfg(feature = "codex-connector")]
mod codex;
pub use native::NativeClaudeConnector;
#[cfg(feature = "aider-connector")]
pub use aider::AiderConnector;
#[cfg(feature = "cline-connector")]
pub use cline::ClineConnector;
#[cfg(feature = "opencode-connector")]
pub use opencode::OpenCodeConnector;
#[cfg(feature = "codex-connector")]
pub use codex::CodexConnector;
use crate::model::Session;
use anyhow::Result;
use async_trait::async_trait;
use std::path::PathBuf;
use tokio::sync::mpsc;
#[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>>;
fn supports_watch(&self) -> bool {
false
}
async fn watch(&self) -> Result<mpsc::Receiver<Session>> {
anyhow::bail!("Watch not supported for this connector")
}
}
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 = "aider-connector")]
connectors.push(Box::new(AiderConnector));
#[cfg(feature = "cline-connector")]
connectors.push(Box::new(ClineConnector::new()));
#[cfg(feature = "opencode-connector")]
connectors.push(Box::new(OpenCodeConnector));
#[cfg(feature = "codex-connector")]
connectors.push(Box::new(CodexConnector));
#[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 connectors = self.available();
let total_connectors = connectors.len();
let mut all_sessions = Vec::new();
for (idx, connector) in connectors.iter().enumerate() {
tracing::info!(
"Importing from {}/{}: {}",
idx + 1,
total_connectors,
connector.display_name()
);
match connector.import(options).await {
Ok(mut sessions) => {
let count = sessions.len();
all_sessions.append(&mut sessions);
tracing::info!(
" Imported {} sessions from {}",
count,
connector.display_name()
);
}
Err(e) => {
tracing::warn!("Failed to import from {}: {}", connector.display_name(), e);
}
}
if let Some(limit) = options.limit
&& all_sessions.len() >= limit
{
tracing::info!("Reached global limit of {} sessions", limit);
all_sessions.truncate(limit);
break;
}
}
tracing::info!("Total sessions imported: {}", all_sessions.len());
Ok(all_sessions)
}
pub async fn watch_all(&self) -> Vec<mpsc::Receiver<Session>> {
let mut receivers = Vec::new();
for connector in self.connectors() {
if connector.supports_watch() {
match connector.watch().await {
Ok(rx) => {
tracing::info!("Watching: {}", connector.display_name());
receivers.push(rx);
}
Err(e) => {
tracing::warn!(
"Failed to start watch for {}: {}",
connector.display_name(),
e
);
}
}
}
}
receivers
}
}
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);
}
}