claudex_cli/commands/
activity.rs1use 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}