Skip to content

Commit ee5b472

Browse files
feat(cli): expand AI agent detection (#1261)
* feat(cli): expand AI agent detection to cover Cline, Codex CLI, Gemini CLI, and OpenCode Add detection for four new AI coding agents that were missing compared to similar tooling (e.g. Stripe CLI). Also add CURSOR_AGENT as a fallback env var for Cursor detection alongside the existing CURSOR_TRACE_ID. New operators and their env vars: - Cline: CLINE_ACTIVE - Codex CLI: CODEX_SANDBOX, CODEX_THREAD_ID - Gemini CLI: GEMINI_CLI - OpenCode: OPENCODE - Cursor (additional): CURSOR_AGENT * fix(cli): move VS Code check after all agent checks Cline (and potentially other future agents) are VS Code extensions, so TERM_PROGRAM=vscode is always set when they're active. The previous ordering would misclassify Cline as 'vscode' (interactive) instead of 'cline' (agent). Moving the VS Code check after all agent-specific checks ensures extensions are correctly detected as agents.
1 parent e1616a0 commit ee5b472

2 files changed

Lines changed: 67 additions & 5 deletions

File tree

packages/cli/src/helpers/__tests__/cli-mode.spec.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { beforeEach, describe, expect, it } from 'vitest'
22
import { detectOperator, detectCliMode } from '../cli-mode'
33

44
const operatorEnvVars = [
5-
'CLAUDECODE', 'CURSOR_TRACE_ID', 'TERM_PROGRAM', 'GITHUB_COPILOT',
6-
'AIDER', 'WINDSURF', 'CODEIUM_ENV', 'GITHUB_ACTIONS', 'GITLAB_CI', 'CI',
5+
'CLAUDECODE', 'CURSOR_TRACE_ID', 'CURSOR_AGENT', 'TERM_PROGRAM', 'GITHUB_COPILOT',
6+
'AIDER', 'WINDSURF', 'CODEIUM_ENV', 'CLINE_ACTIVE', 'CODEX_SANDBOX', 'CODEX_THREAD_ID',
7+
'GEMINI_CLI', 'OPENCODE', 'GITHUB_ACTIONS', 'GITLAB_CI', 'CI',
78
]
89

910
describe('detectOperator', () => {
@@ -20,11 +21,16 @@ describe('detectOperator', () => {
2021
expect(detectOperator()).toBe('claude-code')
2122
})
2223

23-
it('detects Cursor', () => {
24+
it('detects Cursor via CURSOR_TRACE_ID', () => {
2425
process.env.CURSOR_TRACE_ID = 'some-trace-id'
2526
expect(detectOperator()).toBe('cursor')
2627
})
2728

29+
it('detects Cursor via CURSOR_AGENT', () => {
30+
process.env.CURSOR_AGENT = '1'
31+
expect(detectOperator()).toBe('cursor')
32+
})
33+
2834
it('detects VS Code', () => {
2935
process.env.TERM_PROGRAM = 'vscode'
3036
expect(detectOperator()).toBe('vscode')
@@ -65,11 +71,42 @@ describe('detectOperator', () => {
6571
expect(detectOperator()).toBe('gitlab-ci')
6672
})
6773

74+
it('detects Cline', () => {
75+
process.env.CLINE_ACTIVE = '1'
76+
expect(detectOperator()).toBe('cline')
77+
})
78+
79+
it('detects Codex CLI via CODEX_SANDBOX', () => {
80+
process.env.CODEX_SANDBOX = '1'
81+
expect(detectOperator()).toBe('codex-cli')
82+
})
83+
84+
it('detects Codex CLI via CODEX_THREAD_ID', () => {
85+
process.env.CODEX_THREAD_ID = 'thread-123'
86+
expect(detectOperator()).toBe('codex-cli')
87+
})
88+
89+
it('detects Gemini CLI', () => {
90+
process.env.GEMINI_CLI = '1'
91+
expect(detectOperator()).toBe('gemini-cli')
92+
})
93+
94+
it('detects OpenCode', () => {
95+
process.env.OPENCODE = '1'
96+
expect(detectOperator()).toBe('opencode')
97+
})
98+
6899
it('detects generic CI', () => {
69100
process.env.CI = 'true'
70101
expect(detectOperator()).toBe('ci')
71102
})
72103

104+
it('prioritizes Cline over VS Code (Cline is a VS Code extension)', () => {
105+
process.env.CLINE_ACTIVE = '1'
106+
process.env.TERM_PROGRAM = 'vscode'
107+
expect(detectOperator()).toBe('cline')
108+
})
109+
73110
it('prioritizes Claude Code over CI', () => {
74111
process.env.CLAUDECODE = '1'
75112
process.env.CI = 'true'
@@ -118,6 +155,26 @@ describe('detectCliMode', () => {
118155
expect(detectCliMode()).toBe('agent')
119156
})
120157

158+
it('returns "agent" when operator is cline', () => {
159+
process.env.CLINE_ACTIVE = '1'
160+
expect(detectCliMode()).toBe('agent')
161+
})
162+
163+
it('returns "agent" when operator is codex-cli', () => {
164+
process.env.CODEX_SANDBOX = '1'
165+
expect(detectCliMode()).toBe('agent')
166+
})
167+
168+
it('returns "agent" when operator is gemini-cli', () => {
169+
process.env.GEMINI_CLI = '1'
170+
expect(detectCliMode()).toBe('agent')
171+
})
172+
173+
it('returns "agent" when operator is opencode', () => {
174+
process.env.OPENCODE = '1'
175+
expect(detectCliMode()).toBe('agent')
176+
})
177+
121178
it('returns "ci" when operator is github-actions', () => {
122179
process.env.GITHUB_ACTIONS = 'true'
123180
expect(detectCliMode()).toBe('ci')

packages/cli/src/helpers/cli-mode.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const VALID_CLI_MODES: ReadonlySet<string> = new Set(['interactive', 'ci', 'agen
44

55
const AGENT_OPERATORS: ReadonlySet<string> = new Set([
66
'claude-code', 'cursor', 'windsurf', 'aider', 'github-copilot',
7+
'cline', 'codex-cli', 'gemini-cli', 'opencode',
78
])
89

910
const CI_OPERATORS: ReadonlySet<string> = new Set([
@@ -12,11 +13,15 @@ const CI_OPERATORS: ReadonlySet<string> = new Set([
1213

1314
export function detectOperator (): string {
1415
if (process.env.CLAUDECODE) return 'claude-code'
15-
if (process.env.CURSOR_TRACE_ID) return 'cursor'
16-
if (process.env.TERM_PROGRAM === 'vscode') return 'vscode'
16+
if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_AGENT) return 'cursor'
1717
if (process.env.GITHUB_COPILOT) return 'github-copilot'
1818
if (process.env.AIDER) return 'aider'
1919
if (process.env.WINDSURF || process.env.CODEIUM_ENV) return 'windsurf'
20+
if (process.env.CLINE_ACTIVE) return 'cline'
21+
if (process.env.CODEX_SANDBOX || process.env.CODEX_THREAD_ID) return 'codex-cli'
22+
if (process.env.GEMINI_CLI) return 'gemini-cli'
23+
if (process.env.OPENCODE) return 'opencode'
24+
if (process.env.TERM_PROGRAM === 'vscode') return 'vscode'
2025
if (process.env.GITHUB_ACTIONS) return 'github-actions'
2126
if (process.env.GITLAB_CI) return 'gitlab-ci'
2227
if (process.env.CI) return 'ci'

0 commit comments

Comments
 (0)