Skip to content

Commit 0521821

Browse files
committed
fix(cli): match the GitHub CLI's coding agent detection table
1 parent 9df26f2 commit 0521821

2 files changed

Lines changed: 60 additions & 52 deletions

File tree

packages/sim-cli/src/telemetry/coding-agent.test.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,35 @@ describe('detectCodingAgent', () => {
1212
[{ CLAUDECODE: '1', CLAUDE_CODE_IS_COWORK: '1' }, 'cowork'],
1313
[{ CODEX_THREAD_ID: 'thr_1' }, 'codex'],
1414
[{ CODEX_SANDBOX: 'seatbelt' }, 'codex'],
15+
[{ CODEX_CI: '1' }, 'codex'],
1516
[{ GEMINI_CLI: '1' }, 'gemini-cli'],
16-
[{ CURSOR_AGENT: '1' }, 'cursor'],
17+
[{ COPILOT_CLI: '1' }, 'copilot-cli'],
18+
[{ OPENCODE: '1' }, 'opencode'],
19+
[{ ANTIGRAVITY_AGENT: '1' }, 'antigravity'],
20+
[{ AUGMENT_AGENT: '1' }, 'augment-cli'],
1721
[{ CURSOR_TRACE_ID: 'abc' }, 'cursor'],
18-
[{ CURSOR_EXTENSION_HOST_ROLE: 'agent-exec' }, 'cursor'],
19-
[{ OPENCODE: '1', AGENT: '1' }, 'opencode'],
20-
[{ CLINE_ACTIVE: 'true' }, 'cline'],
21-
[{ OZ_RUN_ID: 'run_1' }, 'warp'],
22-
[{ PI_CODING_AGENT: 'true' }, 'pi'],
22+
[{ CURSOR_AGENT: '1' }, 'cursor-cli'],
23+
[{ CURSOR_EXTENSION_HOST_ROLE: 'agent-exec' }, 'cursor-cli'],
2324
])('recognises %o as %s', (env, expected) => {
2425
expect(detectCodingAgent(env)).toBe(expected)
2526
})
2627

2728
it('names Amp rather than the Claude Code marker it also sets', () => {
2829
expect(detectCodingAgent({ AGENT: 'amp', CLAUDECODE: '1' })).toBe('amp')
29-
expect(detectCodingAgent({ AMP_CURRENT_THREAD_ID: 'T-1', CLAUDECODE: '1' })).toBe('amp')
30+
})
31+
32+
it('names the Cursor IDE over the Cursor CLI signal', () => {
33+
expect(detectCodingAgent({ CURSOR_TRACE_ID: 'abc', CURSOR_AGENT: '1' })).toBe('cursor')
34+
})
35+
36+
it('reads the declaration Claude Code sets on its shells as claude-code', () => {
37+
expect(
38+
detectCodingAgent({
39+
AI_AGENT: 'claude-code_2-1-270_agent',
40+
CLAUDECODE: '1',
41+
CLAUDE_CODE_ENTRYPOINT: 'cli',
42+
})
43+
).toBe('claude-code')
3044
})
3145

3246
it('lets an agent declare its own name over every vendor marker', () => {
@@ -52,10 +66,17 @@ describe('detectCodingAgent', () => {
5266
expect(detectCodingAgent({ AI_AGENT: 'x'.repeat(65) })).toBeUndefined()
5367
})
5468

55-
it('ignores markers that only mean an agent is installed', () => {
56-
expect(detectCodingAgent({ REPL_ID: 'abc', GOOSE_PROVIDER: 'x', AIDER_API_KEY: 'k' })).toBe(
57-
undefined
58-
)
69+
it('ignores signals that describe where a person works rather than an agent driving it', () => {
70+
expect(
71+
detectCodingAgent({
72+
REPL_ID: 'abc',
73+
GOOSE_PROVIDER: 'x',
74+
TERM_PROGRAM: 'kiro',
75+
PATH: '/home/me/.pi/agent/bin:/usr/bin',
76+
AGENT: 'goose',
77+
OPENCODE_CLIENT: 'vscode',
78+
})
79+
).toBeUndefined()
5980
})
6081

6182
it('ignores a cursor role that is not the agent executor', () => {

packages/sim-cli/src/telemetry/coding-agent.ts

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@
33
*
44
* Agents mark the shells they spawn with an environment variable, and the CLI
55
* reports that mark so usage driven by an agent can be told apart from a person
6-
* at a terminal. The checks, their order, and the names follow the GitHub CLI
7-
* (`internal/agents/detect.go`), which is the most complete verified table:
8-
* generic conventions first, then vendor markers, with the more specific
9-
* marker ahead of a broader one it implies (Amp sets `CLAUDECODE` too; Cowork
10-
* is Claude Code plus its own flag).
6+
* at a terminal. The checks, their order, and the names follow the GitHub CLI's
7+
* `internal/agents/detect.go` exactly: the generic `AI_AGENT` declaration
8+
* first, then vendor markers, with a more specific agent ahead of a broader
9+
* marker it also sets (Amp and Cowork both set `CLAUDECODE`).
1110
*
12-
* Only markers an agent sets on the shells it drives are consulted. Variables
13-
* that merely mean an agent is installed or configured — `REPL_ID`,
14-
* `GOOSE_PROVIDER`, `AIDER_*`, `COPILOT_*` — are deliberately absent, because
15-
* they would attribute a person's own command to an agent.
11+
* Four of that table's signals are deliberately left out, each one the GitHub
12+
* CLI itself marks as low confidence: `REPL_ID` (present in every Replit
13+
* environment), `GOOSE_PROVIDER` (Goose is merely configured),
14+
* `TERM_PROGRAM=kiro` (Kiro's terminal, which a person uses too), and a
15+
* `.pi/agent` entry on `PATH`. Each describes where a person is working rather
16+
* than an agent driving the command, so reporting it would attribute that
17+
* person's own commands to an agent.
1618
*/
1719

18-
/** The value an agent may declare itself with under the generic conventions. */
20+
/** The value an agent may declare itself with under `AI_AGENT`. */
1921
const AGENT_NAME_PATTERN = /^[a-z0-9_-]+$/i
2022
const MAX_AGENT_NAME_LENGTH = 64
2123

@@ -29,34 +31,25 @@ const anyOf =
2931
(env: NodeJS.ProcessEnv) =>
3032
variables.some((variable) => Boolean(env[variable]))
3133

32-
/** Vendor markers, most specific first. */
34+
/** Vendor markers, in the GitHub CLI's order. */
3335
const AGENT_MARKERS: readonly AgentMarker[] = [
34-
{ name: 'amp', matches: (env) => env.AGENT === 'amp' || Boolean(env.AMP_CURRENT_THREAD_ID) },
35-
{
36-
name: 'codex',
37-
matches: anyOf(
38-
'CODEX_THREAD_ID',
39-
'CODEX_SANDBOX',
40-
'CODEX_CI',
41-
'CODEX_SANDBOX_NETWORK_DISABLED'
42-
),
43-
},
36+
{ name: 'amp', matches: (env) => env.AGENT === 'amp' },
37+
/** Set by Codex on the commands it runs (`codex-rs/core`: `spawn.rs`, `exec_env.rs`, `unified_exec`). */
38+
{ name: 'codex', matches: anyOf('CODEX_SANDBOX', 'CODEX_CI', 'CODEX_THREAD_ID') },
4439
{ name: 'gemini-cli', matches: anyOf('GEMINI_CLI') },
40+
{ name: 'copilot-cli', matches: anyOf('COPILOT_CLI') },
41+
/** Not `OPENCODE_CALLER` or `OPENCODE_CLIENT`, which name what launched OpenCode. */
4542
{ name: 'opencode', matches: anyOf('OPENCODE') },
4643
{ name: 'antigravity', matches: anyOf('ANTIGRAVITY_AGENT') },
47-
{ name: 'augment', matches: anyOf('AUGMENT_AGENT') },
48-
{ name: 'cline', matches: anyOf('CLINE_ACTIVE') },
44+
{ name: 'augment-cli', matches: anyOf('AUGMENT_AGENT') },
4945
{ name: 'cowork', matches: anyOf('CLAUDE_CODE_IS_COWORK') },
46+
/** `CLAUDECODE` is documented in Claude Code's environment variable reference. */
5047
{ name: 'claude-code', matches: anyOf('CLAUDECODE', 'CLAUDE_CODE') },
48+
{ name: 'cursor', matches: anyOf('CURSOR_TRACE_ID') },
5149
{
52-
name: 'cursor',
53-
matches: (env) =>
54-
anyOf('CURSOR_AGENT', 'CURSOR_TRACE_ID')(env) ||
55-
env.CURSOR_EXTENSION_HOST_ROLE === 'agent-exec',
50+
name: 'cursor-cli',
51+
matches: (env) => Boolean(env.CURSOR_AGENT) || env.CURSOR_EXTENSION_HOST_ROLE === 'agent-exec',
5652
},
57-
{ name: 'warp', matches: anyOf('OZ_RUN_ID') },
58-
{ name: 'pi', matches: anyOf('PI_CODING_AGENT') },
59-
{ name: 'crush', matches: anyOf('CRUSH') },
6053
]
6154

6255
/**
@@ -91,17 +84,11 @@ export const NO_CODING_AGENT = 'none'
9184
/**
9285
* The agent driving this shell, or `undefined` for a person at a terminal.
9386
*
94-
* `AI_AGENT` and `AGENT` are the two generic conventions agents have converged
95-
* on for naming themselves and win over vendor markers when set. `AGENT` is
96-
* consulted only when it carries a name: OpenCode sets it to `1`, which names
97-
* nothing, and its own marker handles it.
87+
* `AI_AGENT` is the generic convention agents use to name themselves, and wins
88+
* over every vendor marker when it holds a well-formed name.
9889
*/
9990
export function detectCodingAgent(env: NodeJS.ProcessEnv = process.env): string | undefined {
100-
const declared = declaredAgentName(env.AI_AGENT)
101-
if (declared) return declared
102-
103-
const generic = declaredAgentName(env.AGENT)
104-
if (generic && generic !== '1') return generic
105-
106-
return AGENT_MARKERS.find((marker) => marker.matches(env))?.name
91+
return (
92+
declaredAgentName(env.AI_AGENT) ?? AGENT_MARKERS.find((marker) => marker.matches(env))?.name
93+
)
10794
}

0 commit comments

Comments
 (0)