terraphim_session_analyzer/
lib.rs

1//! Claude Log Analyzer
2//!
3//! A high-performance Rust tool for analyzing Claude Code session logs
4//! to identify which AI agents were used to build specific documents.
5//!
6//! ## Features
7//!
8//! - Parse JSONL session logs from `$HOME/.claude/projects/`
9//! - Identify Task tool invocations with agent types
10//! - Track file operations and attribute them to agents
11//! - Generate rich terminal output with colored tables
12//! - Export to JSON, CSV, and Markdown formats
13//! - Timeline visualization and collaboration pattern detection
14//! - Real-time session monitoring
15//!
16//! ## Example Usage
17//!
18//! ```rust
19//! use terraphim_session_analyzer::{Analyzer, Reporter};
20//!
21//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! // Analyze sessions from default location
23//! let analyzer = Analyzer::from_default_location()?;
24//! let analyses = analyzer.analyze(None)?;
25//!
26//! // Generate terminal report
27//! let reporter = Reporter::new();
28//! reporter.print_terminal(&analyses);
29//!
30//! // Export to JSON
31//! let json = reporter.to_json(&analyses)?;
32//! println!("{}", json);
33//! # Ok(())
34//! # }
35//! ```
36
37pub mod analyzer;
38pub mod models;
39pub mod parser;
40pub mod patterns;
41pub mod reporter;
42pub mod tool_analyzer;
43
44#[cfg(feature = "terraphim")]
45pub mod kg;
46
47pub mod connectors;
48
49// Re-export main types for convenience
50pub use analyzer::{Analyzer, SummaryStats};
51pub use models::{
52    get_agent_category, normalize_agent_name, AgentAttribution, AgentInvocation, AgentStatistics,
53    AgentToolCorrelation, AnalyzerConfig, CollaborationPattern, FileOperation, SessionAnalysis,
54    ToolAnalysis, ToolCategory, ToolChain, ToolInvocation, ToolStatistics,
55};
56pub use parser::{SessionParser, TimelineEvent, TimelineEventType};
57pub use patterns::{
58    create_matcher, load_patterns, AhoCorasickMatcher, PatternMatcher, ToolMatch, ToolMetadata,
59    ToolPattern,
60};
61pub use reporter::Reporter;
62
63/// Version information
64pub const VERSION: &str = env!("CARGO_PKG_VERSION");
65
66/// Default Claude session directory relative to home
67pub const DEFAULT_SESSION_DIR: &str = ".claude/projects";
68
69/// Common agent types used in Claude Code
70pub const COMMON_AGENT_TYPES: &[&str] = &[
71    "architect",
72    "developer",
73    "backend-architect",
74    "frontend-developer",
75    "rust-performance-expert",
76    "rust-code-reviewer",
77    "debugger",
78    "technical-writer",
79    "test-writer-fixer",
80    "rapid-prototyper",
81    "devops-automator",
82    "overseer",
83    "ai-engineer",
84    "general-purpose",
85];
86
87/// Utility functions for common operations
88pub mod utils {
89    use crate::models::*;
90    use std::path::Path;
91
92    /// Get the default Claude session directory
93    #[must_use]
94    pub fn get_default_session_dir() -> Option<std::path::PathBuf> {
95        home::home_dir().map(|home| home.join(crate::DEFAULT_SESSION_DIR))
96    }
97
98    /// Check if a path looks like a Claude session file
99    pub fn is_session_file<P: AsRef<Path>>(path: P) -> bool {
100        let path = path.as_ref();
101        path.extension() == Some("jsonl".as_ref())
102            && path
103                .file_name()
104                .and_then(|name| name.to_str())
105                .is_some_and(|name| name.len() == 42) // UUID (36) + .jsonl (6) = 42
106    }
107
108    /// Extract project name from session path
109    pub fn extract_project_name<P: AsRef<Path>>(session_path: P) -> Option<String> {
110        let path = session_path.as_ref();
111        path.parent()
112            .and_then(|parent| parent.file_name())
113            .and_then(|name| name.to_str())
114            .map(|name| {
115                // Convert encoded path back to readable format
116                name.replace("-home-", "/home/").replace('-', "/")
117            })
118    }
119
120    /// Filter analyses by project name
121    #[must_use]
122    pub fn filter_by_project<'a>(
123        analyses: &'a [SessionAnalysis],
124        project_filter: &str,
125    ) -> Vec<&'a SessionAnalysis> {
126        analyses
127            .iter()
128            .filter(|analysis| analysis.project_path.contains(project_filter))
129            .collect()
130    }
131
132    /// Get unique agent types across all analyses
133    #[must_use]
134    pub fn get_unique_agents(analyses: &[SessionAnalysis]) -> Vec<String> {
135        let mut agents: std::collections::HashSet<String> = std::collections::HashSet::new();
136
137        for analysis in analyses {
138            for agent in &analysis.agents {
139                agents.insert(agent.agent_type.clone());
140            }
141        }
142
143        let mut sorted: Vec<String> = agents.into_iter().collect();
144        sorted.sort();
145        sorted
146    }
147
148    /// Calculate total session time across analyses
149    #[must_use]
150    pub fn total_session_time(analyses: &[SessionAnalysis]) -> u64 {
151        analyses.iter().map(|a| a.duration_ms).sum()
152    }
153
154    /// Find the most productive session (most files modified)
155    #[must_use]
156    pub fn most_productive_session(analyses: &[SessionAnalysis]) -> Option<&SessionAnalysis> {
157        analyses.iter().max_by_key(|a| a.file_to_agents.len())
158    }
159
160    /// Find sessions that used a specific agent
161    #[must_use]
162    pub fn sessions_with_agent<'a>(
163        analyses: &'a [SessionAnalysis],
164        agent_type: &str,
165    ) -> Vec<&'a SessionAnalysis> {
166        analyses
167            .iter()
168            .filter(|analysis| {
169                analysis
170                    .agents
171                    .iter()
172                    .any(|agent| agent.agent_type == agent_type)
173            })
174            .collect()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use crate::utils::*;
181
182    #[test]
183    fn test_is_session_file() {
184        assert!(is_session_file(
185            "b325985c-5c1c-48f1-97e2-e3185bb55886.jsonl"
186        ));
187        assert!(!is_session_file("regular-file.txt"));
188        assert!(!is_session_file("short.jsonl"));
189    }
190
191    #[test]
192    fn test_extract_project_name() {
193        let path = "/home/alex/.claude/projects/-home-alex-projects-zestic-at-charm/session.jsonl";
194        let project = extract_project_name(path);
195        assert_eq!(
196            project,
197            Some("/home/alex/projects/zestic/at/charm".to_string())
198        );
199    }
200
201    #[test]
202    fn test_get_default_session_dir() {
203        let dir = get_default_session_dir();
204        assert!(dir.is_some());
205
206        let path = dir.unwrap();
207        assert!(path.to_string_lossy().contains(".claude"));
208        assert!(path.to_string_lossy().contains("projects"));
209    }
210}