#[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",
"openclaw",
"openmoat",
"moat",
];
const AGENT_DIRS: &[&[&str]] = &[
&["claude", "versions"],
&["claude-code"],
&["cursor-agent"],
&["openclaw", "dist"],
];
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> {
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;