Skip to main content

claudex_cli/commands/
tools.rs

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}