Skip to main content

claudex_cli/commands/
activity.rs

1use anyhow::Result;
2use chrono::DateTime;
3
4use crate::cli::ResolvedFilter;
5use crate::commands::sessions::format_duration;
6use crate::ui;
7use claudex::index::IndexStore;
8use claudex::providers::enabled_default;
9use claudex::store::short_name;
10
11pub fn run(limit: usize, json: bool, filter: &ResolvedFilter) -> Result<()> {
12    let providers = enabled_default()?;
13    let mut idx = IndexStore::open()?;
14    idx.ensure_fresh(&providers)?;
15    idx.ensure_pr_links_fresh(&providers)?;
16    let data = idx.query_activity(filter, limit)?;
17
18    if json {
19        println!(
20            "{}",
21            serde_json::to_string_pretty(&serde_json::json!({
22                "summary": {
23                    "sessions": data.summary.total_sessions,
24                    "cost_usd": data.summary.total_cost,
25                    "tokens": data.summary.total_input_tokens + data.summary.total_output_tokens + data.summary.total_cache_creation + data.summary.total_cache_read,
26                    "pr_count": data.summary.pr_count,
27                    "files_modified_count": data.summary.files_modified_count,
28                    "avg_turn_duration_ms": data.summary.avg_turn_duration_ms,
29                },
30                "recent_sessions": data.recent_sessions.iter().map(|s| serde_json::json!({
31                    "provider": s.provider,
32                    "project": s.project_name,
33                    "session_id": s.session_id,
34                    "date": s.last_timestamp_ms.or(s.first_timestamp_ms).and_then(DateTime::from_timestamp_millis).map(|d| d.to_rfc3339()),
35                    "model": s.model,
36                    "cost_known": true,
37                })).collect::<Vec<_>>(),
38                "recent_prs": data.recent_prs.iter().map(|p| serde_json::json!({
39                    "provider": p.provider,
40                    "project": p.project,
41                    "session_id": p.session_id,
42                    "timestamp": p.timestamp,
43                    "pr_number": p.pr_number,
44                    "pr_repository": p.pr_repository,
45                    "pr_url": p.pr_url,
46                })).collect::<Vec<_>>(),
47                "hot_files": data.hot_files.iter().map(|f| serde_json::json!({
48                    "file_path": f.file_path,
49                    "modification_count": f.modification_count,
50                    "distinct_session_count": f.distinct_session_count,
51                    "top_project": f.top_project,
52                })).collect::<Vec<_>>(),
53                "slow_projects": data.slow_projects.iter().map(|t| serde_json::json!({
54                    "project": t.project,
55                    "turn_count": t.turn_count,
56                    "avg_duration_ms": t.avg_duration_ms,
57                    "p95_duration_ms": t.p95_duration_ms,
58                })).collect::<Vec<_>>(),
59            }))?
60        );
61        return Ok(());
62    }
63
64    println!(
65        "Sessions: {}",
66        ui::fmt_count(data.summary.total_sessions as u64)
67    );
68    println!("Cost:     {}", ui::fmt_cost(data.summary.total_cost));
69    println!(
70        "Tokens:   {}",
71        ui::fmt_count(
72            (data.summary.total_input_tokens
73                + data.summary.total_output_tokens
74                + data.summary.total_cache_creation
75                + data.summary.total_cache_read) as u64
76        )
77    );
78    println!();
79
80    print_sessions(&data.recent_sessions);
81    print_prs(&data.recent_prs);
82    print_files(&data.hot_files);
83    print_slow(&data.slow_projects);
84    Ok(())
85}
86
87fn print_sessions(rows: &[claudex::index::IndexedSession]) {
88    if rows.is_empty() {
89        return;
90    }
91    println!("{}", ui::emphasis("Recent sessions"));
92    let mut table = ui::table();
93    table.set_header(ui::header([
94        "Provider", "Project", "Session", "When", "Model",
95    ]));
96    for s in rows {
97        let when = s
98            .last_timestamp_ms
99            .or(s.first_timestamp_ms)
100            .and_then(DateTime::from_timestamp_millis)
101            .map(|d| d.format("%Y-%m-%d %H:%M").to_string())
102            .unwrap_or_else(|| "-".to_string());
103        table.add_row([
104            ui::cell_provider(&s.provider),
105            ui::cell_project(&short_name(&s.project_name)),
106            ui::cell_dim(s.session_id.as_deref().unwrap_or("-")),
107            ui::cell_dim(&when),
108            ui::cell_model(s.model.as_deref().unwrap_or("-")),
109        ]);
110    }
111    println!("{table}");
112}
113
114fn print_prs(rows: &[claudex::index::PrLinkRow]) {
115    if rows.is_empty() {
116        return;
117    }
118    println!("{}", ui::emphasis("Recent PRs"));
119    let mut table = ui::table();
120    table.set_header(ui::header(["Provider", "Repo", "PR", "When"]));
121    for p in rows {
122        table.add_row([
123            ui::cell_provider(&p.provider),
124            ui::cell_project(&p.pr_repository),
125            ui::cell_dim(&format!("#{}", p.pr_number)),
126            ui::cell_dim(&p.timestamp),
127        ]);
128    }
129    println!("{table}");
130}
131
132fn print_files(rows: &[claudex::index::FileModRow]) {
133    if rows.is_empty() {
134        return;
135    }
136    println!("{}", ui::emphasis("Hot files"));
137    let mut table = ui::table();
138    table.set_header(ui::header(["File", "Edits", "Sessions"]));
139    ui::right_align(&mut table, &[1, 2]);
140    for f in rows {
141        table.add_row([
142            ui::cell_project(&short_name(&f.file_path)),
143            ui::cell_count(f.modification_count as u64),
144            ui::cell_count(f.distinct_session_count as u64),
145        ]);
146    }
147    println!("{table}");
148}
149
150fn print_slow(rows: &[claudex::index::TurnStatsRow]) {
151    if rows.is_empty() {
152        return;
153    }
154    println!("{}", ui::emphasis("Slow projects"));
155    let mut table = ui::table();
156    table.set_header(ui::header(["Project", "Turns", "Avg", "P95"]));
157    ui::right_align(&mut table, &[1, 2, 3]);
158    for t in rows {
159        table.add_row([
160            ui::cell_project(&short_name(&t.project)),
161            ui::cell_count(t.turn_count as u64),
162            ui::cell_plain(format_duration(t.avg_duration_ms.round() as u64)),
163            ui::cell_plain(format_duration(t.p95_duration_ms.round() as u64)),
164        ]);
165    }
166    println!("{table}");
167}