use std::path::PathBuf;
use anyhow::Result;
use crate::filter::{ProviderKind, QueryFilter, ResolvedFilter};
use crate::index::{
ActivityData, CostSummary, FileModRow, IndexStore, IndexedSession, ModelUsageRow, PrLinkRow,
ProviderStatusRow, RetentionStats, SearchFtsOptions, SearchHit, SessionCostRow, SessionDetail,
SessionToolRow, SummaryData, TimelineRow, ToolRow, TurnStatsRow,
};
use crate::providers::{
ClaudeProvider, CodexProvider, CopilotProvider, CopilotVscodeProvider, OpenClawProvider,
PiProvider, Provider as ProviderImpl,
};
pub use crate::filter::{ProviderKind as Provider, QueryFilter as Filter};
const NO_PROVIDER_MATCH: &str = "__claudex_no_provider_match__";
#[derive(Debug, Clone, Default)]
pub struct ClaudexConfig {
pub state_dir: Option<PathBuf>,
pub providers: Vec<ProviderKind>,
}
pub struct Claudex {
index: IndexStore,
providers: Vec<ProviderImpl>,
provider_scope: Vec<String>,
}
impl Claudex {
pub fn new() -> Result<Self> {
Self::with_config(ClaudexConfig::default())
}
pub fn with_config(config: ClaudexConfig) -> Result<Self> {
let index = match config.state_dir {
Some(dir) => IndexStore::open_at(&dir.join("index.db"))?,
None => IndexStore::open()?,
};
let providers = providers_for(&config.providers)?;
let provider_scope = provider_scope(&providers);
Ok(Self {
index,
providers,
provider_scope,
})
}
pub fn index(&self) -> &IndexStore {
&self.index
}
pub fn index_mut(&mut self) -> &mut IndexStore {
&mut self.index
}
pub fn ensure_fresh(&mut self) -> Result<()> {
self.index.ensure_fresh(&self.providers)
}
pub fn sync_now(&mut self) -> Result<usize> {
self.index.sync_now(&self.providers)
}
pub fn force_rebuild(&mut self) -> Result<usize> {
self.index.force_rebuild(&self.providers)
}
pub fn summary(&mut self, filter: QueryFilter) -> Result<SummaryData> {
let filter = self.prepare(filter)?;
self.index.query_summary(&filter)
}
pub fn sessions(
&mut self,
project: Option<&str>,
file: Option<&str>,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<IndexedSession>> {
let filter = self.prepare(filter)?;
self.index.query_sessions(project, file, &filter, limit)
}
pub fn search(
&mut self,
query: &str,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<SearchHit>> {
let filter = self.prepare(filter)?;
self.index.search_fts(SearchFtsOptions {
query,
project_filter: None,
filter: &filter,
role_filter: None,
tool_filter: None,
file_filter: None,
pr_filter: None,
context: 0,
limit,
})
}
pub fn costs_by_project(
&mut self,
project: Option<&str>,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<crate::index::ProjectCostRow>> {
let filter = self.prepare(filter)?;
self.index.query_cost_by_project(project, &filter, limit)
}
pub fn costs_per_session(
&mut self,
project: Option<&str>,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<SessionCostRow>> {
let filter = self.prepare(filter)?;
self.index.query_cost_per_session(project, &filter, limit)
}
pub fn cost_summary(
&mut self,
project: Option<&str>,
filter: QueryFilter,
) -> Result<CostSummary> {
let filter = self.prepare(filter)?;
self.index.query_cost_summary(project, &filter)
}
pub fn tools(
&mut self,
project: Option<&str>,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<ToolRow>> {
let filter = self.prepare(filter)?;
self.index.query_tools_aggregate(project, &filter, limit)
}
pub fn tools_per_session(
&mut self,
project: Option<&str>,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<SessionToolRow>> {
let filter = self.prepare(filter)?;
self.index.query_tools_per_session(project, &filter, limit)
}
pub fn models(
&mut self,
project: Option<&str>,
filter: QueryFilter,
) -> Result<Vec<ModelUsageRow>> {
let filter = self.prepare(filter)?;
self.index.query_model_usage(project, &filter)
}
pub fn timeline(
&mut self,
filter: QueryFilter,
weekly: bool,
limit: usize,
) -> Result<Vec<TimelineRow>> {
let filter = self.prepare(filter)?;
self.index.query_timeline(&filter, weekly, limit)
}
pub fn activity(&mut self, filter: QueryFilter, limit: usize) -> Result<ActivityData> {
let filter = self.prepare(filter)?;
self.index.query_activity(&filter, limit)
}
pub fn provider_status(
&mut self,
filter: QueryFilter,
deep: bool,
) -> Result<Vec<ProviderStatusRow>> {
let filter = self.prepare(filter)?;
self.index.provider_status(&self.providers, deep, &filter)
}
pub fn retention_stats(&mut self, filter: QueryFilter) -> Result<RetentionStats> {
let filter = self.prepare(filter)?;
self.index.retention_stats(&filter)
}
pub fn session_detail(&mut self, file_path: &str) -> Result<Option<SessionDetail>> {
self.ensure_fresh()?;
let detail = self.index.query_session_detail(file_path)?;
Ok(detail.filter(|row| self.provider_in_scope(&row.provider)))
}
pub fn session_matches(
&mut self,
selector: &str,
project_filter: Option<&str>,
) -> Result<Vec<IndexedSession>> {
self.ensure_fresh()?;
let matches = self.index.query_session_matches(selector, project_filter)?;
Ok(matches
.into_iter()
.filter(|row| self.provider_in_scope(&row.provider))
.collect())
}
pub fn prs(
&mut self,
project: Option<&str>,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<PrLinkRow>> {
let filter = self.prepare(filter)?;
self.index.ensure_pr_links_fresh(&self.providers)?;
self.index.query_pr_links(project, &filter, limit)
}
pub fn files(
&mut self,
project: Option<&str>,
path: Option<&str>,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<FileModRow>> {
let filter = self.prepare(filter)?;
self.index.query_file_mods(project, path, &filter, limit)
}
pub fn turns(
&mut self,
project: Option<&str>,
filter: QueryFilter,
limit: usize,
) -> Result<Vec<TurnStatsRow>> {
let filter = self.prepare(filter)?;
self.index.query_turn_stats(project, &filter, limit)
}
fn prepare(&mut self, filter: QueryFilter) -> Result<ResolvedFilter> {
self.ensure_fresh()?;
let mut filter = filter.resolve()?;
self.apply_provider_scope(&mut filter);
Ok(filter)
}
fn apply_provider_scope(&self, filter: &mut ResolvedFilter) {
if self.provider_scope.is_empty() {
return;
}
if filter.providers.is_empty() {
filter.providers = self.provider_scope.clone();
return;
}
filter
.providers
.retain(|provider| self.provider_scope.iter().any(|id| id == provider));
if filter.providers.is_empty() {
filter.providers.push(NO_PROVIDER_MATCH.to_string());
}
}
fn provider_in_scope(&self, provider: &str) -> bool {
self.provider_scope.is_empty() || self.provider_scope.iter().any(|id| id == provider)
}
}
fn provider_scope(providers: &[ProviderImpl]) -> Vec<String> {
let mut scope: Vec<String> = providers
.iter()
.map(|provider| provider.id().to_string())
.collect();
scope.sort();
scope.dedup();
scope
}
fn providers_for(kinds: &[ProviderKind]) -> Result<Vec<ProviderImpl>> {
if kinds.is_empty() {
return crate::providers::enabled_default();
}
let mut providers = Vec::new();
for kind in kinds {
let provider = match kind {
ProviderKind::Claude => ProviderImpl::Claude(ClaudeProvider::new()?),
ProviderKind::Codex => ProviderImpl::Codex(CodexProvider::new()?),
ProviderKind::Copilot => ProviderImpl::Copilot(CopilotProvider::new()?),
ProviderKind::CopilotVscode => {
ProviderImpl::CopilotVscode(CopilotVscodeProvider::new()?)
}
ProviderKind::OpenClaw => ProviderImpl::OpenClaw(OpenClawProvider::new()?),
ProviderKind::Pi => ProviderImpl::Pi(PiProvider::new()?),
};
providers.push(provider);
}
Ok(providers)
}