1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use anyhow::Result;
5use chrono::DateTime;
6
7use crate::cli::ResolvedFilter;
8use crate::ui;
9use claudex::index::IndexStore;
10use claudex::parser::parse_session;
11use claudex::providers::enabled_default;
12use claudex::store::{SessionStore, decode_project_name, display_project_name, short_name};
13
14pub fn run(
15 project: Option<&str>,
16 per_session: bool,
17 limit: usize,
18 json: bool,
19 no_index: bool,
20 filter: &ResolvedFilter,
21) -> Result<()> {
22 if !no_index && let Ok(()) = run_indexed(project, per_session, limit, json, filter) {
23 return Ok(());
24 }
25 run_from_files(project, per_session, limit, json, filter)
26}
27
28fn run_indexed(
29 project: Option<&str>,
30 per_session: bool,
31 limit: usize,
32 json: bool,
33 filter: &ResolvedFilter,
34) -> Result<()> {
35 let providers = enabled_default()?;
36 let mut idx = IndexStore::open()?;
37 idx.ensure_fresh(&providers)?;
38
39 if per_session {
40 let rows = idx.query_tools_per_session(project, filter, limit)?;
41
42 if json {
43 let output: Vec<_> = rows
44 .iter()
45 .map(|r| {
46 let date = r
47 .first_timestamp_ms
48 .and_then(DateTime::from_timestamp_millis)
49 .map(|d| d.to_rfc3339());
50 serde_json::json!({
51 "project": r.project,
52 "session_id": r.session_id,
53 "date": date,
54 "tools": r.tools,
55 })
56 })
57 .collect();
58 println!("{}", serde_json::to_string_pretty(&output)?);
59 return Ok(());
60 }
61
62 let mut table = ui::table();
63 table.set_header(ui::header([
64 "Project",
65 "Session",
66 "Date",
67 "Top Tools",
68 "Total Calls",
69 ]));
70 ui::right_align(&mut table, &[4]);
71
72 for r in &rows {
73 let sid: String = r
74 .session_id
75 .as_deref()
76 .unwrap_or("-")
77 .chars()
78 .take(8)
79 .collect();
80 let total: i64 = r.tools.values().sum();
81 let mut sorted: Vec<_> = r.tools.iter().collect();
82 sorted.sort_by(|a, b| b.1.cmp(a.1));
83 let date = r
84 .first_timestamp_ms
85 .and_then(DateTime::from_timestamp_millis)
86 .map(|d| d.format("%Y-%m-%d").to_string())
87 .unwrap_or_else(|| "-".to_string());
88 let top: Vec<_> = sorted
89 .iter()
90 .take(3)
91 .map(|(k, v)| format!("{}({})", k, ui::fmt_count(**v as u64)))
92 .collect();
93 table.add_row([
94 ui::cell_project(&short_name(&r.project)),
95 ui::cell_dim(&sid),
96 ui::cell_dim(&date),
97 ui::cell_plain(top.join(", ")),
98 ui::cell_count(total as u64),
99 ]);
100 }
101 println!("{table}");
102 return Ok(());
103 }
104
105 let rows = idx.query_tools_aggregate(project, filter, limit)?;
106
107 if json {
108 let output: Vec<_> = rows
109 .iter()
110 .map(|r| serde_json::json!({"tool": r.tool_name, "count": r.count}))
111 .collect();
112 println!("{}", serde_json::to_string_pretty(&output)?);
113 return Ok(());
114 }
115
116 let mut table = ui::table();
117 table.set_header(ui::header(["Tool", "Calls"]));
118 ui::right_align(&mut table, &[1]);
119 for r in &rows {
120 table.add_row([ui::cell_tool(&r.tool_name), ui::cell_count(r.count as u64)]);
121 }
122 println!("{table}");
123 Ok(())
124}
125
126fn run_from_files(
127 project: Option<&str>,
128 per_session: bool,
129 limit: usize,
130 json: bool,
131 filter: &ResolvedFilter,
132) -> Result<()> {
133 filter.ensure_no_index_supported()?;
134
135 let store = SessionStore::new()?;
136 let files = store.all_session_files(project)?;
137 if per_session {
138 run_per_session(files, limit, json, filter)
139 } else {
140 run_aggregate(files, limit, json, filter)
141 }
142}
143
144fn run_aggregate(
145 files: Vec<(String, PathBuf)>,
146 limit: usize,
147 json: bool,
148 filter: &ResolvedFilter,
149) -> Result<()> {
150 let mut counts: HashMap<String, u64> = HashMap::new();
151
152 for (_, path) in &files {
153 let stats = match parse_session(path) {
154 Ok(s) => s,
155 Err(_) => continue,
156 };
157 if !filter.matches("claude", &stats, false) {
158 continue;
159 }
160 for name in stats.tool_names {
161 *counts.entry(name).or_insert(0) += 1;
162 }
163 }
164
165 let mut rows: Vec<(String, u64)> = counts.into_iter().collect();
166 rows.sort_by_key(|r| std::cmp::Reverse(r.1));
167 rows.truncate(limit);
168
169 if json {
170 let output: Vec<_> = rows
171 .iter()
172 .map(|(name, count)| serde_json::json!({"tool": name, "count": count}))
173 .collect();
174 println!("{}", serde_json::to_string_pretty(&output)?);
175 return Ok(());
176 }
177
178 let mut table = ui::table();
179 table.set_header(ui::header(["Tool", "Calls"]));
180 ui::right_align(&mut table, &[1]);
181 for (name, count) in &rows {
182 table.add_row([ui::cell_tool(name), ui::cell_count(*count)]);
183 }
184 println!("{table}");
185 Ok(())
186}
187
188fn run_per_session(
189 files: Vec<(String, PathBuf)>,
190 limit: usize,
191 json: bool,
192 filter: &ResolvedFilter,
193) -> Result<()> {
194 let mut rows = Vec::new();
195 for (project_raw, path) in &files {
196 let stats = match parse_session(path) {
197 Ok(s) => s,
198 Err(_) => continue,
199 };
200 if stats.tool_names.is_empty() {
201 continue;
202 }
203 if !filter.matches("claude", &stats, false) {
204 continue;
205 }
206 let mut counts: HashMap<String, u64> = HashMap::new();
207 for name in &stats.tool_names {
208 *counts.entry(name.clone()).or_insert(0) += 1;
209 }
210 rows.push((
211 display_project_name(&decode_project_name(project_raw)),
212 stats.session_id,
213 stats.first_timestamp,
214 counts,
215 ));
216 }
217 rows.sort_by(|a, b| {
218 b.2.cmp(&a.2)
219 .then_with(|| a.0.cmp(&b.0))
220 .then_with(|| a.1.cmp(&b.1))
221 });
222 rows.truncate(limit);
223
224 if json {
225 let output: Vec<_> = rows
226 .iter()
227 .map(|(project, session_id, date, counts)| {
228 serde_json::json!({
229 "project": project,
230 "session_id": session_id,
231 "date": date.map(|d| d.to_rfc3339()),
232 "tools": counts,
233 })
234 })
235 .collect();
236 println!("{}", serde_json::to_string_pretty(&output)?);
237 return Ok(());
238 }
239
240 let mut table = ui::table();
241 table.set_header(ui::header([
242 "Project",
243 "Session",
244 "Date",
245 "Top Tools",
246 "Total Calls",
247 ]));
248 ui::right_align(&mut table, &[4]);
249
250 for (project, session_id, date, counts) in &rows {
251 let sid: String = session_id
252 .as_deref()
253 .unwrap_or("-")
254 .chars()
255 .take(8)
256 .collect();
257 let date = date
258 .map(|d| d.format("%Y-%m-%d").to_string())
259 .unwrap_or_else(|| "-".to_string());
260 let total: u64 = counts.values().sum();
261 let mut sorted: Vec<_> = counts.iter().collect();
262 sorted.sort_by(|a, b| b.1.cmp(a.1));
263 let top: Vec<_> = sorted
264 .iter()
265 .take(3)
266 .map(|(k, v)| format!("{}({})", k, ui::fmt_count(**v)))
267 .collect();
268 table.add_row([
269 ui::cell_project(&short_name(project)),
270 ui::cell_dim(&sid),
271 ui::cell_dim(&date),
272 ui::cell_plain(top.join(", ")),
273 ui::cell_count(total),
274 ]);
275 }
276 println!("{table}");
277 Ok(())
278}