Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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);
});
});
91 changes: 10 additions & 81 deletions code/core/src/telemetry/detect-agent.ts
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 };
};
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();
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:

Before After
claude-code claude
gemini-cli gemini

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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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