-
-
Notifications
You must be signed in to change notification settings - Fork 9.9k
Maintenance: Use std-env for AI agent detection in telemetry #34114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,95 +1,65 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { detectAgent } from './detect-agent'; | ||
|
|
||
| describe('detectAgent', () => { | ||
| it('detects amp via AGENT=amp (highest precedence)', () => { | ||
| expect( | ||
| detectAgent({ | ||
| stdoutIsTTY: true, | ||
| env: { | ||
| AGENT: 'amp', | ||
| CLAUDECODE: '1', | ||
| GEMINI_CLI: '1', | ||
| CODEX_SANDBOX: '1', | ||
| CURSOR_AGENT: '1', | ||
| }, | ||
| }) | ||
| ).toEqual({ name: 'amp' }); | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
|
|
||
| expect( | ||
| detectAgent({ | ||
| stdoutIsTTY: true, | ||
| env: { | ||
| CLAUDECODE: '1', | ||
| GEMINI_CLI: '1', | ||
| CODEX_SANDBOX: '1', | ||
| CURSOR_AGENT: '1', | ||
| AGENT: 'something', | ||
| }, | ||
| }) | ||
| ).toEqual({ name: 'claude-code' }); | ||
| it('detects claude via CLAUDECODE', () => { | ||
| vi.stubEnv('CLAUDECODE', '1'); | ||
| expect(detectAgent()).toEqual({ name: 'claude' }); | ||
| }); | ||
|
|
||
| it('detects Gemini CLI via GEMINI_CLI', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { GEMINI_CLI: '1' } })).toEqual({ | ||
| name: 'gemini-cli', | ||
| }); | ||
| it('detects claude via CLAUDE_CODE', () => { | ||
| vi.stubEnv('CLAUDE_CODE', '1'); | ||
| expect(detectAgent()).toEqual({ name: 'claude' }); | ||
| }); | ||
|
|
||
| it('detects OpenAI Codex via CODEX_SANDBOX', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { CODEX_SANDBOX: '1' } })).toEqual({ | ||
| name: 'codex', | ||
| }); | ||
| it('detects gemini via GEMINI_CLI', () => { | ||
| vi.stubEnv('GEMINI_CLI', '1'); | ||
| expect(detectAgent()).toEqual({ name: 'gemini' }); | ||
| }); | ||
|
|
||
| it('detects Cursor Agent via CURSOR_AGENT (even if AGENT is also set)', () => { | ||
| expect( | ||
| detectAgent({ stdoutIsTTY: true, env: { CURSOR_AGENT: '1', AGENT: 'something' } }) | ||
| ).toEqual({ | ||
| name: 'cursor', | ||
| }); | ||
| it('detects codex via CODEX_SANDBOX', () => { | ||
| vi.stubEnv('CODEX_SANDBOX', '1'); | ||
| expect(detectAgent()).toEqual({ name: 'codex' }); | ||
| }); | ||
|
|
||
| it('treats generic AGENT as unknown', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { AGENT: 'some-agent' } })).toEqual({ | ||
| name: 'unknown', | ||
| }); | ||
| it('detects codex via CODEX_THREAD_ID', () => { | ||
| vi.stubEnv('CODEX_THREAD_ID', '1'); | ||
| expect(detectAgent()).toEqual({ name: 'codex' }); | ||
| }); | ||
|
|
||
| it('does not use heuristics when stdout is a TTY', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { TERM: 'dumb' } })).toEqual(undefined); | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { GIT_PAGER: 'cat' } })).toEqual(undefined); | ||
| it('detects cursor via CURSOR_AGENT', () => { | ||
| vi.stubEnv('CURSOR_AGENT', '1'); | ||
| expect(detectAgent()).toEqual({ name: 'cursor' }); | ||
| }); | ||
|
|
||
| it('detects unknown agent via TERM=dumb when stdout is not a TTY', () => { | ||
| expect(detectAgent({ stdoutIsTTY: false, env: { TERM: 'dumb' } })).toEqual({ | ||
| name: 'unknown', | ||
| }); | ||
| it('detects opencode via OPENCODE', () => { | ||
| vi.stubEnv('OPENCODE', '1'); | ||
| expect(detectAgent()).toEqual({ name: 'opencode' }); | ||
| }); | ||
|
|
||
| it('detects unknown agent via GIT_PAGER=cat when stdout is not a TTY', () => { | ||
| expect(detectAgent({ stdoutIsTTY: false, env: { GIT_PAGER: 'cat' } })).toEqual({ | ||
| name: 'unknown', | ||
| }); | ||
| it('detects explicit agent via AI_AGENT env var', () => { | ||
| vi.stubEnv('AI_AGENT', 'copilot'); | ||
| expect(detectAgent()).toEqual({ name: 'copilot' }); | ||
| }); | ||
|
|
||
| it('returns isAgent=false when there are no signals', () => { | ||
| expect(detectAgent({ stdoutIsTTY: false, env: {} })).toEqual(undefined); | ||
| it('normalizes AI_AGENT to lowercase', () => { | ||
| vi.stubEnv('AI_AGENT', 'Copilot'); | ||
| expect(detectAgent()).toEqual({ name: 'copilot' }); | ||
| }); | ||
|
|
||
| it('applies heuristics even when CI is set (no CI special-casing)', () => { | ||
| expect( | ||
| detectAgent({ | ||
| stdoutIsTTY: false, | ||
| env: { CI: 'true', TERM: 'dumb' }, | ||
| }) | ||
| ).toEqual({ name: 'unknown' }); | ||
| it('AI_AGENT takes precedence over other env vars', () => { | ||
| vi.stubEnv('AI_AGENT', 'copilot'); | ||
| vi.stubEnv('CLAUDECODE', '1'); | ||
| vi.stubEnv('GEMINI_CLI', '1'); | ||
| expect(detectAgent()).toEqual({ name: 'copilot' }); | ||
| }); | ||
|
|
||
| it('still detects explicit agents in CI', () => { | ||
| expect(detectAgent({ stdoutIsTTY: false, env: { CI: 'true', CODEX_SANDBOX: '1' } })).toEqual({ | ||
| name: 'codex', | ||
| }); | ||
| it('returns undefined when there are no signals', () => { | ||
| expect(detectAgent()).toEqual(undefined); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,90 +1,19 @@ | ||
| export type KnownAgentName = | ||
| | 'claude-code' | ||
| | 'gemini-cli' | ||
| | 'cursor' | ||
| | 'codex' | ||
| | 'opencode' | ||
| | 'amp' | ||
| | 'unknown'; | ||
| import { detectAgent as stdEnvDetectAgent } from 'std-env'; | ||
|
|
||
| export type AgentInfo = { | ||
| name: KnownAgentName; | ||
| /** The name of the detected AI coding agent (e.g. `claude`, `gemini`, `codex`, `cursor`). | ||
| * Can be any value supported by std-env or explicitly set via the `AI_AGENT` environment variable. | ||
| */ | ||
| name: string; | ||
| }; | ||
|
|
||
| export type AgentDetection = AgentInfo | undefined; | ||
|
|
||
| type DetectAgentOptions = { | ||
| stdoutIsTTY: boolean; | ||
| env: NodeJS.ProcessEnv; | ||
| }; | ||
|
|
||
| function detectExplicitAgent(env: NodeJS.ProcessEnv): AgentInfo | undefined { | ||
| // Amp | ||
| if (env.AGENT === 'amp') { | ||
| return { | ||
| name: 'amp', | ||
| }; | ||
| } | ||
|
|
||
| // Claude Code | ||
| if (env.CLAUDECODE) { | ||
| return { | ||
| name: 'claude-code', | ||
| }; | ||
| } | ||
|
|
||
| // Gemini CLI | ||
| if (env.GEMINI_CLI) { | ||
| return { | ||
| name: 'gemini-cli', | ||
| }; | ||
| } | ||
|
|
||
| // OpenAI Codex | ||
| if (env.CODEX_SANDBOX) { | ||
| return { | ||
| name: 'codex', | ||
| }; | ||
| } | ||
|
|
||
| // Cursor Agent (proposed / best-effort; Cursor often sets VSCode env vars too) | ||
| if (env.CURSOR_AGENT) { | ||
| return { | ||
| name: 'cursor', | ||
| }; | ||
| } | ||
|
|
||
| // Generic "AGENT" marker (unknown implementation) | ||
| if (env.AGENT) { | ||
| return { name: 'unknown' }; | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| /** Detect whether Storybook CLI is likely being invoked by an AI agent. */ | ||
| export const detectAgent = (options: DetectAgentOptions): AgentDetection => { | ||
| const env = options.env; | ||
|
|
||
| // 1) Explicit agent variables (strong signal; allow even in CI/TTY) | ||
| const explicit = detectExplicitAgent(env); | ||
| if (explicit) { | ||
| return explicit; | ||
| } | ||
|
|
||
| const stdoutIsTTY = options.stdoutIsTTY; | ||
|
|
||
| // 2) Behavioral / fingerprint heuristics (exclude CI to reduce false positives) | ||
| if (stdoutIsTTY) { | ||
| /** Detect whether Storybook CLI is likely being invoked by an AI agent, using std-env. */ | ||
| export const detectAgent = (): AgentDetection => { | ||
| const { name } = stdEnvDetectAgent(); | ||
| if (!name) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const isDumbTerm = env.TERM === 'dumb'; | ||
| const hasAgentPager = env.GIT_PAGER === 'cat'; | ||
|
|
||
| if (isDumbTerm || hasAgentPager) { | ||
| return { name: 'unknown' }; | ||
| } | ||
|
|
||
| return undefined; | ||
| return { name }; | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -51,7 +51,7 @@ const getOperatingSystem = (): 'Windows' | 'macOS' | 'Linux' | `Other: ${string} | |||||||
| // by the app. currently: | ||||||||
| // - cliVersion | ||||||||
| const inCI = isCI(); | ||||||||
| const agentDetection = detectAgent({ stdoutIsTTY: process.stdout.isTTY, env: process.env }); | ||||||||
| const agentDetection = detectAgent(); | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may break former telemetry as name are not exactly the same as before. 'claude-code' for example is 'claude' in std-env
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @copilot
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is an intentional breaking change that comes with adopting std-env's naming conventions. It's documented in the PR description's "Behavioural changes to note" table:
Since the goal is to use std-env as the canonical source for agent detection, these new names are the ones std-env uses and will continue to maintain. Any telemetry dashboards/queries that filter on the old names would need to be updated to account for the rename. Let me know if you'd prefer to add a name-mapping layer to preserve backward compatibility with historical telemetry data, and I'll implement that.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will break our telemetry. It is probably better to have a name-mapping.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @shilman |
||||||||
| const globalContext = { | ||||||||
| inCI, | ||||||||
| isTTY: process.stdout.isTTY, | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
std-env detectAgent function API💡 Result:
std-env(the UnJS npm package) does not expose adetectAgent()function. Its public API is primarily environment flags, plus CI/provider detection and runtime detection via exported constants likeprovider,providerInfo,runtime, andruntimeInfo. [1][2][3]Example usage:
If you meant a different “std-env” library (or a different package that does have
detectAgent), share a link/repo name and I’ll pull the exact API. [1][2]Sources: [1] [2] [3]
🏁 Script executed:
rg "detectAgent" --type js --type ts -B 2 -A 2Repository: storybookjs/storybook
Length of output: 5940
🏁 Script executed:
Repository: storybookjs/storybook
Length of output: 803
Critical: std-env v4.0.0 does not export
detectAgentfunction.The code imports
detectAgentfromstd-env(line 1 ofdetect-agent.ts), but std-env v4's public API provides onlyprovider,providerInfo,runtime,runtimeInfo, andisCI. This import will fail at runtime. Either use a different package that provides agent detection, or implement the agent detection logic directly without external dependency.🤖 Prompt for AI Agents