Skip to content

Commit b9c15b5

Browse files
authored
feat: Detect if ran from coding agent (#92)
1 parent 58384cc commit b9c15b5

4 files changed

Lines changed: 202 additions & 12 deletions

File tree

packages/spark/src/clack-wizard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1251,7 +1251,7 @@ async function loginWithCiCredentials(args: {
12511251
export function buildDefaultDeps(args: DefaultDepsArgs): WizardDeps {
12521252
const cwd = args.cwd ?? processCwd();
12531253
const env = args.env ?? process.env;
1254-
const clientContext = buildCliSetupClientContext(args.options);
1254+
const clientContext = buildCliSetupClientContext(args.options, env);
12551255
return {
12561256
cwd,
12571257
env,

packages/spark/src/events.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { existsSync } from "node:fs";
2+
13
import pkg from "../package.json" with { type: "json" };
24
import type { WizardSessionCreateResponse } from "./auth";
35
import { DEFAULT_APP_URL, type WizardOptions } from "./options";
46
import {
57
CLI_SETUP_DOCS_PAGES,
8+
type CliSetupAgentMarker,
69
type CliSetupAuthMode,
710
type CliSetupClientContext,
811
type CliSetupDocsPage,
@@ -11,6 +14,7 @@ import {
1114

1215
export type {
1316
CliSetupAuthMode,
17+
CliSetupAgentMarker,
1418
CliSetupClientContext,
1519
CliSetupDocsPage,
1620
CliSetupEntryPoint,
@@ -213,6 +217,29 @@ const FINAL_FLUSH_TIMEOUT_MS = 5_000;
213217

214218
const DOCS_SOURCE_PREFIX = "docs_";
215219

220+
const DECLARED_AGENT_MARKERS = {
221+
amp: "amp",
222+
antigravity: "antigravity",
223+
"augment-cli": "augment",
224+
augment: "augment",
225+
claude: "claude_code",
226+
"claude-code": "claude_code",
227+
codex: "codex",
228+
cursor: "cursor",
229+
"cursor-cli": "cursor",
230+
devin: "devin",
231+
gemini: "gemini_cli",
232+
"gemini-cli": "gemini_cli",
233+
"github-copilot": "github_copilot",
234+
"github-copilot-cli": "github_copilot",
235+
github_copilot_vscode_agent: "github_copilot",
236+
goose: "goose",
237+
opencode: "opencode",
238+
replit: "replit",
239+
} as const satisfies Record<string, CliSetupAgentMarker>;
240+
241+
const FALSY_AGENT_MARKERS = new Set(["0", "false", "no", "off"]);
242+
216243
export function setupAttribution(args: {
217244
readonly from?: string | undefined;
218245
}): Pick<CliSetupClientContext, "entryPoint" | "docsPage"> {
@@ -238,14 +265,79 @@ export function setupAttribution(args: {
238265

239266
export function buildCliSetupClientContext(
240267
options: WizardOptions,
268+
env: NodeJS.ProcessEnv = process.env,
269+
pathExists: (path: string) => boolean = existsSync,
241270
): CliSetupClientContext {
242271
const ci = options.apiKey !== undefined && options.projectId !== undefined;
272+
const declaredAiAgent = env["AI_AGENT"]?.trim().toLowerCase();
273+
let agentMarker: CliSetupAgentMarker | undefined =
274+
declaredAiAgent && !FALSY_AGENT_MARKERS.has(declaredAiAgent)
275+
? (DECLARED_AGENT_MARKERS[
276+
declaredAiAgent as keyof typeof DECLARED_AGENT_MARKERS
277+
] ?? "other")
278+
: undefined;
279+
if (
280+
agentMarker === undefined &&
281+
(env["CODEX_THREAD_ID"] || env["CODEX_CI"] || env["CODEX_SANDBOX"])
282+
) {
283+
agentMarker = "codex";
284+
} else if (
285+
agentMarker === undefined &&
286+
(env["CLAUDECODE"] || env["CLAUDE_CODE"] || env["CLAUDE_CODE_ENTRYPOINT"])
287+
) {
288+
agentMarker = "claude_code";
289+
} else if (
290+
agentMarker === undefined &&
291+
(env["CURSOR_AGENT"] ||
292+
env["CURSOR_TRACE_ID"] ||
293+
env["CURSOR_EXTENSION_HOST_ROLE"] === "agent-exec")
294+
) {
295+
agentMarker = "cursor";
296+
} else if (agentMarker === undefined && env["GEMINI_CLI"]) {
297+
agentMarker = "gemini_cli";
298+
} else if (
299+
agentMarker === undefined &&
300+
(env["OPENCODE"] || env["OPENCODE_CLIENT"])
301+
) {
302+
agentMarker = "opencode";
303+
} else if (
304+
agentMarker === undefined &&
305+
(env["VSCODE_AGENT"] ||
306+
env["COPILOT_MODEL"] ||
307+
env["COPILOT_ALLOW_ALL"] ||
308+
env["COPILOT_GITHUB_TOKEN"])
309+
) {
310+
agentMarker = "github_copilot";
311+
} else if (agentMarker === undefined && env["GOOSE_TERMINAL"]) {
312+
agentMarker = "goose";
313+
} else if (agentMarker === undefined && env["ANTIGRAVITY_AGENT"]) {
314+
agentMarker = "antigravity";
315+
} else if (agentMarker === undefined && env["AUGMENT_AGENT"]) {
316+
agentMarker = "augment";
317+
} else if (agentMarker === undefined && env["REPL_ID"]) {
318+
agentMarker = "replit";
319+
}
320+
const declaredAgent = env["AGENT"]?.trim().toLowerCase();
321+
if (
322+
agentMarker === undefined &&
323+
declaredAgent &&
324+
!FALSY_AGENT_MARKERS.has(declaredAgent)
325+
) {
326+
agentMarker =
327+
DECLARED_AGENT_MARKERS[
328+
declaredAgent as keyof typeof DECLARED_AGENT_MARKERS
329+
] ?? "other";
330+
}
331+
if (agentMarker === undefined && pathExists("/opt/.devin")) {
332+
agentMarker = "devin";
333+
}
243334
return {
244335
cliVersion: pkg.version,
245336
platform: process.platform,
246337
architecture: process.arch,
247338
...setupAttribution({ from: options.from }),
248339
...(ci ? { authMode: "ci" as const } : {}),
340+
...(agentMarker === undefined ? {} : { agentMarker }),
249341
};
250342
}
251343

packages/spark/src/setup-events-contract.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ export const CLI_SETUP_DOCS_PAGES = [
1717

1818
export type CliSetupDocsPage = (typeof CLI_SETUP_DOCS_PAGES)[number];
1919
export type CliSetupAuthMode = "signin" | "signup" | "ci";
20+
export type CliSetupAgentMarker =
21+
| "amp"
22+
| "antigravity"
23+
| "augment"
24+
| "claude_code"
25+
| "codex"
26+
| "cursor"
27+
| "devin"
28+
| "gemini_cli"
29+
| "github_copilot"
30+
| "goose"
31+
| "opencode"
32+
| "other"
33+
| "replit";
2034

2135
export type CliSetupClientContext = {
2236
readonly cliVersion: string;
@@ -25,4 +39,5 @@ export type CliSetupClientContext = {
2539
readonly entryPoint: CliSetupEntryPoint;
2640
readonly docsPage?: CliSetupDocsPage | undefined;
2741
readonly authMode?: CliSetupAuthMode | undefined;
42+
readonly agentMarker?: CliSetupAgentMarker | undefined;
2843
};

packages/spark/test/events.test.ts

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const CONTEXT: CliSetupClientContext = {
2222
platform: "linux",
2323
architecture: "x64",
2424
entryPoint: "direct",
25+
agentMarker: "cursor",
2526
};
2627

2728
describe("setupAttribution", () => {
@@ -61,16 +62,19 @@ describe("setupAttribution", () => {
6162
});
6263

6364
it("builds credential auth context without replacing the entry point", () => {
64-
const context = buildCliSetupClientContext({
65-
apiUrl: "https://api.test",
66-
appUrl: "https://app.test",
67-
apiKey: "secret",
68-
projectId: "project-id",
69-
orgId: undefined,
70-
projId: undefined,
71-
yolo: false,
72-
from: "homepage",
73-
});
65+
const context = buildCliSetupClientContext(
66+
{
67+
apiUrl: "https://api.test",
68+
appUrl: "https://app.test",
69+
apiKey: "secret",
70+
projectId: "project-id",
71+
orgId: undefined,
72+
projId: undefined,
73+
yolo: false,
74+
from: "homepage",
75+
},
76+
{},
77+
);
7478

7579
expect(context).toMatchObject({
7680
entryPoint: "homepage",
@@ -80,6 +84,85 @@ describe("setupAttribution", () => {
8084
expect(context.platform).toBe(process.platform);
8185
expect(context.architecture).toBe(process.arch);
8286
});
87+
88+
it.each([
89+
[{ CODEX_THREAD_ID: "thread-id" }, "codex"],
90+
[{ CODEX_CI: "1" }, "codex"],
91+
[{ CODEX_SANDBOX: "seatbelt" }, "codex"],
92+
[{ CLAUDECODE: "1" }, "claude_code"],
93+
[{ CLAUDE_CODE: "1" }, "claude_code"],
94+
[{ CLAUDE_CODE_ENTRYPOINT: "cli" }, "claude_code"],
95+
[{ CURSOR_AGENT: "1" }, "cursor"],
96+
[{ CURSOR_TRACE_ID: "trace-id" }, "cursor"],
97+
[{ CURSOR_EXTENSION_HOST_ROLE: "agent-exec" }, "cursor"],
98+
[{ GEMINI_CLI: "1" }, "gemini_cli"],
99+
[{ OPENCODE: "1" }, "opencode"],
100+
[{ OPENCODE_CLIENT: "acp" }, "opencode"],
101+
[{ VSCODE_AGENT: "1" }, "github_copilot"],
102+
[{ COPILOT_MODEL: "model" }, "github_copilot"],
103+
[{ COPILOT_ALLOW_ALL: "1" }, "github_copilot"],
104+
[{ COPILOT_GITHUB_TOKEN: "secret" }, "github_copilot"],
105+
[{ GOOSE_TERMINAL: "1" }, "goose"],
106+
[{ ANTIGRAVITY_AGENT: "1" }, "antigravity"],
107+
[{ AUGMENT_AGENT: "1" }, "augment"],
108+
[{ REPL_ID: "repl-id" }, "replit"],
109+
[{ AI_AGENT: "github_copilot_vscode_agent" }, "github_copilot"],
110+
[{ AI_AGENT: "custom-agent" }, "other"],
111+
[{ AGENT: "amp" }, "amp"],
112+
[{ AGENT: "goose" }, "goose"],
113+
] as const)("reports the coding agent marker from %j", (env, agentMarker) => {
114+
const context = buildCliSetupClientContext(
115+
{
116+
apiUrl: "https://api.test",
117+
appUrl: "https://app.test",
118+
apiKey: undefined,
119+
projectId: undefined,
120+
orgId: undefined,
121+
projId: undefined,
122+
yolo: false,
123+
},
124+
env,
125+
() => false,
126+
);
127+
128+
expect(context.agentMarker).toBe(agentMarker);
129+
});
130+
131+
it("reports Devin from its local environment path", () => {
132+
const context = buildCliSetupClientContext(
133+
{
134+
apiUrl: "https://api.test",
135+
appUrl: "https://app.test",
136+
apiKey: undefined,
137+
projectId: undefined,
138+
orgId: undefined,
139+
projId: undefined,
140+
yolo: false,
141+
},
142+
{},
143+
(path) => path === "/opt/.devin",
144+
);
145+
146+
expect(context.agentMarker).toBe("devin");
147+
});
148+
149+
it("omits the coding agent marker when no supported marker is present", () => {
150+
const context = buildCliSetupClientContext(
151+
{
152+
apiUrl: "https://api.test",
153+
appUrl: "https://app.test",
154+
apiKey: undefined,
155+
projectId: undefined,
156+
orgId: undefined,
157+
projId: undefined,
158+
yolo: false,
159+
},
160+
{ CI: "true", CODEX_HOME: "/tmp/codex" },
161+
() => false,
162+
);
163+
164+
expect(context).not.toHaveProperty("agentMarker");
165+
});
83166
});
84167

85168
describe("createWizardEvents", () => {
@@ -153,7 +236,7 @@ describe("createWizardEvents", () => {
153236
});
154237
expect(requests[4]).toMatchObject({
155238
event: "cliSetupTerminated",
156-
clientContext: { authMode: "signin" },
239+
clientContext: { authMode: "signin", agentMarker: "cursor" },
157240
properties: {
158241
outcome: "completed",
159242
durationMs: 65,

0 commit comments

Comments
 (0)