use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use anyhow::Result;
use chrono::{DateTime, Utc};
use crate::parser::ModelSessionStats;
use crate::types::TokenUsage;
pub mod claude;
pub mod codex;
pub mod openclaw;
pub mod pi;
pub(crate) mod pr;
pub use claude::ClaudeProvider;
pub use codex::CodexProvider;
pub use openclaw::OpenClawProvider;
pub use pi::PiProvider;
pub struct DiscoveredFile {
pub path: PathBuf,
pub project_display: String,
pub parent_session_id: Option<String>,
pub archived: bool,
pub source_key: Option<String>,
}
pub struct MessageForFts {
pub msg_type: String,
pub content: String,
pub timestamp_ms: Option<i64>,
}
#[derive(Default)]
pub struct ProviderRecord {
pub session_id: Option<String>,
pub parent_session_id: Option<String>,
pub first_timestamp: Option<DateTime<Utc>>,
pub last_timestamp: Option<DateTime<Utc>>,
pub duration_ms: u64,
pub message_count: usize,
pub model: Option<String>,
pub usage: TokenUsage,
pub model_usage: BTreeMap<String, ModelSessionStats>,
pub tool_names: Vec<String>,
pub messages: Vec<MessageForFts>,
pub turn_durations: Vec<(u64, String)>,
pub pr_links: Vec<(i64, String, String, String)>,
pub file_paths_modified: Vec<String>,
pub thinking_block_count: u64,
pub stop_reason_counts: HashMap<String, u64>,
pub attachments: Vec<(String, String)>,
pub permission_modes: Vec<(String, String)>,
pub inference_geo: Option<String>,
pub speed: Option<f64>,
pub service_tier: Option<String>,
pub iterations: u64,
pub project_display: String,
pub source_key: Option<String>,
pub embedded_cost: Option<f64>,
pub extras: Option<String>,
}
pub trait SessionProvider {
fn id(&self) -> &'static str;
fn root_dir(&self) -> &Path;
fn enabled(&self) -> bool {
self.root_dir().exists()
}
fn enumerate(&self) -> Result<Vec<DiscoveredFile>>;
fn parse(&self, file: &DiscoveredFile) -> Result<ProviderRecord>;
}
pub enum Provider {
Claude(ClaudeProvider),
Codex(CodexProvider),
OpenClaw(OpenClawProvider),
Pi(PiProvider),
}
impl Provider {
fn inner(&self) -> &dyn SessionProvider {
match self {
Provider::Claude(p) => p,
Provider::Codex(p) => p,
Provider::OpenClaw(p) => p,
Provider::Pi(p) => p,
}
}
pub fn id(&self) -> &'static str {
self.inner().id()
}
pub fn root_dir(&self) -> &Path {
self.inner().root_dir()
}
pub fn enabled(&self) -> bool {
self.inner().enabled()
}
pub fn enumerate(&self) -> Result<Vec<DiscoveredFile>> {
self.inner().enumerate()
}
pub fn parse(&self, file: &DiscoveredFile) -> Result<ProviderRecord> {
self.inner().parse(file)
}
}
pub fn enabled_default() -> Result<Vec<Provider>> {
let candidates = vec![
Provider::Claude(ClaudeProvider::new()?),
Provider::Codex(CodexProvider::new()?),
Provider::OpenClaw(OpenClawProvider::new()?),
Provider::Pi(PiProvider::new()?),
];
Ok(candidates.into_iter().filter(|p| p.enabled()).collect())
}