Skip to content
Merged
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
38 changes: 26 additions & 12 deletions src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ namespace Microsoft.DotNet.Cli.Telemetry;

internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector
{
// Most rules are presence-based (a variable being set/non-empty is enough); a few use exact-value
// matching (e.g. OR_APP_NAME, which several tools share). All matching rules contribute to the result.
private static readonly EnvironmentDetectionRuleWithResult<string>[] _detectionRules = [
// Cowork (Claude Code cowork mode) - placed before Claude so the more specific variable is listed first
new EnvironmentDetectionRuleWithResult<string>("cowork", new AnyPresentEnvironmentRule("CLAUDE_CODE_IS_COWORK")),
// Claude Code
new EnvironmentDetectionRuleWithResult<string>("claude", new AnyPresentEnvironmentRule("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT")),
new EnvironmentDetectionRuleWithResult<string>("claude", new AnyPresentEnvironmentRule("CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT")),
// Cursor AI
new EnvironmentDetectionRuleWithResult<string>("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR", "CURSOR_AI")),
new EnvironmentDetectionRuleWithResult<string>("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR", "CURSOR_AI", "CURSOR_TRACE_ID", "CURSOR_AGENT")),
// Gemini
new EnvironmentDetectionRuleWithResult<string>("gemini", new BooleanEnvironmentRule("GEMINI_CLI")),
// GitHub Copilot (legacy gh extension: GITHUB_COPILOT_CLI_MODE=true; new Copilot CLI: GH_COPILOT_WORKING_DIRECTORY, COPILOT_CLI, or COPILOT_AGENT is set)
new EnvironmentDetectionRuleWithResult<string>("copilot", new AnyMatchEnvironmentRule(
new BooleanEnvironmentRule("GITHUB_COPILOT_CLI_MODE"),
new AnyPresentEnvironmentRule("GH_COPILOT_WORKING_DIRECTORY", "COPILOT_CLI", "COPILOT_AGENT"))),
new EnvironmentDetectionRuleWithResult<string>("gemini", new AnyPresentEnvironmentRule("GEMINI_CLI")),
// GitHub Copilot CLI (legacy gh extension: GITHUB_COPILOT_CLI_MODE; new Copilot CLI: GH_COPILOT_WORKING_DIRECTORY, COPILOT_CLI, COPILOT_MODEL, COPILOT_ALLOW_ALL, or COPILOT_GITHUB_TOKEN is set).
new EnvironmentDetectionRuleWithResult<string>("copilot-cli", new AnyPresentEnvironmentRule(
"GITHUB_COPILOT_CLI_MODE", "GH_COPILOT_WORKING_DIRECTORY", "COPILOT_CLI", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", "COPILOT_GITHUB_TOKEN")),
// GitHub Copilot agent mode in VS Code, which sets AI_AGENT=github_copilot_vscode_agent and COPILOT_AGENT=1 on the terminals it runs commands in.
// See https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts
new EnvironmentDetectionRuleWithResult<string>("copilot-vscode", new AnyMatchEnvironmentRule(
new EnvironmentVariableValueRule("AI_AGENT", "github_copilot_vscode_agent"),
new AnyPresentEnvironmentRule("COPILOT_AGENT"))),
// Codex CLI
new EnvironmentDetectionRuleWithResult<string>("codex", new AnyPresentEnvironmentRule("CODEX_CLI", "CODEX_SANDBOX")),
new EnvironmentDetectionRuleWithResult<string>("codex", new AnyPresentEnvironmentRule("CODEX_CLI", "CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID")),
// Aider
new EnvironmentDetectionRuleWithResult<string>("aider", new EnvironmentVariableValueRule("OR_APP_NAME", "Aider")),
// Plandex
Expand All @@ -30,25 +38,31 @@ internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector
// Qwen Code
new EnvironmentDetectionRuleWithResult<string>("qwen", new AnyPresentEnvironmentRule("QWEN_CODE")),
// Droid
new EnvironmentDetectionRuleWithResult<string>("droid", new BooleanEnvironmentRule("DROID_CLI")),
new EnvironmentDetectionRuleWithResult<string>("droid", new AnyPresentEnvironmentRule("DROID_CLI")),
// OpenCode
new EnvironmentDetectionRuleWithResult<string>("opencode", new AnyPresentEnvironmentRule("OPENCODE_AI")),
// Zed AI
new EnvironmentDetectionRuleWithResult<string>("zed", new AnyPresentEnvironmentRule("ZED_ENVIRONMENT", "ZED_TERM")),
// Kimi CLI
new EnvironmentDetectionRuleWithResult<string>("kimi", new BooleanEnvironmentRule("KIMI_CLI")),
new EnvironmentDetectionRuleWithResult<string>("kimi", new AnyPresentEnvironmentRule("KIMI_CLI")),
// OpenHands
new EnvironmentDetectionRuleWithResult<string>("openhands", new EnvironmentVariableValueRule("OR_APP_NAME", "OpenHands")),
// Goose
new EnvironmentDetectionRuleWithResult<string>("goose", new AnyPresentEnvironmentRule("GOOSE_TERMINAL")),
new EnvironmentDetectionRuleWithResult<string>("goose", new AnyPresentEnvironmentRule("GOOSE_TERMINAL", "GOOSE_PROVIDER")),
// Cline
new EnvironmentDetectionRuleWithResult<string>("cline", new AnyPresentEnvironmentRule("CLINE_TASK_ID")),
// Roo Code
new EnvironmentDetectionRuleWithResult<string>("roo", new AnyPresentEnvironmentRule("ROO_CODE_TASK_ID")),
// Windsurf
new EnvironmentDetectionRuleWithResult<string>("windsurf", new AnyPresentEnvironmentRule("WINDSURF_SESSION")),
// Replit
new EnvironmentDetectionRuleWithResult<string>("replit", new AnyPresentEnvironmentRule("REPL_ID")),
// Augment
new EnvironmentDetectionRuleWithResult<string>("augment", new AnyPresentEnvironmentRule("AUGMENT_AGENT")),
// Antigravity
new EnvironmentDetectionRuleWithResult<string>("antigravity", new AnyPresentEnvironmentRule("ANTIGRAVITY_AGENT")),
// (proposed) generic flag for Agentic usage
new EnvironmentDetectionRuleWithResult<string>("generic_agent", new BooleanEnvironmentRule("AGENT_CLI")),
new EnvironmentDetectionRuleWithResult<string>("generic_agent", new AnyPresentEnvironmentRule("AGENT_CLI")),
];

/// <inheritdoc/>
Expand Down
42 changes: 31 additions & 11 deletions test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,29 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId)
public static TheoryData<Dictionary<string, string>?, string?> LLMTelemetryTestCases => new()
{
{ new Dictionary<string, string> { {"CLAUDECODE", "1" } }, "claude" },
{ new Dictionary<string, string> { {"CLAUDE_CODE", "1" } }, "claude" },
{ new Dictionary<string, string> { {"CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" },
{ new Dictionary<string, string> { {"CLAUDE_CODE_IS_COWORK", "1" } }, "cowork" },
{ new Dictionary<string, string> { { "CURSOR_EDITOR", "1" } }, "cursor" },
{ new Dictionary<string, string> { { "CURSOR_AI", "1" } }, "cursor" },
{ new Dictionary<string, string> { { "CURSOR_TRACE_ID", "abc" } }, "cursor" },
{ new Dictionary<string, string> { { "CURSOR_AGENT", "1" } }, "cursor" },
{ new Dictionary<string, string> { { "GEMINI_CLI", "true" } }, "gemini" },
{ new Dictionary<string, string> { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" },
{ new Dictionary<string, string> { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot" },
{ new Dictionary<string, string> { { "COPILOT_CLI", "1" } }, "copilot" },
{ new Dictionary<string, string> { { "COPILOT_AGENT", "1" } }, "copilot" },
// Existence-based: any non-empty value now matches, even "0"
{ new Dictionary<string, string> { { "GEMINI_CLI", "0" } }, "gemini" },
{ new Dictionary<string, string> { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot-cli" },
{ new Dictionary<string, string> { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot-cli" },
{ new Dictionary<string, string> { { "COPILOT_CLI", "1" } }, "copilot-cli" },
{ new Dictionary<string, string> { { "COPILOT_MODEL", "gpt" } }, "copilot-cli" },
{ new Dictionary<string, string> { { "COPILOT_ALLOW_ALL", "1" } }, "copilot-cli" },
{ new Dictionary<string, string> { { "COPILOT_GITHUB_TOKEN", "token" } }, "copilot-cli" },
// GitHub Copilot agent mode in VS Code
{ new Dictionary<string, string> { { "COPILOT_AGENT", "1" } }, "copilot-vscode" },
{ new Dictionary<string, string> { { "AI_AGENT", "github_copilot_vscode_agent" } }, "copilot-vscode" },
{ new Dictionary<string, string> { { "CODEX_CLI", "1" } }, "codex" },
{ new Dictionary<string, string> { { "CODEX_SANDBOX", "1" } }, "codex" },
{ new Dictionary<string, string> { { "CODEX_CI", "1" } }, "codex" },
{ new Dictionary<string, string> { { "CODEX_THREAD_ID", "thread1" } }, "codex" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "Aider" } }, "aider" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "aider" } }, "aider" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "plandex" } }, "plandex" },
Expand All @@ -268,24 +281,31 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId)
{ new Dictionary<string, string> { { "OR_APP_NAME", "OpenHands" } }, "openhands" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "openhands" } }, "openhands" },
{ new Dictionary<string, string> { { "GOOSE_TERMINAL", "1" } }, "goose" },
{ new Dictionary<string, string> { { "GOOSE_PROVIDER", "openai" } }, "goose" },
{ new Dictionary<string, string> { { "CLINE_TASK_ID", "task123" } }, "cline" },
{ new Dictionary<string, string> { { "ROO_CODE_TASK_ID", "task456" } }, "roo" },
{ new Dictionary<string, string> { { "WINDSURF_SESSION", "session789" } }, "windsurf" },
{ new Dictionary<string, string> { { "REPL_ID", "repl1" } }, "replit" },
{ new Dictionary<string, string> { { "AUGMENT_AGENT", "1" } }, "augment" },
{ new Dictionary<string, string> { { "ANTIGRAVITY_AGENT", "1" } }, "antigravity" },
{ new Dictionary<string, string> { { "AGENT_CLI", "true" } }, "generic_agent" },
// Test combinations of older tools
{ new Dictionary<string, string> { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" },
{ new Dictionary<string, string> { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot" },
{ new Dictionary<string, string> { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot-cli" },
{ new Dictionary<string, string> { { "CLAUDECODE", "1" }, { "GEMINI_CLI", "true" }, { "AGENT_CLI", "true" } }, "claude, gemini, generic_agent" },
{ new Dictionary<string, string> { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" }, { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" }, { "AGENT_CLI", "true" } }, "claude, cursor, gemini, copilot, generic_agent" },
{ new Dictionary<string, string> { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" }, { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" }, { "AGENT_CLI", "true" } }, "claude, cursor, gemini, copilot-cli, generic_agent" },
// Test combinations of newer tools
{ new Dictionary<string, string> { { "OR_APP_NAME", "Aider" }, { "CLINE_TASK_ID", "task123" } }, "aider, cline" },
{ new Dictionary<string, string> { { "CODEX_CLI", "1" }, { "WINDSURF_SESSION", "session789" } }, "codex, windsurf" },
{ new Dictionary<string, string> { { "GOOSE_TERMINAL", "1" }, { "ROO_CODE_TASK_ID", "task456" } }, "goose, roo" },
{ new Dictionary<string, string> { { "GEMINI_CLI", "false" } }, null },
{ new Dictionary<string, string> { { "GITHUB_COPILOT_CLI_MODE", "false" } }, null },
{ new Dictionary<string, string> { { "AGENT_CLI", "false" } }, null },
{ new Dictionary<string, string> { { "DROID_CLI", "false" } }, null },
{ new Dictionary<string, string> { { "KIMI_CLI", "false" } }, null },
// Existence-based loosened vars now match regardless of value (e.g. "false" is still a non-empty value)
{ new Dictionary<string, string> { { "GEMINI_CLI", "false" } }, "gemini" },
{ new Dictionary<string, string> { { "GITHUB_COPILOT_CLI_MODE", "false" } }, "copilot-cli" },
{ new Dictionary<string, string> { { "AGENT_CLI", "false" } }, "generic_agent" },
{ new Dictionary<string, string> { { "DROID_CLI", "false" } }, "droid" },
{ new Dictionary<string, string> { { "KIMI_CLI", "false" } }, "kimi" },
// Cowork is distinct from claude and reported independently
{ new Dictionary<string, string> { { "CLAUDE_CODE_IS_COWORK", "1" }, { "CLAUDE_CODE", "1" } }, "cowork, claude" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "SomeOtherApp" } }, null },
{ new Dictionary<string, string>(), null },
};
Expand Down
Loading