Skip to main content

agentwatch_core/
lib.rs

1//! AgentWatch Core — AI Agent Detection Library
2//!
3//! This crate provides the core detection logic for identifying AI coding agents
4//! running on a system. It's used by both the AgentWatch desktop app and CLI.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use agentwatch_core::{scan_agents, AgentProcess};
10//!
11//! let agents = scan_agents().expect("failed to scan");
12//! for agent in agents {
13//!     println!("{}: {} ({})", agent.pid, agent.name, agent.vendor);
14//! }
15//! ```
16
17pub mod detection;
18pub mod error;
19
20use detection::{classify_process, ProcessRole};
21use serde::{Deserialize, Serialize};
22use sysinfo::System;
23
24// Re-exports
25pub use detection::{AgentFingerprint, get_agent_db, matches_patterns, generate_session_key};
26pub use error::{Error, Result};
27
28/// Represents a detected AI agent process
29#[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
49/// Extract project name from working directory path
50pub fn extract_project(cwd: &str) -> String {
51    cwd.split('/')
52        .rfind(|s| !s.is_empty())
53        .unwrap_or("-")
54        .to_string()
55}
56
57/// Format uptime seconds into human-readable string
58pub 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
73/// Scan the system for AI coding agents
74///
75/// Returns a vector of detected agent processes, sorted by CPU usage (descending).
76///
77/// # Errors
78///
79/// Currently infallible, but returns `Result` for API stability and future
80/// extensibility (e.g., permission errors, sandboxing restrictions).
81pub fn scan_agents() -> Result<Vec<AgentProcess>> {
82    let mut sys = System::new_all();
83    sys.refresh_all();
84    
85    // Brief pause for accurate CPU readings
86    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    // Sort by CPU usage descending
128    agents.sort_by(|a, b| b.cpu.partial_cmp(&a.cpu).unwrap_or(std::cmp::Ordering::Equal));
129    Ok(agents)
130}
131
132/// Summary statistics for detected agents
133#[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
143/// Calculate summary statistics for a list of agents
144pub 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}