-
-
Notifications
You must be signed in to change notification settings - Fork 10k
Telemetry: Add agent detection #33675
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
Changes from 1 commit
017aced
985fbab
392624d
629defe
0d8afbe
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 |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import { describe, expect, it } 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({ isAgent: true, agent: { name: 'amp' } }); | ||
| }); | ||
|
|
||
| it('prefers CLAUDECODE over other explicit signals', () => { | ||
| expect( | ||
| detectAgent({ | ||
| stdoutIsTTY: true, | ||
| env: { CLAUDECODE: '1', GEMINI_CLI: '1', CODEX_SANDBOX: '1', CURSOR_AGENT: '1', AGENT: 'something' }, | ||
| }) | ||
| ).toEqual({ isAgent: true, agent: { name: 'claude-code' } }); | ||
| }); | ||
|
|
||
| it('detects Gemini CLI via GEMINI_CLI', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { GEMINI_CLI: '1' } })).toEqual({ | ||
| isAgent: true, | ||
| agent: { name: 'gemini-cli' }, | ||
| }); | ||
| }); | ||
|
|
||
| it('detects OpenAI Codex via CODEX_SANDBOX', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { CODEX_SANDBOX: '1' } })).toEqual({ | ||
| isAgent: true, | ||
| agent: { name: 'codex' }, | ||
| }); | ||
| }); | ||
|
|
||
| it('detects Cursor Agent via CURSOR_AGENT (even if AGENT is also set)', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { CURSOR_AGENT: '1', AGENT: 'something' } })).toEqual({ | ||
| isAgent: true, | ||
| agent: { name: 'cursor' }, | ||
| }); | ||
| }); | ||
|
|
||
| it('treats generic AGENT as unknown', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { AGENT: 'some-agent' } })).toEqual({ | ||
| isAgent: true, | ||
| agent: { name: 'unknown' }, | ||
| }); | ||
| }); | ||
|
|
||
| it('does not use heuristics when stdout is a TTY', () => { | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { TERM: 'dumb' } })).toEqual({ isAgent: false }); | ||
| expect(detectAgent({ stdoutIsTTY: true, env: { GIT_PAGER: 'cat' } })).toEqual({ isAgent: false }); | ||
| }); | ||
|
|
||
| it('detects unknown agent via TERM=dumb when stdout is not a TTY', () => { | ||
| expect(detectAgent({ stdoutIsTTY: false, env: { TERM: 'dumb' } })).toEqual({ | ||
| isAgent: true, | ||
| agent: { name: 'unknown' }, | ||
| }); | ||
| }); | ||
|
|
||
| it('detects unknown agent via GIT_PAGER=cat when stdout is not a TTY', () => { | ||
| expect(detectAgent({ stdoutIsTTY: false, env: { GIT_PAGER: 'cat' } })).toEqual({ | ||
| isAgent: true, | ||
| agent: { name: 'unknown' }, | ||
| }); | ||
| }); | ||
|
|
||
| it('returns isAgent=false when there are no signals', () => { | ||
| expect(detectAgent({ stdoutIsTTY: false, env: {} })).toEqual({ isAgent: false }); | ||
| }); | ||
|
|
||
| it('applies heuristics even when CI is set (no CI special-casing)', () => { | ||
| expect( | ||
| detectAgent({ | ||
| stdoutIsTTY: false, | ||
| env: { CI: 'true', TERM: 'dumb' }, | ||
| }) | ||
| ).toEqual({ isAgent: true, agent: { name: 'unknown' } }); | ||
| }); | ||
|
|
||
| it('still detects explicit agents in CI', () => { | ||
| expect(detectAgent({ stdoutIsTTY: false, env: { CI: 'true', CODEX_SANDBOX: '1' } })).toEqual({ | ||
| isAgent: true, | ||
| agent: { name: 'codex' }, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| export type KnownAgentName = | ||
| | 'claude-code' | ||
| | 'gemini-cli' | ||
| | 'cursor' | ||
| | 'codex' | ||
| | 'opencode' | ||
| | 'amp' | ||
| | 'unknown'; | ||
|
|
||
| export type AgentInfo = { | ||
| name: KnownAgentName; | ||
| }; | ||
|
|
||
| export type AgentDetection = { | ||
| isAgent: boolean; | ||
| agent?: AgentInfo; | ||
| }; | ||
|
|
||
| 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' }; | ||
| } | ||
|
Comment on lines
+1
to
+60
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. 🧩 Analysis chain🏁 Script executed: # First, let's examine the detect-agent.ts file to confirm the current state
cat -n code/core/src/telemetry/detect-agent.tsRepository: storybookjs/storybook Length of output: 2555 🏁 Script executed: # Search for any references to 'opencode' in the codebase
rg -i "opencode" --type ts --type tsx --type js --type jsx -B 2 -A 2Repository: storybookjs/storybook Length of output: 92 🏁 Script executed: # Also search for 'OPENCODE' environment variable references
rg "OPENCODE" -B 2 -A 2Repository: storybookjs/storybook Length of output: 47 🏁 Script executed: # Let me search more broadly for 'opencode' with simpler rg command
rg -i "opencode"Repository: storybookjs/storybook Length of output: 119 🏁 Script executed: # Also check git history to see when 'opencode' was added
git log --oneline --all -- code/core/src/telemetry/detect-agent.ts | head -20Repository: storybookjs/storybook Length of output: 106 🏁 Script executed: # Check the actual git diff for that file to understand the context
git log -p --all -- code/core/src/telemetry/detect-agent.ts | head -150Repository: storybookjs/storybook Length of output: 47 🏁 Script executed: # Check the full git history of that commit to understand intent
git show 017acedcc0Repository: storybookjs/storybook Length of output: 50382 🏁 Script executed: # Search for any TODO or FIXME comments near opencode
rg -i "opencode|TODO|FIXME" code/core/src/telemetry/ -B 2 -A 2Repository: storybookjs/storybook Length of output: 1327 🌐 Web query:
💡 Result: In OpenCode CLI, the environment variable used to identify the client (agent/client identifier) is:
To select which OpenCode “agent” to run, there isn’t an
Add explicit detection for
🤖 Prompt for AI Agents |
||
|
|
||
| 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 { isAgent: true, agent: explicit }; | ||
| } | ||
|
|
||
| const stdoutIsTTY = options.stdoutIsTTY; | ||
|
|
||
| // 2) Behavioral / fingerprint heuristics (exclude CI to reduce false positives) | ||
| if (stdoutIsTTY) { | ||
| return { isAgent: false }; | ||
| } | ||
|
|
||
| const isDumbTerm = env.TERM === 'dumb'; | ||
| const hasAgentPager = env.GIT_PAGER === 'cat'; | ||
|
|
||
| if (isDumbTerm || hasAgentPager) { | ||
| return { isAgent: true, agent: { name: 'unknown' } }; | ||
| } | ||
|
|
||
| return { isAgent: false }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import type { StorybookConfig, TypescriptOptions } from 'storybook/internal/type | |
|
|
||
| import type { DetectResult } from 'package-manager-detector'; | ||
|
|
||
| import type { AgentInfo } from './detect-agent'; | ||
| import type { KnownPackagesList } from './get-known-packages'; | ||
| import type { MonorepoType } from './get-monorepo-type'; | ||
|
|
||
|
|
@@ -56,6 +57,10 @@ export type StorybookMetadata = { | |
| storybookVersionSpecifier: string; | ||
| generatedAt?: number; | ||
| userSince?: number; | ||
| /** Whether this process is likely invoked by an AI agent */ | ||
| isAgent?: boolean; | ||
|
Member
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. Redundant to |
||
| /** If we can identify the agent, report it; otherwise `unknown` when detected heuristically. */ | ||
| agent?: AgentInfo; | ||
| language: 'typescript' | 'javascript'; | ||
| framework?: { | ||
| name?: string; | ||
|
|
||
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.
Fix misleading test description.
The test description says "returns isAgent=false" but the assertion expects
undefined. ThedetectAgentfunction returnsAgentInfo | undefined, not an object with anisAgentfield.📝 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents