detect_coding_agent/lib.rs
1//! # detect-coding-agent
2//!
3//! Detect whether your application is being invoked by an AI coding agent,
4//! such as **Claude Code**, **GitHub Copilot Cloud Agent**, **OpenAI Codex**,
5//! **Cursor**, **Aider**, **Gemini CLI**, and [many more](providers).
6//!
7//! ## Why?
8//!
9//! When your application knows it is running inside an automated coding agent
10//! it can:
11//!
12//! - Suggest machine-friendly interfaces (e.g. MCP) instead of interactive UIs.
13//! - Emit structured output that the agent can parse more reliably.
14//! - Skip interactive prompts and use sensible defaults instead.
15//! - Provide agent-specific hints or documentation snippets.
16//!
17//! ## Quick start
18//!
19//! ```rust
20//! use detect_coding_agent::{detect, is_agent};
21//!
22//! if is_agent() {
23//! println!("Running inside a coding agent — switching to machine-friendly output.");
24//! }
25//!
26//! if let Some(agent) = detect() {
27//! println!("Detected: {} ({})", agent.name, agent.kind);
28//! }
29//! ```
30//!
31//! ## Detection strategy
32//!
33//! Detection is based on **environment variables** only (no subprocesses are
34//! spawned, no filesystem access is performed). The first provider whose
35//! signals match the current environment is returned.
36//!
37//! Each provider has a documented source for its detection heuristic — see
38//! [`providers`] for the full list.
39//!
40//! ## Testing
41//!
42//! Because detection reads the process environment, tests should supply an
43//! explicit environment snapshot via [`detect_with_env`]. Use
44//! [`std::collections::HashMap`] to build the environment:
45//!
46//! ```rust
47//! use std::collections::HashMap;
48//! use detect_coding_agent::detect_with_env;
49//!
50//! let mut env = HashMap::new();
51//! env.insert("CLAUDECODE".to_string(), "1".to_string());
52//!
53//! let agent = detect_with_env(env).unwrap();
54//! assert_eq!(agent.id, "claude-code");
55//! ```
56
57#![forbid(unsafe_code)]
58
59mod env;
60pub mod providers;
61mod types;
62
63use std::collections::HashMap;
64
65pub use types::{AgentKind, DetectedAgent};
66
67/// Detect the AI coding agent present in the **current process environment**.
68///
69/// Returns `None` when no known agent is detected.
70///
71/// # Example
72///
73/// ```rust
74/// if let Some(agent) = detect_coding_agent::detect() {
75/// eprintln!("Running inside: {}", agent.name);
76/// }
77/// ```
78pub fn detect() -> Option<DetectedAgent> {
79 let env = env::Env::current();
80 providers::detect(&env)
81}
82
83/// Detect the AI coding agent using a **caller-supplied** environment map.
84///
85/// This is the testable variant of [`detect`]. Pass a [`HashMap<String, String>`]
86/// built from whichever key/value pairs you want to simulate.
87///
88/// # Example
89///
90/// ```rust
91/// use std::collections::HashMap;
92///
93/// let mut env = HashMap::new();
94/// env.insert("CODEX_THREAD_ID".to_string(), "thread-42".to_string());
95///
96/// let agent = detect_coding_agent::detect_with_env(env).unwrap();
97/// assert_eq!(agent.id, "codex");
98/// ```
99pub fn detect_with_env(env: HashMap<String, String>) -> Option<DetectedAgent> {
100 let env = env::Env::from_map(env);
101 providers::detect(&env)
102}
103
104/// Returns `true` when the current process is running inside an autonomous
105/// coding **agent** or a [`AgentKind::Hybrid`] environment.
106///
107/// Equivalent to `detect().map(|a| a.is_agent() || a.is_hybrid()).unwrap_or(false)`.
108pub fn is_agent() -> bool {
109 detect()
110 .map(|a| matches!(a.kind, AgentKind::Agent | AgentKind::Hybrid))
111 .unwrap_or(false)
112}
113
114/// Returns `true` when the current process is running inside an
115/// **interactive** AI-assisted coding tool or a [`AgentKind::Hybrid`] environment.
116///
117/// Equivalent to `detect().map(|a| a.is_interactive() || a.is_hybrid()).unwrap_or(false)`.
118pub fn is_interactive() -> bool {
119 detect()
120 .map(|a| matches!(a.kind, AgentKind::Interactive | AgentKind::Hybrid))
121 .unwrap_or(false)
122}
123
124/// Returns `true` when the current process is running inside a
125/// [`AgentKind::Hybrid`] environment (supports both agent and interactive use).
126pub fn is_hybrid() -> bool {
127 detect()
128 .map(|a| a.kind == AgentKind::Hybrid)
129 .unwrap_or(false)
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn env(pairs: &[(&str, &str)]) -> HashMap<String, String> {
137 pairs
138 .iter()
139 .map(|(k, v)| (k.to_string(), v.to_string()))
140 .collect()
141 }
142
143 #[test]
144 fn detect_returns_none_for_empty_env() {
145 assert!(detect_with_env(HashMap::new()).is_none());
146 }
147
148 #[test]
149 fn detect_with_env_finds_claude_code() {
150 let agent = detect_with_env(env(&[("CLAUDECODE", "1")])).unwrap();
151 assert_eq!(agent.id, "claude-code");
152 assert_eq!(agent.name, "Claude Code");
153 assert!(agent.is_agent());
154 assert!(!agent.is_interactive());
155 assert!(!agent.is_hybrid());
156 }
157
158 #[test]
159 fn detect_with_env_finds_codex() {
160 let agent = detect_with_env(env(&[("CODEX_THREAD_ID", "t-1")])).unwrap();
161 assert_eq!(agent.id, "codex");
162 }
163
164 #[test]
165 fn detect_with_env_finds_warp_as_hybrid() {
166 let agent = detect_with_env(env(&[("TERM_PROGRAM", "WarpTerminal")])).unwrap();
167 assert_eq!(agent.id, "warp");
168 assert!(agent.is_hybrid());
169 assert!(!agent.is_agent());
170 assert!(!agent.is_interactive());
171 }
172
173 #[test]
174 fn detect_with_env_finds_cursor_interactive() {
175 let agent = detect_with_env(env(&[("CURSOR_TRACE_ID", "t-1")])).unwrap();
176 assert_eq!(agent.id, "cursor");
177 assert!(agent.is_interactive());
178 }
179
180 #[test]
181 fn agent_kind_display() {
182 assert_eq!(AgentKind::Agent.to_string(), "agent");
183 assert_eq!(AgentKind::Interactive.to_string(), "interactive");
184 assert_eq!(AgentKind::Hybrid.to_string(), "hybrid");
185 }
186
187 #[test]
188 fn detected_agent_display() {
189 let agent = DetectedAgent {
190 id: "test",
191 name: "Test Agent",
192 kind: AgentKind::Agent,
193 };
194 assert_eq!(agent.to_string(), "Test Agent (agent)");
195 }
196}