terraphim_usage 1.20.3

Usage tracking and analytics for Terraphim AI
Documentation
use crate::{MetricLine, ProgressFormat, ProviderUsage, Result, UsageError, UsageProvider};
use std::time::Duration;

pub struct CcusageProvider {
    client: std::sync::Mutex<terraphim_ccusage::CcusageClient>,
}

impl CcusageProvider {
    pub fn new() -> Self {
        let client =
            terraphim_ccusage::CcusageClient::new(terraphim_ccusage::CcusageProvider::Claude)
                .with_cache_ttl(Duration::from_secs(300));
        Self {
            client: std::sync::Mutex::new(client),
        }
    }
}

impl Default for CcusageProvider {
    fn default() -> Self {
        Self::new()
    }
}

impl UsageProvider for CcusageProvider {
    fn id(&self) -> &str {
        "ccusage"
    }

    fn display_name(&self) -> &str {
        "Claude Code (ccusage)"
    }

    fn fetch_usage(
        &self,
    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ProviderUsage>> + Send + '_>>
    {
        Box::pin(async move {
            let since = chrono::Utc::now()
                .checked_sub_signed(chrono::Duration::days(30))
                .map(|d| d.format("%Y-%m-%d").to_string())
                .unwrap_or_else(|| "2020-01-01".to_string());

            let report = {
                let mut client = self.client.lock().map_err(|e| UsageError::FetchFailed {
                    provider: "ccusage".to_string(),
                    source: format!("Lock poisoned: {}", e).into(),
                })?;
                client
                    .query(&since, None)
                    .map_err(|e| UsageError::FetchFailed {
                        provider: "ccusage".to_string(),
                        source: e.into(),
                    })?
            };

            let mut lines = Vec::new();

            let total_cost: f64 = report
                .daily
                .iter()
                .filter_map(|d| d.total_cost.or(d.cost_usd))
                .sum();
            let total_tokens: u64 = report.daily.iter().filter_map(|d| d.total_tokens).sum();

            let daily_cost: f64 = report
                .daily
                .last()
                .and_then(|d| d.total_cost.or(d.cost_usd))
                .unwrap_or(0.0);

            let daily_tokens: u64 = report
                .daily
                .last()
                .and_then(|d| d.total_tokens)
                .unwrap_or(0);

            let days_with_usage = report.daily.len() as f64;

            lines.push(MetricLine::Progress {
                label: "30-day spend".to_string(),
                used: total_cost,
                limit: 50.0,
                format: ProgressFormat::Dollars,
                resets_at: None,
                period_duration_ms: Some(30 * 24 * 3600 * 1000),
                color: None,
            });

            lines.push(MetricLine::Text {
                label: "Today".to_string(),
                value: format!("${:.2} ({} tokens)", daily_cost, daily_tokens),
                color: None,
                subtitle: None,
            });

            lines.push(MetricLine::Text {
                label: "30-day total".to_string(),
                value: format!(
                    "${:.2} ({} tokens, {} days)",
                    total_cost, total_tokens, days_with_usage as u64
                ),
                color: None,
                subtitle: None,
            });

            Ok(ProviderUsage {
                provider_id: "ccusage".to_string(),
                display_name: "Claude Code (ccusage)".to_string(),
                plan: None,
                lines,
                fetched_at: chrono::Utc::now().to_rfc3339(),
            })
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ccusage_provider_id() {
        let provider = CcusageProvider::new();
        assert_eq!(provider.id(), "ccusage");
    }

    #[test]
    fn test_ccusage_provider_display_name() {
        let provider = CcusageProvider::new();
        assert_eq!(provider.display_name(), "Claude Code (ccusage)");
    }
}