pub mod analyzer;
pub mod models;
pub mod parser;
pub mod patterns;
pub mod reporter;
pub mod tool_analyzer;
#[cfg(feature = "terraphim")]
pub mod kg;
pub mod connectors;
pub use analyzer::{Analyzer, SummaryStats};
pub use models::{
AgentAttribution, AgentInvocation, AgentStatistics, AgentToolCorrelation, AnalyzerConfig,
CollaborationPattern, FileOperation, SessionAnalysis, ToolAnalysis, ToolCategory, ToolChain,
ToolInvocation, ToolStatistics, get_agent_category, normalize_agent_name,
};
pub use parser::{SessionParser, TimelineEvent, TimelineEventType};
pub use patterns::{
AhoCorasickMatcher, PatternMatcher, ToolMatch, ToolMetadata, ToolPattern, create_matcher,
load_patterns,
};
pub use reporter::Reporter;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DEFAULT_SESSION_DIR: &str = ".claude/projects";
pub const COMMON_AGENT_TYPES: &[&str] = &[
"architect",
"developer",
"backend-architect",
"frontend-developer",
"rust-performance-expert",
"rust-code-reviewer",
"debugger",
"technical-writer",
"test-writer-fixer",
"rapid-prototyper",
"devops-automator",
"overseer",
"ai-engineer",
"general-purpose",
];
pub mod utils {
use crate::models::*;
use std::path::Path;
#[must_use]
pub fn get_default_session_dir() -> Option<std::path::PathBuf> {
home::home_dir().map(|home| home.join(crate::DEFAULT_SESSION_DIR))
}
pub fn is_session_file<P: AsRef<Path>>(path: P) -> bool {
let path = path.as_ref();
path.extension() == Some("jsonl".as_ref())
&& path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.len() == 42) }
pub fn extract_project_name<P: AsRef<Path>>(session_path: P) -> Option<String> {
let path = session_path.as_ref();
path.parent()
.and_then(|parent| parent.file_name())
.and_then(|name| name.to_str())
.map(|name| {
name.replace("-home-", "/home/").replace('-', "/")
})
}
#[must_use]
pub fn filter_by_project<'a>(
analyses: &'a [SessionAnalysis],
project_filter: &str,
) -> Vec<&'a SessionAnalysis> {
analyses
.iter()
.filter(|analysis| analysis.project_path.contains(project_filter))
.collect()
}
#[must_use]
pub fn get_unique_agents(analyses: &[SessionAnalysis]) -> Vec<String> {
let mut agents: std::collections::HashSet<String> = std::collections::HashSet::new();
for analysis in analyses {
for agent in &analysis.agents {
agents.insert(agent.agent_type.clone());
}
}
let mut sorted: Vec<String> = agents.into_iter().collect();
sorted.sort();
sorted
}
#[must_use]
pub fn total_session_time(analyses: &[SessionAnalysis]) -> u64 {
analyses.iter().map(|a| a.duration_ms).sum()
}
#[must_use]
pub fn most_productive_session(analyses: &[SessionAnalysis]) -> Option<&SessionAnalysis> {
analyses.iter().max_by_key(|a| a.file_to_agents.len())
}
#[must_use]
pub fn sessions_with_agent<'a>(
analyses: &'a [SessionAnalysis],
agent_type: &str,
) -> Vec<&'a SessionAnalysis> {
analyses
.iter()
.filter(|analysis| {
analysis
.agents
.iter()
.any(|agent| agent.agent_type == agent_type)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::utils::*;
#[test]
fn test_is_session_file() {
assert!(is_session_file(
"b325985c-5c1c-48f1-97e2-e3185bb55886.jsonl"
));
assert!(!is_session_file("regular-file.txt"));
assert!(!is_session_file("short.jsonl"));
}
#[test]
fn test_extract_project_name() {
let path = "/home/alex/.claude/projects/-home-alex-projects-zestic-at-charm/session.jsonl";
let project = extract_project_name(path);
assert_eq!(
project,
Some("/home/alex/projects/zestic/at/charm".to_string())
);
}
#[test]
fn test_get_default_session_dir() {
let dir = get_default_session_dir();
assert!(dir.is_some());
let path = dir.unwrap();
assert!(path.to_string_lossy().contains(".claude"));
assert!(path.to_string_lossy().contains("projects"));
}
}