Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@
"sirv": "^2.0.4",
"slash": "^5.0.0",
"source-map": "^0.7.4",
"std-env": "^4.0.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

std-env detectAgent function API

💡 Result:

std-env (the UnJS npm package) does not expose a detectAgent() function. Its public API is primarily environment flags, plus CI/provider detection and runtime detection via exported constants like provider, providerInfo, runtime, and runtimeInfo. [1][2][3]

Example usage:

import { provider, providerInfo, runtime, runtimeInfo, isCI } from "std-env";

console.log({ isCI, provider, providerInfo, runtime, runtimeInfo });

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 2

Repository: storybookjs/storybook

Length of output: 5940


🏁 Script executed:

cat -n code/core/src/telemetry/detect-agent.ts

Repository: storybookjs/storybook

Length of output: 803


Critical: std-env v4.0.0 does not export detectAgent function.

The code imports detectAgent from std-env (line 1 of detect-agent.ts), but std-env v4's public API provides only provider, providerInfo, runtime, runtimeInfo, and isCI. 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
Verify each finding against the current code and only fix it if needed.

In `@code/core/package.json` at line 360, The project imports detectAgent from
std-env but std-env v4 doesn't export it; update detect-agent.ts to stop
importing detectAgent from std-env and provide your own implementation or switch
to a package that actually exposes that API. Specifically, in detect-agent.ts
remove the failing import, implement and export a local detectAgent function (or
use a compatible library) that inspects known environment indicators (e.g.,
process.env variables like VERCEL, NETLIFY, GITHUB_ACTIONS, CI, etc.) and any
runtime/provider info you need, and then update any callers to use this local
detectAgent; if you prefer to keep std-env, change package.json to a std-env
version that exports detectAgent and adjust imports accordingly.

"store2": "^2.14.2",
"strip-ansi": "^7.1.0",
"strip-json-comments": "^5.0.1",
Expand Down
106 changes: 38 additions & 68 deletions code/core/src/telemetry/detect-agent.test.ts
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);
});
});
92 changes: 11 additions & 81 deletions code/core/src/telemetry/detect-agent.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,20 @@
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 };
};
2 changes: 1 addition & 1 deletion code/core/src/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment thread
huang-julien marked this conversation as resolved.
const globalContext = {
inCI,
isTTY: process.stdout.isTTY,
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -28255,6 +28255,13 @@ __metadata:
languageName: node
linkType: hard

"std-env@npm:^4.0.0":
version: 4.0.0
resolution: "std-env@npm:4.0.0"
checksum: 10c0/63b1716eae27947adde49e21b7225a0f75fb2c3d410273ae9de8333c07c7d5fc7a0628ae4c8af6b4b49b4274ed46c2bf118ed69b64f1261c9d8213d76ed1c16c
languageName: node
linkType: hard

"steno@npm:^0.4.1":
version: 0.4.4
resolution: "steno@npm:0.4.4"
Expand Down Expand Up @@ -28422,6 +28429,7 @@ __metadata:
sirv: "npm:^2.0.4"
slash: "npm:^5.0.0"
source-map: "npm:^0.7.4"
std-env: "npm:^4.0.0"
store2: "npm:^2.14.2"
strip-ansi: "npm:^7.1.0"
strip-json-comments: "npm:^5.0.1"
Expand Down
Loading