Skip to main content

smc/cmd/
stats.rs

1/// smc stats — aggregate statistics across all conversation logs.
2use std::collections::HashMap;
3use std::io::Write;
4
5use anyhow::Result;
6use serde::Serialize;
7
8use crate::output::Emitter;
9use crate::util::discover::SessionFile;
10
11// ── Opts ───────────────────────────────────────────────────────────────────
12
13pub struct StatsOpts {
14    pub max_tokens: usize,
15}
16
17// ── Records ────────────────────────────────────────────────────────────────
18
19#[derive(Serialize, Debug)]
20struct StatsRecord {
21    #[serde(rename = "type")]
22    record_type: &'static str,
23    total_sessions: usize,
24    total_size_bytes: u64,
25    total_size_human: String,
26    project_count: usize,
27    projects: Vec<ProjectStat>,
28}
29
30#[derive(Serialize, Debug)]
31struct ProjectStat {
32    name: String,
33    sessions: usize,
34    size_bytes: u64,
35    size_human: String,
36}
37
38// ── run ────────────────────────────────────────────────────────────────────
39
40pub fn run<W: Write>(_opts: &StatsOpts, files: &[SessionFile], em: &mut Emitter<W>) -> Result<()> {
41    let total_size: u64 = files.iter().map(|f| f.size_bytes).sum();
42
43    let mut projects: HashMap<String, (usize, u64)> = HashMap::new();
44    for f in files {
45        let entry = projects.entry(f.project_name.clone()).or_default();
46        entry.0 += 1;
47        entry.1 += f.size_bytes;
48    }
49
50    let mut sorted: Vec<_> = projects.into_iter().collect();
51    sorted.sort_by(|a, b| b.1 .1.cmp(&a.1 .1));
52
53    let project_stats: Vec<ProjectStat> = sorted
54        .iter()
55        .take(15)
56        .map(|(name, (count, size))| ProjectStat {
57            name: name.clone(),
58            sessions: *count,
59            size_bytes: *size,
60            size_human: format_bytes(*size),
61        })
62        .collect();
63
64    let rec = StatsRecord {
65        record_type: "stats",
66        total_sessions: files.len(),
67        total_size_bytes: total_size,
68        total_size_human: format_bytes(total_size),
69        project_count: sorted.len(),
70        projects: project_stats,
71    };
72
73    em.emit(&rec)?;
74    em.flush()?;
75    Ok(())
76}
77
78// ── Helpers ────────────────────────────────────────────────────────────────
79
80pub fn format_bytes(bytes: u64) -> String {
81    if bytes < 1024 {
82        format!("{}B", bytes)
83    } else if bytes < 1024 * 1024 {
84        format!("{:.1}KB", bytes as f64 / 1024.0)
85    } else if bytes < 1024 * 1024 * 1024 {
86        format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
87    } else {
88        format!("{:.2}GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
89    }
90}