Skip to content

Commit 884d6b7

Browse files
simonfaltumclaude
andauthored
Add agent detection for Cline, Codex, Antigravity and OpenCode (#4496)
## Summary - Add agent detection for three new coding agents: Cline (`CLINE_ACTIVE`), Codex (`CODEX_CI`), OpenCode (`OPENCODE`) and Antigravity - Refactor detection logic from per-agent if-blocks to a table-driven `knownAgents` slice, so adding a new agent only requires a new constant and one table entry - Refactor tests to be table-driven to match ## Test plan - [x] `go test ./libs/agent/ -v` — all detection tests pass - [x] `go test ./cmd/root/ -run TestAgent -v` — all user agent tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 21214fe commit 884d6b7

File tree

4 files changed

+93
-116
lines changed

4 files changed

+93
-116
lines changed

acceptance/test.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ Env.PYTHONUNBUFFERED = "1"
1818
Env.PYTHONUTF8 = "1"
1919

2020
# Clear agent env vars to prevent them from affecting test output
21+
Env.ANTIGRAVITY_AGENT = ""
2122
Env.CLAUDECODE = ""
22-
Env.GEMINI_CLI = ""
23+
Env.CLINE_ACTIVE = ""
24+
Env.CODEX_CI = ""
2325
Env.CURSOR_AGENT = ""
26+
Env.GEMINI_CLI = ""
27+
Env.OPENCODE = ""
2428

2529
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"]
2630
EnvMatrixExclude.noplantf = ["DATABRICKS_BUNDLE_ENGINE=terraform", "READPLAN=1"]

cmd/root/user_agent_agent_test.go

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,24 @@ import (
99
"github.com/stretchr/testify/assert"
1010
)
1111

12-
func TestAgentClaudeCode(t *testing.T) {
13-
ctx := context.Background()
14-
ctx = agent.Mock(ctx, agent.ClaudeCode)
15-
16-
ctx = withAgentInUserAgent(ctx)
17-
assert.Contains(t, useragent.FromContext(ctx), "agent/claude-code")
18-
}
19-
20-
func TestAgentGeminiCLI(t *testing.T) {
21-
ctx := context.Background()
22-
ctx = agent.Mock(ctx, agent.GeminiCLI)
23-
24-
ctx = withAgentInUserAgent(ctx)
25-
assert.Contains(t, useragent.FromContext(ctx), "agent/gemini-cli")
26-
}
27-
28-
func TestAgentCursor(t *testing.T) {
29-
ctx := context.Background()
30-
ctx = agent.Mock(ctx, agent.Cursor)
31-
32-
ctx = withAgentInUserAgent(ctx)
33-
assert.Contains(t, useragent.FromContext(ctx), "agent/cursor")
12+
func TestAgentInUserAgent(t *testing.T) {
13+
for _, product := range []string{
14+
agent.Antigravity,
15+
agent.ClaudeCode,
16+
agent.Cline,
17+
agent.Codex,
18+
agent.Cursor,
19+
agent.GeminiCLI,
20+
agent.OpenCode,
21+
} {
22+
t.Run(product, func(t *testing.T) {
23+
ctx := context.Background()
24+
ctx = agent.Mock(ctx, product)
25+
26+
ctx = withAgentInUserAgent(ctx)
27+
assert.Contains(t, useragent.FromContext(ctx), "agent/"+product)
28+
})
29+
}
3430
}
3531

3632
func TestAgentNotSet(t *testing.T) {

libs/agent/agent.go

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,53 @@ import (
88

99
// Product name constants
1010
const (
11-
ClaudeCode = "claude-code"
12-
GeminiCLI = "gemini-cli"
13-
Cursor = "cursor"
11+
Antigravity = "antigravity"
12+
ClaudeCode = "claude-code"
13+
Cline = "cline"
14+
Codex = "codex"
15+
Cursor = "cursor"
16+
GeminiCLI = "gemini-cli"
17+
OpenCode = "opencode"
1418
)
1519

16-
// Environment variable constants
17-
const (
18-
claudeCodeEnvVar = "CLAUDECODE"
19-
geminiCliEnvVar = "GEMINI_CLI"
20-
cursorAgentEnvVar = "CURSOR_AGENT"
21-
)
20+
// knownAgents maps environment variables to product names.
21+
// Adding a new agent only requires a new entry here and a new constant above.
22+
//
23+
// References for each environment variable:
24+
// - ANTIGRAVITY_AGENT: Closed source. Verified locally that Google Antigravity sets this variable.
25+
// - CLAUDECODE: https://github.com/anthropics/claude-code (open source npm package, sets CLAUDECODE=1)
26+
// - CLINE_ACTIVE: https://github.com/cline/cline (shipped in v3.24.0, see also https://github.com/cline/cline/discussions/5366)
27+
// - CODEX_CI: https://github.com/openai/codex/blob/main/codex-rs/core/src/unified_exec/process_manager.rs (part of UNIFIED_EXEC_ENV array)
28+
// - CURSOR_AGENT: Closed source. Referenced in https://gist.github.com/johnlindquist/9a90c5f1aedef0477c60d0de4171da3f
29+
// - GEMINI_CLI: https://google-gemini.github.io/gemini-cli/docs/tools/shell.html ("sets the GEMINI_CLI=1 environment variable")
30+
// - OPENCODE: https://github.com/opencode-ai/opencode (open source, sets OPENCODE=1)
31+
var knownAgents = []struct {
32+
envVar string
33+
product string
34+
}{
35+
{"ANTIGRAVITY_AGENT", Antigravity},
36+
{"CLAUDECODE", ClaudeCode},
37+
{"CLINE_ACTIVE", Cline},
38+
{"CODEX_CI", Codex},
39+
{"CURSOR_AGENT", Cursor},
40+
{"GEMINI_CLI", GeminiCLI},
41+
{"OPENCODE", OpenCode},
42+
}
2243

23-
// key is a package-local type for context keys
24-
type key int
44+
// productKeyType is a package-local context key with zero size.
45+
type productKeyType struct{}
2546

26-
const (
27-
productKey = key(1)
28-
)
47+
var productKey productKeyType
2948

3049
// detect performs the actual detection logic.
3150
// Returns product name string or empty string if detection is ambiguous.
3251
// Only returns a product if exactly one agent is detected.
3352
func detect(ctx context.Context) string {
3453
var detected []string
35-
36-
if env.Get(ctx, claudeCodeEnvVar) != "" {
37-
detected = append(detected, ClaudeCode)
38-
}
39-
40-
if env.Get(ctx, geminiCliEnvVar) != "" {
41-
detected = append(detected, GeminiCLI)
42-
}
43-
44-
if env.Get(ctx, cursorAgentEnvVar) != "" {
45-
detected = append(detected, Cursor)
54+
for _, a := range knownAgents {
55+
if env.Get(ctx, a.envVar) != "" {
56+
detected = append(detected, a.product)
57+
}
4658
}
4759

4860
// Only return a product if exactly one agent is detected

libs/agent/agent_test.go

Lines changed: 33 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,96 +9,61 @@ import (
99
"github.com/stretchr/testify/require"
1010
)
1111

12-
func TestDetect(t *testing.T) {
13-
ctx := context.Background()
14-
// Clear other agent env vars to ensure clean test environment
15-
ctx = env.Set(ctx, geminiCliEnvVar, "")
16-
ctx = env.Set(ctx, cursorAgentEnvVar, "")
17-
ctx = env.Set(ctx, claudeCodeEnvVar, "1")
18-
19-
ctx = Detect(ctx)
20-
21-
assert.Equal(t, ClaudeCode, Product(ctx))
12+
func clearAllAgentEnvVars(ctx context.Context) context.Context {
13+
for _, a := range knownAgents {
14+
ctx = env.Set(ctx, a.envVar, "")
15+
}
16+
return ctx
2217
}
2318

24-
func TestProductCalledBeforeDetect(t *testing.T) {
25-
ctx := context.Background()
19+
func TestDetectEachAgent(t *testing.T) {
20+
for _, a := range knownAgents {
21+
t.Run(a.product, func(t *testing.T) {
22+
ctx := clearAllAgentEnvVars(context.Background())
23+
ctx = env.Set(ctx, a.envVar, "1")
2624

27-
require.Panics(t, func() {
28-
Product(ctx)
29-
})
25+
assert.Equal(t, a.product, detect(ctx))
26+
})
27+
}
3028
}
3129

32-
func TestMock(t *testing.T) {
33-
ctx := context.Background()
34-
ctx = Mock(ctx, "test-agent")
30+
func TestDetectViaContext(t *testing.T) {
31+
ctx := clearAllAgentEnvVars(context.Background())
32+
ctx = env.Set(ctx, knownAgents[0].envVar, "1")
3533

36-
assert.Equal(t, "test-agent", Product(ctx))
34+
ctx = Detect(ctx)
35+
36+
assert.Equal(t, knownAgents[0].product, Product(ctx))
3737
}
3838

3939
func TestDetectNoAgent(t *testing.T) {
40-
ctx := context.Background()
41-
ctx = env.Set(ctx, claudeCodeEnvVar, "")
42-
ctx = env.Set(ctx, geminiCliEnvVar, "")
43-
ctx = env.Set(ctx, cursorAgentEnvVar, "")
40+
ctx := clearAllAgentEnvVars(context.Background())
4441

4542
ctx = Detect(ctx)
4643

4744
assert.Equal(t, "", Product(ctx))
4845
}
4946

50-
func TestDetectClaudeCode(t *testing.T) {
51-
ctx := context.Background()
52-
// Clear other agent env vars to ensure clean test environment
53-
ctx = env.Set(ctx, geminiCliEnvVar, "")
54-
ctx = env.Set(ctx, cursorAgentEnvVar, "")
55-
ctx = env.Set(ctx, claudeCodeEnvVar, "1")
56-
57-
result := detect(ctx)
58-
assert.Equal(t, ClaudeCode, result)
59-
}
60-
61-
func TestDetectGeminiCLI(t *testing.T) {
62-
ctx := context.Background()
63-
// Clear other agent env vars to ensure clean test environment
64-
ctx = env.Set(ctx, claudeCodeEnvVar, "")
65-
ctx = env.Set(ctx, cursorAgentEnvVar, "")
66-
ctx = env.Set(ctx, geminiCliEnvVar, "1")
47+
func TestDetectMultipleAgents(t *testing.T) {
48+
ctx := clearAllAgentEnvVars(context.Background())
49+
for _, a := range knownAgents {
50+
ctx = env.Set(ctx, a.envVar, "1")
51+
}
6752

68-
result := detect(ctx)
69-
assert.Equal(t, GeminiCLI, result)
53+
assert.Equal(t, "", detect(ctx))
7054
}
7155

72-
func TestDetectCursor(t *testing.T) {
56+
func TestProductCalledBeforeDetect(t *testing.T) {
7357
ctx := context.Background()
74-
// Clear other agent env vars to ensure clean test environment
75-
ctx = env.Set(ctx, claudeCodeEnvVar, "")
76-
ctx = env.Set(ctx, geminiCliEnvVar, "")
77-
ctx = env.Set(ctx, cursorAgentEnvVar, "1")
78-
79-
result := detect(ctx)
80-
assert.Equal(t, Cursor, result)
81-
}
8258

83-
func TestDetectMultipleAgents(t *testing.T) {
84-
ctx := context.Background()
85-
// Clear all agent env vars first
86-
ctx = env.Set(ctx, cursorAgentEnvVar, "")
87-
// If multiple agents are detected, return empty string
88-
ctx = env.Set(ctx, claudeCodeEnvVar, "1")
89-
ctx = env.Set(ctx, geminiCliEnvVar, "1")
90-
91-
result := detect(ctx)
92-
assert.Equal(t, "", result)
59+
require.Panics(t, func() {
60+
Product(ctx)
61+
})
9362
}
9463

95-
func TestDetectMultipleAgentsAllThree(t *testing.T) {
64+
func TestMock(t *testing.T) {
9665
ctx := context.Background()
97-
// If all three agents are detected, return empty string
98-
ctx = env.Set(ctx, claudeCodeEnvVar, "1")
99-
ctx = env.Set(ctx, geminiCliEnvVar, "1")
100-
ctx = env.Set(ctx, cursorAgentEnvVar, "1")
66+
ctx = Mock(ctx, "test-agent")
10167

102-
result := detect(ctx)
103-
assert.Equal(t, "", result)
68+
assert.Equal(t, "test-agent", Product(ctx))
10469
}

0 commit comments

Comments
 (0)