pub mod detection;
pub mod error;
use detection::{classify_process, ProcessRole};
use serde::{Deserialize, Serialize};
use sysinfo::System;
pub use detection::{AgentFingerprint, get_agent_db, matches_patterns, generate_session_key};
pub use error::{Error, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProcess {
pub pid: u32,
pub name: String,
pub vendor: String,
pub icon: String,
pub color: String,
pub cpu: f32,
pub mem_mb: f32,
pub uptime_secs: u64,
pub status: String,
pub role: ProcessRole,
pub confidence: u8,
pub host_app: Option<String>,
pub cwd: String,
pub project: String,
pub command: String,
pub match_reason: Vec<String>,
}
pub fn extract_project(cwd: &str) -> String {
cwd.split('/')
.rfind(|s| !s.is_empty())
.unwrap_or("-")
.to_string()
}
pub fn format_uptime(secs: u64) -> String {
let d = secs / 86400;
let h = (secs % 86400) / 3600;
let m = (secs % 3600) / 60;
let s = secs % 60;
if d > 0 {
format!("{}d {:02}:{:02}:{:02}", d, h, m, s)
} else if h > 0 {
format!("{:02}:{:02}:{:02}", h, m, s)
} else {
format!("{:02}:{:02}", m, s)
}
}
pub fn scan_agents() -> Result<Vec<AgentProcess>> {
let mut sys = System::new_all();
sys.refresh_all();
std::thread::sleep(std::time::Duration::from_millis(200));
sys.refresh_all();
let mut agents = Vec::new();
for (pid, process) in sys.processes() {
let cmd: String = process.cmd()
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(" ");
if let Some(detection) = classify_process(&cmd) {
let cwd = process.cwd()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let cpu = process.cpu_usage();
let status = if cpu > 1.0 { "ACTIVE" } else { "IDLE" }.to_string();
agents.push(AgentProcess {
pid: pid.as_u32(),
name: detection.name,
vendor: detection.vendor,
icon: detection.icon,
color: detection.color,
cpu,
mem_mb: process.memory() as f32 / 1024.0 / 1024.0,
uptime_secs: process.run_time(),
status,
role: detection.role,
confidence: detection.confidence,
host_app: detection.host_app,
project: extract_project(&cwd),
cwd,
command: cmd.chars().take(200).collect(),
match_reason: detection.match_reason,
});
}
}
agents.sort_by(|a, b| b.cpu.partial_cmp(&a.cpu).unwrap_or(std::cmp::Ordering::Equal));
Ok(agents)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStats {
pub total: usize,
pub active: usize,
pub idle: usize,
pub total_cpu: f32,
pub total_mem_mb: f32,
pub by_vendor: std::collections::HashMap<String, usize>,
}
pub fn calculate_stats(agents: &[AgentProcess]) -> AgentStats {
let mut by_vendor = std::collections::HashMap::new();
for agent in agents {
*by_vendor.entry(agent.vendor.clone()).or_insert(0) += 1;
}
AgentStats {
total: agents.len(),
active: agents.iter().filter(|a| a.status == "ACTIVE").count(),
idle: agents.iter().filter(|a| a.status == "IDLE").count(),
total_cpu: agents.iter().map(|a| a.cpu).sum(),
total_mem_mb: agents.iter().map(|a| a.mem_mb).sum(),
by_vendor,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_project() {
assert_eq!(extract_project("/Users/dev/projects/myapp"), "myapp");
assert_eq!(extract_project("/home/user/code/"), "code");
assert_eq!(extract_project(""), "-");
}
#[test]
fn test_format_uptime() {
assert_eq!(format_uptime(30), "00:30");
assert_eq!(format_uptime(3661), "01:01:01");
assert_eq!(format_uptime(90061), "1d 01:01:01");
}
#[test]
fn test_classify_claude() {
let result = classify_process("/usr/local/bin/claude --dangerously-skip-permissions");
assert!(result.is_some());
let r = result.unwrap();
assert_eq!(r.vendor, "Anthropic");
}
}