use std::cell::RefCell;
use std::fmt;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
const CACHE_TTL: Duration = Duration::from_secs(300);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Agent {
ClaudeCode,
AugmentCode,
Aider,
Continue,
CodexCli,
GeminiCli,
CopilotCli,
Custom(String),
Unknown,
}
impl Agent {
#[must_use]
pub fn config_key(&self) -> &str {
match self {
Self::ClaudeCode => "claude-code",
Self::AugmentCode => "augment-code",
Self::Aider => "aider",
Self::Continue => "continue",
Self::CodexCli => "codex-cli",
Self::GeminiCli => "gemini-cli",
Self::CopilotCli => "copilot-cli",
Self::Custom(name) => name,
Self::Unknown => "unknown",
}
}
#[must_use]
pub const fn is_known(&self) -> bool {
matches!(
self,
Self::ClaudeCode
| Self::AugmentCode
| Self::Aider
| Self::Continue
| Self::CodexCli
| Self::GeminiCli
| Self::CopilotCli
)
}
#[must_use]
pub const fn is_explicit(&self) -> bool {
matches!(self, Self::Custom(_))
}
#[must_use]
pub fn from_name(name: &str) -> Self {
let normalized = name.to_lowercase().replace(['-', '_'], "");
match normalized.as_str() {
"claudecode" => Self::ClaudeCode,
"augmentcode" | "auggie" | "augment" => Self::AugmentCode,
"aider" => Self::Aider,
"continue" => Self::Continue,
"codexcli" | "codex" => Self::CodexCli,
"geminicli" | "gemini" => Self::GeminiCli,
"copilotcli" | "copilot" => Self::CopilotCli,
"unknown" => Self::Unknown,
_ => Self::Custom(name.to_string()),
}
}
}
impl fmt::Display for Agent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ClaudeCode => write!(f, "Claude Code"),
Self::AugmentCode => write!(f, "Augment Code"),
Self::Aider => write!(f, "Aider"),
Self::Continue => write!(f, "Continue"),
Self::CodexCli => write!(f, "Codex CLI"),
Self::GeminiCli => write!(f, "Gemini CLI"),
Self::CopilotCli => write!(f, "GitHub Copilot CLI"),
Self::Custom(name) => write!(f, "{name}"),
Self::Unknown => write!(f, "Unknown"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectionResult {
pub agent: Agent,
pub method: DetectionMethod,
pub matched_value: Option<String>,
}
impl DetectionResult {
#[must_use]
pub const fn new(agent: Agent, method: DetectionMethod, matched_value: Option<String>) -> Self {
Self {
agent,
method,
matched_value,
}
}
#[must_use]
pub const fn unknown() -> Self {
Self {
agent: Agent::Unknown,
method: DetectionMethod::None,
matched_value: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectionMethod {
Environment,
Explicit,
Process,
None,
}
impl fmt::Display for DetectionMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Environment => write!(f, "environment variable"),
Self::Explicit => write!(f, "explicit flag"),
Self::Process => write!(f, "parent process"),
Self::None => write!(f, "not detected"),
}
}
}
#[derive(Debug)]
struct CachedAgent {
result: DetectionResult,
cached_at: Instant,
}
impl CachedAgent {
fn is_valid(&self) -> bool {
self.cached_at.elapsed() < CACHE_TTL
}
}
thread_local! {
static AGENT_CACHE: RefCell<Option<CachedAgent>> = const { RefCell::new(None) };
}
#[must_use]
pub fn detect_agent() -> Agent {
detect_agent_with_details().agent
}
#[must_use]
pub fn detect_agent_with_details() -> DetectionResult {
let cached = AGENT_CACHE.with(|cache| {
let borrow = cache.borrow();
if let Some(ref entry) = *borrow {
if entry.is_valid() {
return Some(entry.result.clone());
}
}
None
});
if let Some(result) = cached {
return result;
}
let result = perform_detection();
AGENT_CACHE.with(|cache| {
*cache.borrow_mut() = Some(CachedAgent {
result: result.clone(),
cached_at: Instant::now(),
});
});
result
}
fn perform_detection() -> DetectionResult {
if let Some(result) = detect_from_environment() {
return result;
}
if let Some(result) = detect_from_parent_process() {
return result;
}
DetectionResult::unknown()
}
fn detect_from_environment() -> Option<DetectionResult> {
if std::env::var("CLAUDE_CODE").is_ok() {
return Some(DetectionResult::new(
Agent::ClaudeCode,
DetectionMethod::Environment,
Some("CLAUDE_CODE".to_string()),
));
}
if std::env::var("CLAUDE_SESSION_ID").is_ok() {
return Some(DetectionResult::new(
Agent::ClaudeCode,
DetectionMethod::Environment,
Some("CLAUDE_SESSION_ID".to_string()),
));
}
if std::env::var("AUGMENT_AGENT").is_ok() {
return Some(DetectionResult::new(
Agent::AugmentCode,
DetectionMethod::Environment,
Some("AUGMENT_AGENT".to_string()),
));
}
if std::env::var("AUGMENT_CONVERSATION_ID").is_ok() {
return Some(DetectionResult::new(
Agent::AugmentCode,
DetectionMethod::Environment,
Some("AUGMENT_CONVERSATION_ID".to_string()),
));
}
if std::env::var("AIDER_SESSION").is_ok() {
return Some(DetectionResult::new(
Agent::Aider,
DetectionMethod::Environment,
Some("AIDER_SESSION".to_string()),
));
}
if std::env::var("CONTINUE_SESSION_ID").is_ok() {
return Some(DetectionResult::new(
Agent::Continue,
DetectionMethod::Environment,
Some("CONTINUE_SESSION_ID".to_string()),
));
}
if std::env::var("CODEX_CLI").is_ok() {
return Some(DetectionResult::new(
Agent::CodexCli,
DetectionMethod::Environment,
Some("CODEX_CLI".to_string()),
));
}
if std::env::var("GEMINI_CLI").is_ok() {
return Some(DetectionResult::new(
Agent::GeminiCli,
DetectionMethod::Environment,
Some("GEMINI_CLI".to_string()),
));
}
if std::env::var("COPILOT_CLI").is_ok() {
return Some(DetectionResult::new(
Agent::CopilotCli,
DetectionMethod::Environment,
Some("COPILOT_CLI".to_string()),
));
}
if std::env::var("COPILOT_AGENT_START_TIME_SEC").is_ok() {
return Some(DetectionResult::new(
Agent::CopilotCli,
DetectionMethod::Environment,
Some("COPILOT_AGENT_START_TIME_SEC".to_string()),
));
}
None
}
#[cfg(target_os = "linux")]
fn detect_from_parent_process() -> Option<DetectionResult> {
use std::fs;
use std::os::unix::process::parent_id;
let ppid = parent_id();
let comm_path = format!("/proc/{ppid}/comm");
let process_name = fs::read_to_string(&comm_path).ok()?.trim().to_lowercase();
if process_name.contains("claude") {
return Some(DetectionResult::new(
Agent::ClaudeCode,
DetectionMethod::Process,
Some(process_name),
));
}
if process_name.contains("aider") {
return Some(DetectionResult::new(
Agent::Aider,
DetectionMethod::Process,
Some(process_name),
));
}
if process_name.contains("codex") {
return Some(DetectionResult::new(
Agent::CodexCli,
DetectionMethod::Process,
Some(process_name),
));
}
if process_name.contains("gemini") {
return Some(DetectionResult::new(
Agent::GeminiCli,
DetectionMethod::Process,
Some(process_name),
));
}
if process_name.contains("copilot") {
return Some(DetectionResult::new(
Agent::CopilotCli,
DetectionMethod::Process,
Some(process_name),
));
}
None
}
#[cfg(not(target_os = "linux"))]
fn detect_from_parent_process() -> Option<DetectionResult> {
None
}
#[must_use]
pub fn from_explicit(name: &str) -> DetectionResult {
DetectionResult::new(
Agent::from_name(name),
DetectionMethod::Explicit,
Some(name.to_string()),
)
}
pub fn clear_cache() {
AGENT_CACHE.with(|cache| {
*cache.borrow_mut() = None;
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_config_keys() {
assert_eq!(Agent::ClaudeCode.config_key(), "claude-code");
assert_eq!(Agent::AugmentCode.config_key(), "augment-code");
assert_eq!(Agent::Aider.config_key(), "aider");
assert_eq!(Agent::Continue.config_key(), "continue");
assert_eq!(Agent::CodexCli.config_key(), "codex-cli");
assert_eq!(Agent::GeminiCli.config_key(), "gemini-cli");
assert_eq!(Agent::CopilotCli.config_key(), "copilot-cli");
assert_eq!(Agent::Unknown.config_key(), "unknown");
assert_eq!(
Agent::Custom("my-agent".to_string()).config_key(),
"my-agent"
);
}
#[test]
fn test_agent_from_name() {
assert_eq!(Agent::from_name("claude-code"), Agent::ClaudeCode);
assert_eq!(Agent::from_name("augment-code"), Agent::AugmentCode);
assert_eq!(Agent::from_name("aider"), Agent::Aider);
assert_eq!(Agent::from_name("continue"), Agent::Continue);
assert_eq!(Agent::from_name("codex-cli"), Agent::CodexCli);
assert_eq!(Agent::from_name("gemini-cli"), Agent::GeminiCli);
assert_eq!(Agent::from_name("unknown"), Agent::Unknown);
assert_eq!(Agent::from_name("Claude-Code"), Agent::ClaudeCode);
assert_eq!(Agent::from_name("CLAUDE_CODE"), Agent::ClaudeCode);
assert_eq!(Agent::from_name("claudecode"), Agent::ClaudeCode);
assert_eq!(Agent::from_name("augmentcode"), Agent::AugmentCode);
assert_eq!(Agent::from_name("auggie"), Agent::AugmentCode);
assert_eq!(Agent::from_name("augment"), Agent::AugmentCode);
assert_eq!(Agent::from_name("codex"), Agent::CodexCli);
assert_eq!(Agent::from_name("gemini"), Agent::GeminiCli);
assert_eq!(Agent::from_name("copilot"), Agent::CopilotCli);
assert_eq!(Agent::from_name("copilotcli"), Agent::CopilotCli);
assert_eq!(Agent::from_name("copilot-cli"), Agent::CopilotCli);
assert_eq!(
Agent::from_name("my-custom-agent"),
Agent::Custom("my-custom-agent".to_string())
);
}
#[test]
fn test_agent_display() {
assert_eq!(format!("{}", Agent::ClaudeCode), "Claude Code");
assert_eq!(format!("{}", Agent::AugmentCode), "Augment Code");
assert_eq!(format!("{}", Agent::Aider), "Aider");
assert_eq!(format!("{}", Agent::Continue), "Continue");
assert_eq!(format!("{}", Agent::CodexCli), "Codex CLI");
assert_eq!(format!("{}", Agent::GeminiCli), "Gemini CLI");
assert_eq!(format!("{}", Agent::CopilotCli), "GitHub Copilot CLI");
assert_eq!(format!("{}", Agent::Unknown), "Unknown");
assert_eq!(
format!("{}", Agent::Custom("MyAgent".to_string())),
"MyAgent"
);
}
#[test]
fn test_agent_is_known() {
assert!(Agent::ClaudeCode.is_known());
assert!(Agent::AugmentCode.is_known());
assert!(Agent::Aider.is_known());
assert!(Agent::CopilotCli.is_known());
assert!(!Agent::Unknown.is_known());
assert!(!Agent::Custom("x".to_string()).is_known());
}
#[test]
fn test_detection_method_display() {
assert_eq!(
format!("{}", DetectionMethod::Environment),
"environment variable"
);
assert_eq!(format!("{}", DetectionMethod::Explicit), "explicit flag");
assert_eq!(format!("{}", DetectionMethod::Process), "parent process");
assert_eq!(format!("{}", DetectionMethod::None), "not detected");
}
#[test]
fn test_from_explicit() {
let result = from_explicit("claude-code");
assert_eq!(result.agent, Agent::ClaudeCode);
assert_eq!(result.method, DetectionMethod::Explicit);
assert_eq!(result.matched_value, Some("claude-code".to_string()));
}
#[test]
fn test_cache_clear() {
clear_cache();
let _ = detect_agent();
clear_cache();
}
}
#[cfg(test)]
mod env_tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
const AGENT_ENV_VARS: &[&str] = &[
"CLAUDE_CODE",
"CLAUDE_SESSION_ID",
"AUGMENT_AGENT",
"AUGMENT_CONVERSATION_ID",
"AIDER_SESSION",
"CONTINUE_SESSION_ID",
"CODEX_CLI",
"GEMINI_CLI",
"COPILOT_CLI",
"COPILOT_AGENT_START_TIME_SEC",
];
fn with_env_var<F, R>(key: &str, value: &str, f: F) -> R
where
F: FnOnce() -> R,
{
let _lock = ENV_LOCK.lock().unwrap();
clear_cache();
let saved: Vec<_> = AGENT_ENV_VARS
.iter()
.map(|&k| (k, std::env::var(k).ok()))
.collect();
unsafe {
for &k in AGENT_ENV_VARS {
std::env::remove_var(k);
}
std::env::set_var(key, value);
}
let result = f();
unsafe {
std::env::remove_var(key);
for (k, v) in saved {
if let Some(val) = v {
std::env::set_var(k, val);
}
}
}
clear_cache();
result
}
#[test]
fn test_detect_claude_code_env() {
with_env_var("CLAUDE_CODE", "1", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::ClaudeCode);
assert_eq!(result.method, DetectionMethod::Environment);
assert_eq!(result.matched_value, Some("CLAUDE_CODE".to_string()));
});
}
#[test]
fn test_detect_claude_session_id_env() {
with_env_var("CLAUDE_SESSION_ID", "abc123", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::ClaudeCode);
assert_eq!(result.method, DetectionMethod::Environment);
assert_eq!(result.matched_value, Some("CLAUDE_SESSION_ID".to_string()));
});
}
#[test]
fn test_detect_augment_agent_env() {
with_env_var("AUGMENT_AGENT", "1", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::AugmentCode);
assert_eq!(result.method, DetectionMethod::Environment);
assert_eq!(result.matched_value, Some("AUGMENT_AGENT".to_string()));
});
}
#[test]
fn test_detect_augment_conversation_id_env() {
with_env_var("AUGMENT_CONVERSATION_ID", "conv123", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::AugmentCode);
assert_eq!(result.method, DetectionMethod::Environment);
assert_eq!(
result.matched_value,
Some("AUGMENT_CONVERSATION_ID".to_string())
);
});
}
#[test]
fn test_detect_aider_env() {
with_env_var("AIDER_SESSION", "1", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::Aider);
assert_eq!(result.method, DetectionMethod::Environment);
});
}
#[test]
fn test_detect_continue_env() {
with_env_var("CONTINUE_SESSION_ID", "session123", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::Continue);
assert_eq!(result.method, DetectionMethod::Environment);
});
}
#[test]
fn test_detect_codex_cli_env() {
with_env_var("CODEX_CLI", "1", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::CodexCli);
assert_eq!(result.method, DetectionMethod::Environment);
});
}
#[test]
fn test_detect_gemini_cli_env() {
with_env_var("GEMINI_CLI", "1", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::GeminiCli);
assert_eq!(result.method, DetectionMethod::Environment);
});
}
#[test]
fn test_detect_copilot_cli_env() {
with_env_var("COPILOT_CLI", "1", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::CopilotCli);
assert_eq!(result.method, DetectionMethod::Environment);
assert_eq!(result.matched_value, Some("COPILOT_CLI".to_string()));
});
}
#[test]
fn test_detect_copilot_agent_start_time_env() {
with_env_var("COPILOT_AGENT_START_TIME_SEC", "1709573241", || {
let result = detect_agent_with_details();
assert_eq!(result.agent, Agent::CopilotCli);
assert_eq!(result.method, DetectionMethod::Environment);
assert_eq!(
result.matched_value,
Some("COPILOT_AGENT_START_TIME_SEC".to_string())
);
});
}
#[test]
fn test_detect_unknown_no_env() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cache();
unsafe {
std::env::remove_var("CLAUDE_CODE");
std::env::remove_var("CLAUDE_SESSION_ID");
std::env::remove_var("AUGMENT_AGENT");
std::env::remove_var("AUGMENT_CONVERSATION_ID");
std::env::remove_var("AIDER_SESSION");
std::env::remove_var("CONTINUE_SESSION_ID");
std::env::remove_var("CODEX_CLI");
std::env::remove_var("GEMINI_CLI");
std::env::remove_var("COPILOT_CLI");
std::env::remove_var("COPILOT_AGENT_START_TIME_SEC");
}
let result = detect_agent_with_details();
assert!(
result.method == DetectionMethod::None || result.method == DetectionMethod::Process
);
}
}