File: Telemetry\CodingAgentDetector.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// 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;
 
/// <summary>
/// Detects coding agents from known environment variables.
/// </summary>
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.
    // 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"]),
        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"),
        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;
 
    /// <inheritdoc />
    public string? GetCodingAgent()
    {
        List<string>? 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(string agentName, string[] variableNames, string? expectedValue = null)
    {
        private readonly string[] _variableNames = variableNames;
        private readonly string? _expectedValue = expectedValue;
 
        public string AgentName { get; } = agentName;
 
        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;
        }
    }
}