Skip to content

Commit 6a77fc0

Browse files
committed
fix(tracking): Detect more coding agents
### Description Refactors the AI agent detection logic to improve type safety and maintainability. Key changes: - Defined CONSTANT_CASE constants for all supported AI agents in src/env.ts. - Introduced an AiAgents union type for better type safety. - Refactored detectAIAgent() to use these constants and return the new type. - Incorporated detection for GOOGLE_AI_STUDIO via the APPLET_DIR environment variable. - Updated apiv2.ts to use the normalized UNKNOWN constant. - Added 18 unit tests in src/env.spec.ts with Sinon stubs and module reloading. ### Scenarios Tested - Unit Testing (src/env.spec.ts): Verified detection of all AI agents (Antigravity, Claude, Cline, Codex, Cursor, Gemini, OpenCode, Replit, Copilot, Google AI Studio) and alternative environment variables. - Verified isFirebaseStudio() and isFirebaseMcp() logic. ### Sample Commands N/A
1 parent ad7c1e3 commit 6a77fc0

3 files changed

Lines changed: 194 additions & 12 deletions

File tree

src/apiv2.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import util from "util";
99

1010
import * as auth from "./auth";
1111
import { FirebaseError } from "./error";
12-
import { isFirebaseMcp, detectAIAgent } from "./env";
12+
import { isFirebaseMcp, detectAIAgent, UNKNOWN } from "./env";
1313
import { logger } from "./logger";
1414
import { responseToError } from "./responseToError";
1515
import * as FormData from "form-data";
@@ -21,7 +21,7 @@ const CLI_VERSION: string = pkg.version;
2121

2222
export const standardHeaders: () => Record<string, string> = () => {
2323
const agent = detectAIAgent();
24-
const agentStr = agent === "unknown" ? "" : ` agent-name/${agent}`;
24+
const agentStr = agent === UNKNOWN ? "" : ` agent-name/${agent}`;
2525
const platform = isFirebaseMcp() ? "FirebaseMCP" : "FirebaseCLI";
2626
const clientVersion = `${platform}/${CLI_VERSION}${agentStr}`;
2727
return {

src/env.spec.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import * as fsutils from "./fsutils";
4+
5+
describe("env", () => {
6+
let env: typeof import("./env");
7+
8+
beforeEach(() => {
9+
// Reload the env module to reset internal caches
10+
delete require.cache[require.resolve("./env")];
11+
env = require("./env") as typeof import("./env");
12+
});
13+
14+
describe("detectAIAgent", () => {
15+
let envStub: sinon.SinonStub;
16+
17+
beforeEach(() => {
18+
// Stub process.env to an empty object for isolation
19+
envStub = sinon.stub(process, "env").value({});
20+
});
21+
22+
afterEach(() => {
23+
envStub.restore();
24+
});
25+
26+
it("should return UNKNOWN when no agent env vars are set", () => {
27+
expect(env.detectAIAgent()).to.equal(env.UNKNOWN);
28+
});
29+
30+
it("should detect antigravity", () => {
31+
process.env["ANTIGRAVITY_CLI_ALIAS"] = "true";
32+
expect(env.detectAIAgent()).to.equal(env.ANTIGRAVITY);
33+
});
34+
35+
it("should detect claude_code", () => {
36+
process.env["CLAUDECODE"] = "true";
37+
expect(env.detectAIAgent()).to.equal(env.CLAUDE_CODE);
38+
});
39+
40+
it("should detect claude_code_cowork", () => {
41+
process.env["CLAUDECODE"] = "true";
42+
process.env["CLAUDE_CODE_IS_COWORK"] = "true";
43+
expect(env.detectAIAgent()).to.equal(env.CLAUDE_CODE_COWORK);
44+
});
45+
46+
it("should detect cline", () => {
47+
process.env["CLINE_ACTIVE"] = "true";
48+
expect(env.detectAIAgent()).to.equal(env.CLINE);
49+
});
50+
51+
it("should detect codex_cli", () => {
52+
process.env["CODEX_SANDBOX"] = "true";
53+
expect(env.detectAIAgent()).to.equal(env.CODEX_CLI);
54+
});
55+
56+
it("should detect cursor", () => {
57+
process.env["CURSOR_AGENT"] = "true";
58+
expect(env.detectAIAgent()).to.equal(env.CURSOR);
59+
});
60+
61+
it("should detect gemini_cli", () => {
62+
process.env["GEMINI_CLI"] = "true";
63+
expect(env.detectAIAgent()).to.equal(env.GEMINI_CLI);
64+
});
65+
66+
it("should detect open_code", () => {
67+
process.env["OPENCODE"] = "true";
68+
expect(env.detectAIAgent()).to.equal(env.OPEN_CODE);
69+
});
70+
71+
it("should detect replit", () => {
72+
process.env["REPLIT_USER"] = "user";
73+
expect(env.detectAIAgent()).to.equal(env.REPLIT);
74+
});
75+
76+
it("should detect copilot", () => {
77+
process.env["COPILOT_MODEL"] = "gpt-4";
78+
expect(env.detectAIAgent()).to.equal(env.COPILOT);
79+
});
80+
81+
it("should detect google_ai_studio", () => {
82+
process.env["APPLET_DIR"] = "/some/path";
83+
expect(env.detectAIAgent()).to.equal(env.GOOGLE_AI_STUDIO);
84+
});
85+
86+
it("should detect cursor via alternative env vars", () => {
87+
process.env["CURSOR_TRACE_ID"] = "123";
88+
expect(env.detectAIAgent()).to.equal(env.CURSOR);
89+
90+
delete process.env["CURSOR_TRACE_ID"];
91+
process.env["CODEX_THREAD_ID"] = "456";
92+
expect(env.detectAIAgent()).to.equal(env.CURSOR);
93+
94+
delete process.env["CODEX_THREAD_ID"];
95+
process.env["CODEX_SANDBOX_NETWORK_DISABLED"] = "true";
96+
expect(env.detectAIAgent()).to.equal(env.CURSOR);
97+
});
98+
99+
it("should detect open_code via OPENCODE_CLIENT", () => {
100+
process.env["OPENCODE_CLIENT"] = "true";
101+
expect(env.detectAIAgent()).to.equal(env.OPEN_CODE);
102+
});
103+
104+
it("should detect replit via REPL_ID", () => {
105+
process.env["REPL_ID"] = "abc-123";
106+
expect(env.detectAIAgent()).to.equal(env.REPLIT);
107+
});
108+
});
109+
110+
describe("isFirebaseMcp", () => {
111+
it("should reflect setFirebaseMcp value", () => {
112+
env.setFirebaseMcp(true);
113+
expect(env.isFirebaseMcp()).to.be.true;
114+
env.setFirebaseMcp(false);
115+
expect(env.isFirebaseMcp()).to.be.false;
116+
});
117+
});
118+
119+
describe("isFirebaseStudio", () => {
120+
let dirExistsSyncStub: sinon.SinonStub;
121+
122+
beforeEach(() => {
123+
dirExistsSyncStub = sinon.stub(fsutils, "dirExistsSync");
124+
});
125+
126+
afterEach(() => {
127+
dirExistsSyncStub.restore();
128+
});
129+
130+
it("should return true if MONOSPACE_ENV is set", () => {
131+
const envStub = sinon.stub(process, "env").value({ MONOSPACE_ENV: "true" });
132+
expect(env.isFirebaseStudio()).to.be.true;
133+
envStub.restore();
134+
});
135+
136+
it("should return true if /google/idx exists", () => {
137+
dirExistsSyncStub.withArgs("/google/idx").returns(true);
138+
expect(env.isFirebaseStudio()).to.be.true;
139+
});
140+
});
141+
});

src/env.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,56 @@ export function isFirebaseMcp() {
1616
export function setFirebaseMcp(value: boolean) {
1717
isFirebaseMcpFlag = value;
1818
}
19+
export const ANTIGRAVITY = "antigravity" as const;
20+
export const CLAUDE_CODE_COWORK = "claude_code_cowork" as const;
21+
export const CLAUDE_CODE = "claude_code" as const;
22+
export const CLINE = "cline" as const;
23+
export const CODEX_CLI = "codex_cli" as const;
24+
export const CURSOR = "cursor" as const;
25+
export const GEMINI_CLI = "gemini_cli" as const;
26+
export const OPEN_CODE = "open_code" as const;
27+
export const REPLIT = "replit" as const;
28+
export const COPILOT = "copilot" as const;
29+
export const GOOGLE_AI_STUDIO = "google_ai_studio" as const;
30+
export const UNKNOWN = "unknown" as const;
1931

20-
// Detect if the CLI was invoked by a coding agent, based on well-known env vars.
21-
export function detectAIAgent(): string {
22-
if (process.env.ANTIGRAVITY_CLI_ALIAS) return "antigravity";
23-
if (process.env.CLAUDECODE) return "claude_code";
24-
if (process.env.CLINE_ACTIVE) return "cline";
25-
if (process.env.CODEX_SANDBOX) return "codex_cli";
26-
if (process.env.CURSOR_AGENT) return "cursor";
27-
if (process.env.GEMINI_CLI) return "gemini_cli";
28-
if (process.env.OPENCODE) return "open_code";
29-
return "unknown";
32+
/** All possible AI Agents */
33+
export type AiAgents =
34+
| typeof ANTIGRAVITY
35+
| typeof CLAUDE_CODE_COWORK
36+
| typeof CLAUDE_CODE
37+
| typeof CLINE
38+
| typeof CODEX_CLI
39+
| typeof CURSOR
40+
| typeof GEMINI_CLI
41+
| typeof GOOGLE_AI_STUDIO
42+
| typeof OPEN_CODE
43+
| typeof REPLIT
44+
| typeof COPILOT
45+
| typeof UNKNOWN;
46+
47+
/**
48+
* Detect if the CLI was invoked by a coding agent, based on well-known env vars. Returns "unknown" if the coding agent is undefined, or if the environment is not running in a coding agent.
49+
* @returns The AI agent that invoked the CLI, or UNKNOWN if none.
50+
*/
51+
export function detectAIAgent(): AiAgents {
52+
if (process.env["ANTIGRAVITY_CLI_ALIAS"]) return ANTIGRAVITY;
53+
if (process.env["CLAUDECODE"]) {
54+
return process.env["CLAUDE_CODE_IS_COWORK"] ? CLAUDE_CODE_COWORK : CLAUDE_CODE;
55+
}
56+
if (process.env["CLINE_ACTIVE"]) return CLINE;
57+
if (process.env["CODEX_SANDBOX"]) return CODEX_CLI;
58+
if (
59+
process.env["CURSOR_AGENT"] ||
60+
process.env["CURSOR_TRACE_ID"] ||
61+
process.env["CODEX_THREAD_ID"] ||
62+
process.env["CODEX_SANDBOX_NETWORK_DISABLED"]
63+
)
64+
return CURSOR;
65+
if (process.env["GEMINI_CLI"]) return GEMINI_CLI;
66+
if (process.env["OPENCODE"] || process.env["OPENCODE_CLIENT"]) return OPEN_CODE;
67+
if (process.env["REPLIT_USER"] || process.env["REPL_ID"]) return REPLIT;
68+
if (process.env["COPILOT_MODEL"]) return COPILOT;
69+
if (process.env["APPLET_DIR"]) return GOOGLE_AI_STUDIO;
70+
return UNKNOWN;
3071
}

0 commit comments

Comments
 (0)