From 5154ed702c1bd9b7e6203a70e1a393c7fce5b5a3 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 9 Jun 2026 15:21:35 -0700 Subject: [PATCH 1/4] Add coding agent telemetry detection Detect known coding agents from environment variables and include the detected agent name on Aspire CLI main telemetry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Telemetry/AspireCliTelemetry.cs | 16 ++- .../Telemetry/CodingAgentDetector.cs | 96 ++++++++++++++++ .../Telemetry/ICodingAgentDetector.cs | 16 +++ .../Telemetry/TelemetryConstants.cs | 5 + .../TelemetryServiceCollectionExtensions.cs | 1 + .../Telemetry/AspireCliTelemetryTests.cs | 107 +++++++++++++++++- .../Telemetry/TelemetryFixture.cs | 15 ++- .../Telemetry/TestTelemetryHelper.cs | 11 +- 8 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 src/Aspire.Cli/Telemetry/CodingAgentDetector.cs create mode 100644 src/Aspire.Cli/Telemetry/ICodingAgentDetector.cs diff --git a/src/Aspire.Cli/Telemetry/AspireCliTelemetry.cs b/src/Aspire.Cli/Telemetry/AspireCliTelemetry.cs index a6313a06003..80e49cb4613 100644 --- a/src/Aspire.Cli/Telemetry/AspireCliTelemetry.cs +++ b/src/Aspire.Cli/Telemetry/AspireCliTelemetry.cs @@ -46,6 +46,7 @@ internal sealed class AspireCliTelemetry : IHostedService private readonly ActivitySource _reportedActivitySource; private readonly IMachineInformationProvider _machineInformationProvider; private readonly ICIEnvironmentDetector _ciEnvironmentDetector; + private readonly ICodingAgentDetector _codingAgentDetector; private readonly ILogger _logger; private readonly List> _tagsList = []; @@ -57,8 +58,9 @@ internal sealed class AspireCliTelemetry : IHostedService /// The logger instance for recording errors. /// The machine information provider. /// The CI environment detector. - public AspireCliTelemetry(ILogger logger, IMachineInformationProvider machineInformationProvider, ICIEnvironmentDetector ciEnvironmentDetector) - : this(logger, machineInformationProvider, ciEnvironmentDetector, ReportedActivitySourceName, DiagnosticsActivitySourceName) + /// The coding agent detector. + public AspireCliTelemetry(ILogger logger, IMachineInformationProvider machineInformationProvider, ICIEnvironmentDetector ciEnvironmentDetector, ICodingAgentDetector codingAgentDetector) + : this(logger, machineInformationProvider, ciEnvironmentDetector, codingAgentDetector, ReportedActivitySourceName, DiagnosticsActivitySourceName) { } @@ -69,13 +71,15 @@ public AspireCliTelemetry(ILogger logger, IMachineInformatio /// The logger instance for recording errors. /// The machine information provider. /// The CI environment detector. + /// The coding agent detector. /// The name for the reported activity source. /// The name for the diagnostics activity source. - internal AspireCliTelemetry(ILogger logger, IMachineInformationProvider machineInformationProvider, ICIEnvironmentDetector ciEnvironmentDetector, string reportedSourceName, string diagnosticsSourceName) + internal AspireCliTelemetry(ILogger logger, IMachineInformationProvider machineInformationProvider, ICIEnvironmentDetector ciEnvironmentDetector, ICodingAgentDetector codingAgentDetector, string reportedSourceName, string diagnosticsSourceName) { _logger = logger; _machineInformationProvider = machineInformationProvider; _ciEnvironmentDetector = ciEnvironmentDetector; + _codingAgentDetector = codingAgentDetector; _reportedActivitySource = new ActivitySource(reportedSourceName); _diagnosticsActivitySource = new ActivitySource(diagnosticsSourceName); } @@ -225,6 +229,12 @@ internal async Task InitializeAsync() _tagsList.Add(new(TelemetryConstants.Tags.CliVersion, GetCliVersion())); _tagsList.Add(new(TelemetryConstants.Tags.CliBuildId, GetCliBuildId())); + var codingAgent = _codingAgentDetector.GetCodingAgent(); + if (codingAgent is not null) + { + _tagsList.Add(new(TelemetryConstants.Tags.CodingAgent, codingAgent)); + } + _tagsList.Add(new(TelemetryConstants.Tags.DeploymentEnvironmentName, _ciEnvironmentDetector.IsCIEnvironment() ? "ci" : "local")); _tagsList.Add(new(TelemetryConstants.Tags.OsName, GetOsName())); diff --git a/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs new file mode 100644 index 00000000000..78554e3c142 --- /dev/null +++ b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; + +namespace Aspire.Cli.Telemetry; + +/// +/// Detects coding agents from known environment variables. +/// +internal sealed class CodingAgentDetector(IConfiguration configuration) : ICodingAgentDetector +{ + // Keep this in sync with the dotnet CLI's LLMEnvironmentDetectorForTelemetry detection + // order so Aspire reports the same agent names when the same environment variables are set. + private static readonly DetectionRule[] s_detectionRules = + [ + new("cowork", ["CLAUDE_CODE_IS_COWORK"]), + 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"]), + new("codex", ["CODEX_CLI", "CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID"]), + new("aider", ["OR_APP_NAME"], "Aider"), + new("plandex", ["OR_APP_NAME"], "plandex"), + new("amp", ["AMP_HOME"]), + new("qwen", ["QWEN_CODE"]), + new("droid", ["DROID_CLI"]), + new("opencode", ["OPENCODE_AI"]), + new("zed", ["ZED_ENVIRONMENT", "ZED_TERM"]), + new("kimi", ["KIMI_CLI"]), + new("openhands", ["OR_APP_NAME"], "OpenHands"), + new("goose", ["GOOSE_TERMINAL", "GOOSE_PROVIDER"]), + new("cline", ["CLINE_TASK_ID"]), + new("roo", ["ROO_CODE_TASK_ID"]), + new("windsurf", ["WINDSURF_SESSION"]), + new("replit", ["REPL_ID"]), + new("augment", ["AUGMENT_AGENT"]), + new("antigravity", ["ANTIGRAVITY_AGENT"]), + new("generic_agent", ["AGENT_CLI"]) + ]; + + private readonly IConfiguration _configuration = configuration; + + /// + public string? GetCodingAgent() + { + List? agentNames = null; + + foreach (var rule in s_detectionRules) + { + if (rule.IsMatch(_configuration)) + { + agentNames ??= []; + agentNames.Add(rule.AgentName); + } + } + + return agentNames is { Count: > 0 } ? string.Join(", ", agentNames) : null; + } + + private sealed class DetectionRule + { + private readonly string[] _variableNames; + private readonly string? _expectedValue; + + public DetectionRule(string agentName, string[] variableNames, string? expectedValue = null) + { + AgentName = agentName; + _variableNames = variableNames; + _expectedValue = expectedValue; + } + + public string AgentName { get; } + + public bool IsMatch(IConfiguration configuration) + { + foreach (var variableName in _variableNames) + { + var value = configuration[variableName]; + if (_expectedValue is null) + { + if (!string.IsNullOrEmpty(value)) + { + return true; + } + } + else if (string.Equals(value, _expectedValue, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Aspire.Cli/Telemetry/ICodingAgentDetector.cs b/src/Aspire.Cli/Telemetry/ICodingAgentDetector.cs new file mode 100644 index 00000000000..b552ab754f3 --- /dev/null +++ b/src/Aspire.Cli/Telemetry/ICodingAgentDetector.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Cli.Telemetry; + +/// +/// Detects whether the CLI is running under a known coding agent. +/// +internal interface ICodingAgentDetector +{ + /// + /// Gets the detected coding agent name, or names, for the current environment. + /// + /// The detected coding agent names, or when none are detected. + string? GetCodingAgent(); +} diff --git a/src/Aspire.Cli/Telemetry/TelemetryConstants.cs b/src/Aspire.Cli/Telemetry/TelemetryConstants.cs index f52808223c8..2f20f443c7c 100644 --- a/src/Aspire.Cli/Telemetry/TelemetryConstants.cs +++ b/src/Aspire.Cli/Telemetry/TelemetryConstants.cs @@ -73,6 +73,11 @@ internal static class Tags /// public const string CliBuildId = "aspire.cli.build_id"; + /// + /// Tag for the detected coding agent that invoked the CLI process. + /// + public const string CodingAgent = "process.coding_agent"; + /// /// Tag for the deployment environment name ("ci" or "local"). /// diff --git a/src/Aspire.Cli/Telemetry/TelemetryServiceCollectionExtensions.cs b/src/Aspire.Cli/Telemetry/TelemetryServiceCollectionExtensions.cs index ef96eef5319..44d260bd430 100644 --- a/src/Aspire.Cli/Telemetry/TelemetryServiceCollectionExtensions.cs +++ b/src/Aspire.Cli/Telemetry/TelemetryServiceCollectionExtensions.cs @@ -36,6 +36,7 @@ public static IServiceCollection AddTelemetryServices(this IServiceCollection se } services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddHostedService(sp => sp.GetRequiredService()); diff --git a/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs b/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs index d39fc709f6d..6d4669a2335 100644 --- a/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs +++ b/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.InternalTesting; using System.Diagnostics; using Aspire.Cli.Telemetry; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; @@ -238,6 +239,44 @@ public void InitializeAsync_AddsOsInformationTags() Assert.Contains(tags, t => t.Key == TelemetryConstants.Tags.OsType && (string?)t.Value == expectedOsType); } + [Fact] + public void InitializeAsync_AddsCodingAgentTag_WhenCodingAgentIsDetected() + { + var codingAgentDetector = new TelemetryFixture.TestCodingAgentDetector + { + CodingAgent = "copilot" + }; + using var fixture = new TelemetryFixture(codingAgentDetector: codingAgentDetector, sampleResult: ActivitySamplingResult.AllData); + + using var activity = fixture.Telemetry.StartReportedActivity(TelemetryConstants.Activities.Main); + + Assert.NotNull(activity); + Assert.Equal("copilot", activity.GetTagItem(TelemetryConstants.Tags.CodingAgent)); + } + + [Fact] + public void InitializeAsync_DoesNotAddCodingAgentTag_WhenCodingAgentIsNotDetected() + { + using var fixture = new TelemetryFixture(); + + var tags = fixture.Telemetry.GetDefaultTags(); + + Assert.DoesNotContain(tags, t => t.Key == TelemetryConstants.Tags.CodingAgent); + } + + [Theory] + [MemberData(nameof(CodingAgentTelemetryTestCases))] + public void CodingAgentDetector_DetectsKnownCodingAgents(Dictionary environmentVariables, string? expectedCodingAgent) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(environmentVariables) + .Build(); + + var detector = new CodingAgentDetector(configuration); + + Assert.Equal(expectedCodingAgent, detector.GetCodingAgent()); + } + [Fact] public void StartReportedActivity_IncludesAllDefaultTags() { @@ -267,7 +306,8 @@ public void StartReportedActivity_ThrowsIfNotInitialized() { var provider = new TelemetryFixture.TestMachineInformationProvider(); var ciDetector = new TelemetryFixture.TestCIEnvironmentDetector(); - var telemetry = new AspireCliTelemetry(NullLogger.Instance, provider, ciDetector); + var codingAgentDetector = new TelemetryFixture.TestCodingAgentDetector(); + var telemetry = new AspireCliTelemetry(NullLogger.Instance, provider, ciDetector, codingAgentDetector); var exception = Assert.Throws(() => telemetry.StartReportedActivity("test")); Assert.Contains("not been initialized", exception.Message); @@ -278,7 +318,8 @@ public async Task InitializeAsync_IsIdempotent() { var provider = new TelemetryFixture.TestMachineInformationProvider(); var ciDetector = new TelemetryFixture.TestCIEnvironmentDetector(); - var telemetry = new AspireCliTelemetry(NullLogger.Instance, provider, ciDetector); + var codingAgentDetector = new TelemetryFixture.TestCodingAgentDetector(); + var telemetry = new AspireCliTelemetry(NullLogger.Instance, provider, ciDetector, codingAgentDetector); await telemetry.InitializeAsync().DefaultTimeout(); var tagsAfterFirstInit = telemetry.GetDefaultTags().Count; @@ -287,4 +328,66 @@ public async Task InitializeAsync_IsIdempotent() var tags = telemetry.GetDefaultTags(); Assert.Equal(tagsAfterFirstInit, tags.Count); // Should have the same number of tags after second init } + + public static TheoryData, 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 }, + }; } diff --git a/tests/Aspire.Cli.Tests/Telemetry/TelemetryFixture.cs b/tests/Aspire.Cli.Tests/Telemetry/TelemetryFixture.cs index ffa66ce038e..7b5dcfab823 100644 --- a/tests/Aspire.Cli.Tests/Telemetry/TelemetryFixture.cs +++ b/tests/Aspire.Cli.Tests/Telemetry/TelemetryFixture.cs @@ -21,11 +21,13 @@ internal sealed class TelemetryFixture : IDisposable /// /// Optional machine information provider. Uses a default test provider if not specified. /// Optional CI environment detector. Uses a default test detector if not specified. + /// Optional coding agent detector. Uses a default test detector if not specified. /// Optional logger. Uses if not specified. /// The sampling result for the activity listener. Defaults to . public TelemetryFixture( IMachineInformationProvider? machineInfoProvider = null, ICIEnvironmentDetector? ciEnvironmentDetector = null, + ICodingAgentDetector? codingAgentDetector = null, ILogger? logger = null, ActivitySamplingResult sampleResult = ActivitySamplingResult.AllDataAndRecorded) { @@ -42,9 +44,10 @@ public TelemetryFixture( machineInfoProvider ??= new TestMachineInformationProvider(); ciEnvironmentDetector ??= new TestCIEnvironmentDetector(); + codingAgentDetector ??= new TestCodingAgentDetector(); logger ??= NullLogger.Instance; - Telemetry = new AspireCliTelemetry(logger, machineInfoProvider, ciEnvironmentDetector, ReportedSourceName, DiagnosticsSourceName); + Telemetry = new AspireCliTelemetry(logger, machineInfoProvider, ciEnvironmentDetector, codingAgentDetector, ReportedSourceName, DiagnosticsSourceName); Telemetry.InitializeAsync().GetAwaiter().GetResult(); } @@ -94,4 +97,14 @@ internal sealed class TestCIEnvironmentDetector : ICIEnvironmentDetector public bool IsCIEnvironment() => IsCIEnvironmentResult; } + + /// + /// A test implementation of with configurable result. + /// + internal sealed class TestCodingAgentDetector : ICodingAgentDetector + { + public string? CodingAgent { get; set; } + + public string? GetCodingAgent() => CodingAgent; + } } diff --git a/tests/Aspire.Cli.Tests/Telemetry/TestTelemetryHelper.cs b/tests/Aspire.Cli.Tests/Telemetry/TestTelemetryHelper.cs index 446db70a6e5..e6f5b587f48 100644 --- a/tests/Aspire.Cli.Tests/Telemetry/TestTelemetryHelper.cs +++ b/tests/Aspire.Cli.Tests/Telemetry/TestTelemetryHelper.cs @@ -18,7 +18,8 @@ public static AspireCliTelemetry CreateInitializedTelemetry() { var provider = new TestMachineInformationProvider(); var ciDetector = new TestCIEnvironmentDetector(); - var telemetry = new AspireCliTelemetry(NullLogger.Instance, provider, ciDetector); + var codingAgentDetector = new TestCodingAgentDetector(); + var telemetry = new AspireCliTelemetry(NullLogger.Instance, provider, ciDetector, codingAgentDetector); telemetry.InitializeAsync().GetAwaiter().GetResult(); return telemetry; } @@ -30,7 +31,8 @@ public static AspireCliTelemetry CreateInitializedTelemetry(string reportedSourc { var provider = new TestMachineInformationProvider(); var ciDetector = new TestCIEnvironmentDetector(); - var telemetry = new AspireCliTelemetry(NullLogger.Instance, provider, ciDetector, reportedSourceName, diagnosticsSourceName); + var codingAgentDetector = new TestCodingAgentDetector(); + var telemetry = new AspireCliTelemetry(NullLogger.Instance, provider, ciDetector, codingAgentDetector, reportedSourceName, diagnosticsSourceName); telemetry.InitializeAsync().GetAwaiter().GetResult(); return telemetry; } @@ -45,4 +47,9 @@ private sealed class TestCIEnvironmentDetector : ICIEnvironmentDetector { public bool IsCIEnvironment() => false; } + + private sealed class TestCodingAgentDetector : ICodingAgentDetector + { + public string? GetCodingAgent() => null; + } } From 752ed2ec96447b114c2537da4fbc0203f2c1fc0d Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 9 Jun 2026 15:47:35 -0700 Subject: [PATCH 2/4] Update src/Aspire.Cli/Telemetry/CodingAgentDetector.cs --- src/Aspire.Cli/Telemetry/CodingAgentDetector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs index 78554e3c142..782e7371eb6 100644 --- a/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs +++ b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs @@ -12,6 +12,7 @@ internal sealed class CodingAgentDetector(IConfiguration configuration) : ICodin { // Keep this in sync with the dotnet CLI's LLMEnvironmentDetectorForTelemetry detection // order so Aspire reports the same agent names when the same environment variables are set. + // https://github.com/dotnet/sdk/blob/main/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs private static readonly DetectionRule[] s_detectionRules = [ new("cowork", ["CLAUDE_CODE_IS_COWORK"]), From 1aa6bc53b32ec564b3bca2257c8161530256d744 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 10 Jun 2026 12:01:44 -0700 Subject: [PATCH 3/4] Report copilot-cli specifically & update tests to work in test explorer --- .../Telemetry/CodingAgentDetector.cs | 2 +- .../Telemetry/AspireCliTelemetryTests.cs | 122 +++++++++--------- 2 files changed, 61 insertions(+), 63 deletions(-) diff --git a/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs index 782e7371eb6..37207207e6e 100644 --- a/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs +++ b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs @@ -19,7 +19,7 @@ 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"]), + new("copilot-cli", ["COPILOT_CLI", "GITHUB_COPILOT_CLI_MODE", "GH_COPILOT_WORKING_DIRECTORY", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", "COPILOT_GITHUB_TOKEN"]), new("codex", ["CODEX_CLI", "CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID"]), new("aider", ["OR_APP_NAME"], "Aider"), new("plandex", ["OR_APP_NAME"], "plandex"), diff --git a/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs b/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs index 6d4669a2335..bac68877a0a 100644 --- a/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs +++ b/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs @@ -266,8 +266,14 @@ public void InitializeAsync_DoesNotAddCodingAgentTag_WhenCodingAgentIsNotDetecte [Theory] [MemberData(nameof(CodingAgentTelemetryTestCases))] - public void CodingAgentDetector_DetectsKnownCodingAgents(Dictionary environmentVariables, string? expectedCodingAgent) + public void CodingAgentDetector_DetectsKnownCodingAgents((string, string?) environmentVariable, string? expectedCodingAgent) { + var environmentVariables = new Dictionary(); + if (environmentVariable.Item1.Length > 0) + { + environmentVariables.Add(environmentVariable.Item1, environmentVariable.Item2); + } + var configuration = new ConfigurationBuilder() .AddInMemoryCollection(environmentVariables) .Build(); @@ -329,65 +335,57 @@ 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() - { - { 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 }, - }; + public static TheoryData<(string, string?), string?> CodingAgentTelemetryTestCases => + [ + new(("CLAUDECODE", "1"), "claude"), + new(("CLAUDE_CODE", "1"), "claude"), + new(("CLAUDE_CODE_ENTRYPOINT", "some_value"), "claude"), + new(("CLAUDE_CODE_IS_COWORK", "1"), "cowork"), + new(("CURSOR_EDITOR", "1"), "cursor"), + new(("CURSOR_AI", "1"), "cursor"), + new(("CURSOR_TRACE_ID", "abc"), "cursor"), + new(("CURSOR_AGENT", "1"), "cursor"), + new(("GEMINI_CLI", "true"), "gemini"), + new(("GEMINI_CLI", "0"), "gemini"), + new(("GITHUB_COPILOT_CLI_MODE", "true"), "copilot-cli"), + new(("GH_COPILOT_WORKING_DIRECTORY", "/repo"), "copilot-cli"), + new(("COPILOT_CLI", "1"), "copilot-cli"), + new(("COPILOT_AGENT", "1"), null), + new(("COPILOT_MODEL", "gpt"), "copilot-cli"), + new(("COPILOT_ALLOW_ALL", "1"), "copilot-cli"), + new(("COPILOT_GITHUB_TOKEN", "token"), "copilot-cli"), + new(("CODEX_CLI", "1"), "codex"), + new(("CODEX_SANDBOX", "1"), "codex"), + new(("CODEX_CI", "1"), "codex"), + new(("CODEX_THREAD_ID", "thread1"), "codex"), + new(("OR_APP_NAME", "Aider"), "aider"), + new(("OR_APP_NAME", "aider"), "aider"), + new(("OR_APP_NAME", "plandex"), "plandex"), + new(("OR_APP_NAME", "Plandex"), "plandex"), + new(("AMP_HOME", "/path/to/amp"), "amp"), + new(("QWEN_CODE", "1"), "qwen"), + new(("DROID_CLI", "true"), "droid"), + new(("OPENCODE_AI", "1"), "opencode"), + new(("ZED_ENVIRONMENT", "1"), "zed"), + new(("ZED_TERM", "1"), "zed"), + new(("KIMI_CLI", "true"), "kimi"), + new(("OR_APP_NAME", "OpenHands"), "openhands"), + new(("OR_APP_NAME", "openhands"), "openhands"), + new(("GOOSE_TERMINAL", "1"), "goose"), + new(("GOOSE_PROVIDER", "openai"), "goose"), + new(("CLINE_TASK_ID", "task123"), "cline"), + new(("ROO_CODE_TASK_ID", "task456"), "roo"), + new(("WINDSURF_SESSION", "session789"), "windsurf"), + new(("REPL_ID", "repl1"), "replit"), + new(("AUGMENT_AGENT", "1"), "augment"), + new(("ANTIGRAVITY_AGENT", "1"), "antigravity"), + new(("AGENT_CLI", "true"), "generic_agent"), + new(("GEMINI_CLI", "false"), "gemini"), + new(("GITHUB_COPILOT_CLI_MODE", "false"), "copilot-cli"), + new(("AGENT_CLI", "false"), "generic_agent"), + new(("DROID_CLI", "false"), "droid"), + new(("KIMI_CLI", "false"), "kimi"), + new(("OR_APP_NAME", "SomeOtherApp"), null), + new(("",""), null), + ]; } From 1f338baa3dee960c6d9ebcfc78b836aa2b40bac4 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 10 Jun 2026 12:39:38 -0700 Subject: [PATCH 4/4] Enhance CodingAgentDetector to support GitHub Copilot CLI and VS Code agents; update tests for new detection logic --- .../Telemetry/CodingAgentDetector.cs | 20 ++- .../Telemetry/AspireCliTelemetryTests.cs | 128 ++++++++++-------- 2 files changed, 79 insertions(+), 69 deletions(-) diff --git a/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs b/src/Aspire.Cli/Telemetry/CodingAgentDetector.cs index 37207207e6e..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"]), + // 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 bac68877a0a..9a9772df1fe 100644 --- a/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs +++ b/tests/Aspire.Cli.Tests/Telemetry/AspireCliTelemetryTests.cs @@ -266,16 +266,19 @@ public void InitializeAsync_DoesNotAddCodingAgentTag_WhenCodingAgentIsNotDetecte [Theory] [MemberData(nameof(CodingAgentTelemetryTestCases))] - public void CodingAgentDetector_DetectsKnownCodingAgents((string, string?) environmentVariable, string? expectedCodingAgent) + public void CodingAgentDetector_DetectsKnownCodingAgents((string, string?)[] environmentVariables, string? expectedCodingAgent) { - var environmentVariables = new Dictionary(); - if (environmentVariable.Item1.Length > 0) + var configurationValues = new Dictionary(); + foreach (var environmentVariable in environmentVariables) { - environmentVariables.Add(environmentVariable.Item1, environmentVariable.Item2); + 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); @@ -335,57 +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, string?), string?> CodingAgentTelemetryTestCases => - [ - new(("CLAUDECODE", "1"), "claude"), - new(("CLAUDE_CODE", "1"), "claude"), - new(("CLAUDE_CODE_ENTRYPOINT", "some_value"), "claude"), - new(("CLAUDE_CODE_IS_COWORK", "1"), "cowork"), - new(("CURSOR_EDITOR", "1"), "cursor"), - new(("CURSOR_AI", "1"), "cursor"), - new(("CURSOR_TRACE_ID", "abc"), "cursor"), - new(("CURSOR_AGENT", "1"), "cursor"), - new(("GEMINI_CLI", "true"), "gemini"), - new(("GEMINI_CLI", "0"), "gemini"), - new(("GITHUB_COPILOT_CLI_MODE", "true"), "copilot-cli"), - new(("GH_COPILOT_WORKING_DIRECTORY", "/repo"), "copilot-cli"), - new(("COPILOT_CLI", "1"), "copilot-cli"), - new(("COPILOT_AGENT", "1"), null), - new(("COPILOT_MODEL", "gpt"), "copilot-cli"), - new(("COPILOT_ALLOW_ALL", "1"), "copilot-cli"), - new(("COPILOT_GITHUB_TOKEN", "token"), "copilot-cli"), - new(("CODEX_CLI", "1"), "codex"), - new(("CODEX_SANDBOX", "1"), "codex"), - new(("CODEX_CI", "1"), "codex"), - new(("CODEX_THREAD_ID", "thread1"), "codex"), - new(("OR_APP_NAME", "Aider"), "aider"), - new(("OR_APP_NAME", "aider"), "aider"), - new(("OR_APP_NAME", "plandex"), "plandex"), - new(("OR_APP_NAME", "Plandex"), "plandex"), - new(("AMP_HOME", "/path/to/amp"), "amp"), - new(("QWEN_CODE", "1"), "qwen"), - new(("DROID_CLI", "true"), "droid"), - new(("OPENCODE_AI", "1"), "opencode"), - new(("ZED_ENVIRONMENT", "1"), "zed"), - new(("ZED_TERM", "1"), "zed"), - new(("KIMI_CLI", "true"), "kimi"), - new(("OR_APP_NAME", "OpenHands"), "openhands"), - new(("OR_APP_NAME", "openhands"), "openhands"), - new(("GOOSE_TERMINAL", "1"), "goose"), - new(("GOOSE_PROVIDER", "openai"), "goose"), - new(("CLINE_TASK_ID", "task123"), "cline"), - new(("ROO_CODE_TASK_ID", "task456"), "roo"), - new(("WINDSURF_SESSION", "session789"), "windsurf"), - new(("REPL_ID", "repl1"), "replit"), - new(("AUGMENT_AGENT", "1"), "augment"), - new(("ANTIGRAVITY_AGENT", "1"), "antigravity"), - new(("AGENT_CLI", "true"), "generic_agent"), - new(("GEMINI_CLI", "false"), "gemini"), - new(("GITHUB_COPILOT_CLI_MODE", "false"), "copilot-cli"), - new(("AGENT_CLI", "false"), "generic_agent"), - new(("DROID_CLI", "false"), "droid"), - new(("KIMI_CLI", "false"), "kimi"), - new(("OR_APP_NAME", "SomeOtherApp"), null), - new(("",""), null), - ]; + public static TheoryData<(string, string?)[], string?> CodingAgentTelemetryTestCases => new() + { + { [("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 } + }; }