use serde::Serialize; use std::collections::HashMap; use std::io::BufRead; use std::path::Path; /// Statistics derived from a Claude Code transcript JSONL file. #[derive(Debug, Default, Clone, Serialize)] pub struct TranscriptStats { pub total_tool_uses: u64, pub total_turns: u64, pub last_tool_name: Option, /// Per-tool counts sorted by count descending. pub tool_counts: Vec<(String, u64)>, } /// Cached format: "tools:N,turns:N,last:Name;ToolA=C,ToolB=C,..." impl TranscriptStats { pub fn to_cache_string(&self) -> String { let mut s = format!("tools:{},turns:{}", self.total_tool_uses, self.total_turns); if let Some(name) = &self.last_tool_name { s.push_str(&format!(",last:{name}")); } if !self.tool_counts.is_empty() { s.push(';'); let parts: Vec = self .tool_counts .iter() .map(|(name, count)| format!("{name}={count}")) .collect(); s.push_str(&parts.join(",")); } s } pub fn from_cache_string(s: &str) -> Option { let mut stats = Self::default(); // Split on ';' — first part is summary, second is per-tool counts let (summary, breakdown) = s.split_once(';').unwrap_or((s, "")); for part in summary.split(',') { if let Some((key, val)) = part.split_once(':') { match key { "tools" => stats.total_tool_uses = val.parse().unwrap_or(0), "turns" => stats.total_turns = val.parse().unwrap_or(0), "last" => { if !val.is_empty() { stats.last_tool_name = Some(val.to_string()); } } _ => {} } } } if !breakdown.is_empty() { for part in breakdown.split(',') { if let Some((name, count_str)) = part.split_once('=') { let count: u64 = count_str.parse().unwrap_or(0); if count > 0 { stats.tool_counts.push((name.to_string(), count)); } } } } Some(stats) } } /// Parse a Claude Code transcript JSONL file and extract tool use and turn counts. /// `skip_lines` skips that many lines from the start (used after /clear to ignore /// pre-clear entries in the same transcript file). /// /// Transcript format (one JSON object per line): /// - `{"type": "user", ...}` — a user turn /// - `{"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Read"}, ...]}}` — tool uses pub fn parse_transcript(path: &Path, skip_lines: usize) -> Option { let file = std::fs::File::open(path).ok()?; let reader = std::io::BufReader::new(file); let mut stats = TranscriptStats::default(); let mut counts: HashMap = HashMap::new(); for line in reader.lines().skip(skip_lines) { let line = match line { Ok(l) => l, Err(_) => continue, }; if line.is_empty() { continue; } let v: serde_json::Value = match serde_json::from_str(&line) { Ok(v) => v, Err(_) => continue, }; let msg_type = v.get("type").and_then(|t| t.as_str()).unwrap_or(""); match msg_type { "user" => { stats.total_turns += 1; } "assistant" => { if let Some(content) = v .get("message") .and_then(|m| m.get("content")) .and_then(|c| c.as_array()) { for block in content { if block.get("type").and_then(|t| t.as_str()) == Some("tool_use") { stats.total_tool_uses += 1; if let Some(name) = block.get("name").and_then(|n| n.as_str()) { stats.last_tool_name = Some(name.to_string()); *counts.entry(name.to_string()).or_insert(0) += 1; } } } } } _ => {} } } // Sort by count descending let mut sorted: Vec<(String, u64)> = counts.into_iter().collect(); sorted.sort_by(|a, b| b.1.cmp(&a.1)); stats.tool_counts = sorted; Some(stats) }