Skip to main content

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}