diff --git a/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs index 782e7371eb6..690081976d3 100644 --- a/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs +++ b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs @@ -19,7 +19,12 @@ internal sealed class CodingAgentDetector(IConfiguration configuration) : ICodin new("claude", ["CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT"]), new("cursor", ["CURSOR_EDITOR", "CURSOR_AI", "CURSOR_TRACE_ID", "CURSOR_AGENT"]), new("gemini", ["GEMINI_CLI"]), - new("copilot", ["GITHUB_COPILOT_CLI_MODE", "GH_COPILOT_WORKING_DIRECTORY", "COPILOT_CLI", "COPILOT_AGENT", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", "COPILOT_GITHUB_TOKEN"]), + // 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("copilot-cli", ["COPILOT_CLI", "GITHUB_COPILOT_CLI_MODE", "GH_COPILOT_WORKING_DIRECTORY", "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("copilot-vscode", ["AI_AGENT"], "github_copilot_vscode_agent"), + new("copilot-vscode", ["COPILOT_AGENT"]), new("codex", ["CODEX_CLI", "CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID"]), new("aider", ["OR_APP_NAME"], "Aider"), new("plandex", ["OR_APP_NAME"], "plandex"), @@ -59,19 +64,12 @@ internal sealed class CodingAgentDetector(IConfiguration configuration) : ICodin return agentNames is { Count: > 0 } ? string.Join(", ", agentNames) : null; } - private sealed class DetectionRule + private sealed class DetectionRule(string agentName, string[] variableNames, string? expectedValue = null) { - private readonly string[] _variableNames; - private readonly string? _expectedValue; + private readonly string[] _variableNames = variableNames; + private readonly string? _expectedValue = expectedValue; - public DetectionRule(string agentName, string[] variableNames, string? expectedValue = null) - { - AgentName = agentName; - _variableNames = variableNames; - _expectedValue = expectedValue; - } - - public string AgentName { get; } + public string AgentName { get; } = agentName; public bool IsMatch(IConfiguration configuration) { diff --git a/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs b/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs index 6d4669a2335..9a9772df1fe 100644 --- a/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs +++ b/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs @@ -266,10 +266,19 @@ public void InitializeAsync_DoesNotAddCodingAgentTag_WhenCodingAgentIsNotDetecte [Theory] [MemberData(nameof(CodingAgentTelemetryTestCases))] - public void CodingAgentDetector_DetectsKnownCodingAgents(Dictionary environmentVariables, string? expectedCodingAgent) + public void CodingAgentDetector_DetectsKnownCodingAgents((string, string?)[] environmentVariables, string? expectedCodingAgent) { + var configurationValues = new Dictionary(); + foreach (var environmentVariable in environmentVariables) + { + if (environmentVariable.Item1.Length > 0) + { + configurationValues.Add(environmentVariable.Item1, environmentVariable.Item2); + } + } + var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(environmentVariables) + .AddInMemoryCollection(configurationValues) .Build(); var detector = new CodingAgentDetector(configuration); @@ -329,65 +338,66 @@ public async Task InitializeAsync_IsIdempotent() Assert.Equal(tagsAfterFirstInit, tags.Count); // Should have the same number of tags after second init } - public static TheoryData, string?> CodingAgentTelemetryTestCases => new() + public static TheoryData<(string, string?)[], string?> CodingAgentTelemetryTestCases => new() { - { new Dictionary { { "CLAUDECODE", "1" } }, "claude" }, - { new Dictionary { { "CLAUDE_CODE", "1" } }, "claude" }, - { new Dictionary { { "CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" }, - { new Dictionary { { "CLAUDE_CODE_IS_COWORK", "1" } }, "cowork" }, - { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" }, - { new Dictionary { { "CURSOR_AI", "1" } }, "cursor" }, - { new Dictionary { { "CURSOR_TRACE_ID", "abc" } }, "cursor" }, - { new Dictionary { { "CURSOR_AGENT", "1" } }, "cursor" }, - { new Dictionary { { "GEMINI_CLI", "true" } }, "gemini" }, - { new Dictionary { { "GEMINI_CLI", "0" } }, "gemini" }, - { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" }, - { new Dictionary { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot" }, - { new Dictionary { { "COPILOT_CLI", "1" } }, "copilot" }, - { new Dictionary { { "COPILOT_AGENT", "1" } }, "copilot" }, - { new Dictionary { { "COPILOT_MODEL", "gpt" } }, "copilot" }, - { new Dictionary { { "COPILOT_ALLOW_ALL", "1" } }, "copilot" }, - { new Dictionary { { "COPILOT_GITHUB_TOKEN", "token" } }, "copilot" }, - { new Dictionary { { "CODEX_CLI", "1" } }, "codex" }, - { new Dictionary { { "CODEX_SANDBOX", "1" } }, "codex" }, - { new Dictionary { { "CODEX_CI", "1" } }, "codex" }, - { new Dictionary { { "CODEX_THREAD_ID", "thread1" } }, "codex" }, - { new Dictionary { { "OR_APP_NAME", "Aider" } }, "aider" }, - { new Dictionary { { "OR_APP_NAME", "aider" } }, "aider" }, - { new Dictionary { { "OR_APP_NAME", "plandex" } }, "plandex" }, - { new Dictionary { { "OR_APP_NAME", "Plandex" } }, "plandex" }, - { new Dictionary { { "AMP_HOME", "/path/to/amp" } }, "amp" }, - { new Dictionary { { "QWEN_CODE", "1" } }, "qwen" }, - { new Dictionary { { "DROID_CLI", "true" } }, "droid" }, - { new Dictionary { { "OPENCODE_AI", "1" } }, "opencode" }, - { new Dictionary { { "ZED_ENVIRONMENT", "1" } }, "zed" }, - { new Dictionary { { "ZED_TERM", "1" } }, "zed" }, - { new Dictionary { { "KIMI_CLI", "true" } }, "kimi" }, - { new Dictionary { { "OR_APP_NAME", "OpenHands" } }, "openhands" }, - { new Dictionary { { "OR_APP_NAME", "openhands" } }, "openhands" }, - { new Dictionary { { "GOOSE_TERMINAL", "1" } }, "goose" }, - { new Dictionary { { "GOOSE_PROVIDER", "openai" } }, "goose" }, - { new Dictionary { { "CLINE_TASK_ID", "task123" } }, "cline" }, - { new Dictionary { { "ROO_CODE_TASK_ID", "task456" } }, "roo" }, - { new Dictionary { { "WINDSURF_SESSION", "session789" } }, "windsurf" }, - { new Dictionary { { "REPL_ID", "repl1" } }, "replit" }, - { new Dictionary { { "AUGMENT_AGENT", "1" } }, "augment" }, - { new Dictionary { { "ANTIGRAVITY_AGENT", "1" } }, "antigravity" }, - { new Dictionary { { "AGENT_CLI", "true" } }, "generic_agent" }, - { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" }, - { new Dictionary { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot" }, - { new Dictionary { { "CLAUDECODE", "1" }, { "GEMINI_CLI", "true" }, { "AGENT_CLI", "true" } }, "claude, gemini, generic_agent" }, - { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" }, { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" }, { "AGENT_CLI", "true" } }, "claude, cursor, gemini, copilot, generic_agent" }, - { new Dictionary { { "OR_APP_NAME", "Aider" }, { "CLINE_TASK_ID", "task123" } }, "aider, cline" }, - { new Dictionary { { "CODEX_CLI", "1" }, { "WINDSURF_SESSION", "session789" } }, "codex, windsurf" }, - { new Dictionary { { "GOOSE_TERMINAL", "1" }, { "ROO_CODE_TASK_ID", "task456" } }, "goose, roo" }, - { new Dictionary { { "GEMINI_CLI", "false" } }, "gemini" }, - { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "false" } }, "copilot" }, - { new Dictionary { { "AGENT_CLI", "false" } }, "generic_agent" }, - { new Dictionary { { "DROID_CLI", "false" } }, "droid" }, - { new Dictionary { { "KIMI_CLI", "false" } }, "kimi" }, - { new Dictionary { { "CLAUDE_CODE_IS_COWORK", "1" }, { "CLAUDE_CODE", "1" } }, "cowork, claude" }, - { new Dictionary { { "OR_APP_NAME", "SomeOtherApp" } }, null }, - { new Dictionary(), null }, + { [("CLAUDECODE", "1")], "claude" }, + { [("CLAUDE_CODE", "1")], "claude" }, + { [("CLAUDE_CODE_ENTRYPOINT", "some_value")], "claude" }, + { [("CLAUDE_CODE_IS_COWORK", "1")], "cowork" }, + { [("CURSOR_EDITOR", "1")], "cursor" }, + { [("CURSOR_AI", "1")], "cursor" }, + { [("CURSOR_TRACE_ID", "abc")], "cursor" }, + { [("CURSOR_AGENT", "1")], "cursor" }, + { [("GEMINI_CLI", "true")], "gemini" }, + { [("GEMINI_CLI", "0")], "gemini" }, + { [("GITHUB_COPILOT_CLI_MODE", "true")], "copilot-cli" }, + { [("GH_COPILOT_WORKING_DIRECTORY", "/repo")], "copilot-cli" }, + { [("COPILOT_CLI", "1")], "copilot-cli" }, + { [("COPILOT_MODEL", "gpt")], "copilot-cli" }, + { [("COPILOT_ALLOW_ALL", "1")], "copilot-cli" }, + { [("COPILOT_GITHUB_TOKEN", "token")], "copilot-cli" }, + { [("AI_AGENT", "github_copilot_vscode_agent")], "copilot-vscode" }, + { [("COPILOT_AGENT", "1")], "copilot-vscode" }, + { [("CODEX_CLI", "1")], "codex" }, + { [("CODEX_SANDBOX", "1")], "codex" }, + { [("CODEX_CI", "1")], "codex" }, + { [("CODEX_THREAD_ID", "thread1")], "codex" }, + { [("OR_APP_NAME", "Aider")], "aider" }, + { [("OR_APP_NAME", "aider")], "aider" }, + { [("OR_APP_NAME", "plandex")], "plandex" }, + { [("OR_APP_NAME", "Plandex")], "plandex" }, + { [("AMP_HOME", "/path/to/amp")], "amp" }, + { [("QWEN_CODE", "1")], "qwen" }, + { [("DROID_CLI", "true")], "droid" }, + { [("OPENCODE_AI", "1")], "opencode" }, + { [("ZED_ENVIRONMENT", "1")], "zed" }, + { [("ZED_TERM", "1")], "zed" }, + { [("KIMI_CLI", "true")], "kimi" }, + { [("OR_APP_NAME", "OpenHands")], "openhands" }, + { [("OR_APP_NAME", "openhands")], "openhands" }, + { [("GOOSE_TERMINAL", "1")], "goose" }, + { [("GOOSE_PROVIDER", "openai")], "goose" }, + { [("CLINE_TASK_ID", "task123")], "cline" }, + { [("ROO_CODE_TASK_ID", "task456")], "roo" }, + { [("WINDSURF_SESSION", "session789")], "windsurf" }, + { [("REPL_ID", "repl1")], "replit" }, + { [("AUGMENT_AGENT", "1")], "augment" }, + { [("ANTIGRAVITY_AGENT", "1")], "antigravity" }, + { [("AGENT_CLI", "true")], "generic_agent" }, + { [("CLAUDECODE", "1"), ("CURSOR_EDITOR", "1") ], "claude, cursor" }, + { [("GEMINI_CLI", "true"), ("GITHUB_COPILOT_CLI_MODE", "true") ], "gemini, copilot-cli" }, + { [("CLAUDECODE", "1"), ("GEMINI_CLI", "true"), ("AGENT_CLI", "true") ], "claude, gemini, generic_agent" }, + { [("CLAUDECODE", "1"), ("CURSOR_EDITOR", "1"), ("GEMINI_CLI", "true"), ("GITHUB_COPILOT_CLI_MODE", "true"), ("AGENT_CLI", "true") ], "claude, cursor, gemini, copilot-cli, generic_agent" }, + { [("OR_APP_NAME", "Aider"), ("CLINE_TASK_ID", "task123") ], "aider, cline" }, + { [("CODEX_CLI", "1"), ("WINDSURF_SESSION", "session789") ], "codex, windsurf" }, + { [("GOOSE_TERMINAL", "1"), ("ROO_CODE_TASK_ID", "task456") ], "goose, roo" }, + { [("GEMINI_CLI", "false")], "gemini" }, + { [("GITHUB_COPILOT_CLI_MODE", "false")], "copilot-cli" }, + { [("AGENT_CLI", "false")], "generic_agent" }, + { [("DROID_CLI", "false")], "droid" }, + { [("KIMI_CLI", "false")], "kimi" }, + { [("CLAUDE_CODE_IS_COWORK", "1"), ("CLAUDE_CODE", "1")], "cowork, claude" }, + { [("OR_APP_NAME", "SomeOtherApp")], null }, + { [("", "")], null } }; }