1pub mod detection;
18pub mod error;
19
20use detection::{classify_process, ProcessRole};
21use serde::{Deserialize, Serialize};
22use sysinfo::System;
23
24pub use detection::{AgentFingerprint, get_agent_db, matches_patterns, generate_session_key};
26pub use error::{Error, Result};
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct AgentProcess {
31 pub pid: u32,
32 pub name: String,
33 pub vendor: String,
34 pub icon: String,
35 pub color: String,
36 pub cpu: f32,
37 pub mem_mb: f32,
38 pub uptime_secs: u64,
39 pub status: String,
40 pub role: ProcessRole,
41 pub confidence: u8,
42 pub host_app: Option<String>,
43 pub cwd: String,
44 pub project: String,
45 pub command: String,
46 pub match_reason: Vec<String>,
47}
48
49pub fn extract_project(cwd: &str) -> String {
51 cwd.split('/')
52 .rfind(|s| !s.is_empty())
53 .unwrap_or("-")
54 .to_string()
55}
56
57pub fn format_uptime(secs: u64) -> String {
59 let d = secs / 86400;
60 let h = (secs % 86400) / 3600;
61 let m = (secs % 3600) / 60;
62 let s = secs % 60;
63
64 if d > 0 {
65 format!("{}d {:02}:{:02}:{:02}", d, h, m, s)
66 } else if h > 0 {
67 format!("{:02}:{:02}:{:02}", h, m, s)
68 } else {
69 format!("{:02}:{:02}", m, s)
70 }
71}
72
73pub fn scan_agents() -> Result<Vec<AgentProcess>> {
82 let mut sys = System::new_all();
83 sys.refresh_all();
84
85 std::thread::sleep(std::time::Duration::from_millis(200));
87 sys.refresh_all();
88
89 let mut agents = Vec::new();
90
91 for (pid, process) in sys.processes() {
92 let cmd: String = process.cmd()
93 .iter()
94 .map(|s| s.to_string_lossy().to_string())
95 .collect::<Vec<_>>()
96 .join(" ");
97
98 if let Some(detection) = classify_process(&cmd) {
99 let cwd = process.cwd()
100 .map(|p| p.to_string_lossy().to_string())
101 .unwrap_or_default();
102
103 let cpu = process.cpu_usage();
104 let status = if cpu > 1.0 { "ACTIVE" } else { "IDLE" }.to_string();
105
106 agents.push(AgentProcess {
107 pid: pid.as_u32(),
108 name: detection.name,
109 vendor: detection.vendor,
110 icon: detection.icon,
111 color: detection.color,
112 cpu,
113 mem_mb: process.memory() as f32 / 1024.0 / 1024.0,
114 uptime_secs: process.run_time(),
115 status,
116 role: detection.role,
117 confidence: detection.confidence,
118 host_app: detection.host_app,
119 project: extract_project(&cwd),
120 cwd,
121 command: cmd.chars().take(200).collect(),
122 match_reason: detection.match_reason,
123 });
124 }
125 }
126
127 agents.sort_by(|a, b| b.cpu.partial_cmp(&a.cpu).unwrap_or(std::cmp::Ordering::Equal));
129 Ok(agents)
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct AgentStats {
135 pub total: usize,
136 pub active: usize,
137 pub idle: usize,
138 pub total_cpu: f32,
139 pub total_mem_mb: f32,
140 pub by_vendor: std::collections::HashMap<String, usize>,
141}
142
143pub fn calculate_stats(agents: &[AgentProcess]) -> AgentStats {
145 let mut by_vendor = std::collections::HashMap::new();
146
147 for agent in agents {
148 *by_vendor.entry(agent.vendor.clone()).or_insert(0) += 1;
149 }
150
151 AgentStats {
152 total: agents.len(),
153 active: agents.iter().filter(|a| a.status == "ACTIVE").count(),
154 idle: agents.iter().filter(|a| a.status == "IDLE").count(),
155 total_cpu: agents.iter().map(|a| a.cpu).sum(),
156 total_mem_mb: agents.iter().map(|a| a.mem_mb).sum(),
157 by_vendor,
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_extract_project() {
167 assert_eq!(extract_project("/Users/dev/projects/myapp"), "myapp");
168 assert_eq!(extract_project("/home/user/code/"), "code");
169 assert_eq!(extract_project(""), "-");
170 }
171
172 #[test]
173 fn test_format_uptime() {
174 assert_eq!(format_uptime(30), "00:30");
175 assert_eq!(format_uptime(3661), "01:01:01");
176 assert_eq!(format_uptime(90061), "1d 01:01:01");
177 }
178
179 #[test]
180 fn test_classify_claude() {
181 let result = classify_process("/usr/local/bin/claude --dangerously-skip-permissions");
182 assert!(result.is_some());
183 let r = result.unwrap();
184 assert_eq!(r.vendor, "Anthropic");
185 }
186}