#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
mod unsupported;
#[cfg(target_os = "linux")]
use linux as platform;
#[cfg(target_os = "macos")]
use macos as platform;
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
use unsupported as platform;
use std::io;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Process {
pub ppid: u32,
pub argv: Vec<String>,
}
const AGENTS: &[&str] = &[
"claude",
"codex",
"cursor-agent",
"cn",
"copilot",
"gemini",
"openclaw",
"pi",
"openmoat",
"moat",
];
const CURSOR_HELPER: &str = ".app/contents/macos/cursor helper";
const AGENT_DIRS: &[&[&str]] = &[
&["claude", "versions"],
&["claude-code"],
&["cursor-agent"],
&["openclaw", "dist"],
&["pi-coding-agent"],
&["@continuedev", "cli"],
&["@github", "copilot"],
&["@google", "gemini-cli"],
];
const MAX_DEPTH: usize = 256;
pub(super) fn agent_ancestor() -> io::Result<Option<String>> {
platform::agent_ancestor()
}
fn walk(
mut pid: u32,
mut lookup: impl FnMut(u32) -> io::Result<Process>,
) -> io::Result<Option<String>> {
for _ in 0..MAX_DEPTH {
if pid == 0 {
return Ok(None);
}
let process = match lookup(pid) {
Ok(process) => process,
Err(_) if pid == 1 => return Ok(None),
Err(error) => return Err(error),
};
if let Some(name) = agent_name(&process.argv) {
return Ok(Some(name));
}
if pid == 1 || process.ppid == pid {
return Ok(None);
}
pid = process.ppid;
}
Err(io::Error::other(
"the chain of parent processes does not end",
))
}
fn agent_name(argv: &[String]) -> Option<String> {
let program: Vec<&str> = argv
.iter()
.map(String::as_str)
.take_while(|word| !word.starts_with('-'))
.collect();
if program
.join(" ")
.to_ascii_lowercase()
.contains(CURSOR_HELPER)
{
return Some("Cursor Helper".to_owned());
}
argv.iter().take(2).find_map(|word| {
let lower = word.trim_start_matches('-').to_ascii_lowercase();
let segments: Vec<&str> = lower.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
let base = segments.last()?;
let stem = [".exe", ".js", ".mjs", ".cjs"]
.iter()
.find_map(|ext| base.strip_suffix(ext))
.unwrap_or(base);
if let Some(name) = AGENTS.iter().find(|a| **a == stem) {
return Some((*name).to_owned());
}
let dirs = &segments[..segments.len() - 1];
AGENT_DIRS
.iter()
.find(|pattern| dirs.windows(pattern.len()).any(|w| w == **pattern))
.map(|pattern| pattern[0].to_owned())
})
}
#[cfg(test)]
mod tests;