completers/copilot_completer/copilot_completer.ps1

<#
.SYNOPSIS
Registers a native PowerShell argument completer for GitHub Copilot CLI.

.DESCRIPTION
Provides standalone completion for `copilot` and `copilot.exe` using a hybrid
model:

- static command, subcommand, and option metadata
- dynamic model discovery from `copilot help config`
- dynamic marketplace and installed-plugin discovery from local plugin commands
- path completion for directory and file-bearing values
- placeholder completions for freeform values to avoid irrelevant filesystem fallback

Load this script once per session, or dot-source it from your PowerShell profile.
#>


Set-StrictMode -Version Latest

if (-not (Get-Variable -Name CopilotCompletionCache -Scope Script -ErrorAction Ignore)) {
    $script:CopilotCompletionCache = @{
        ExecutablePath             = $null
        ExecutablePathProbed       = $false
        Models                     = @()
        ModelsLoadedAt             = $null
        Marketplaces               = @()
        MarketplacesLoadedAt       = $null
        InstalledPlugins           = @()
        InstalledPluginsLoadedAt   = $null
        ModelCacheTtlSeconds       = 300
        RuntimeCacheTtlSeconds     = 60
        HelpTopics                 = @('billing', 'commands', 'config', 'environment', 'limits', 'logging', 'monitoring', 'permissions', 'providers', 'sandbox')
        GlobalOptions              = $null
        CommandSpecs               = $null
    }
}

function New-CopilotCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ResultType = 'ParameterValue',
        [string]$ToolTip,
        [string]$ListItemText
    )

    if ([string]::IsNullOrWhiteSpace($ListItemText)) {
        $ListItemText = $CompletionText
    }

    if ([string]::IsNullOrWhiteSpace($ToolTip)) {
        $ToolTip = $ListItemText
    }

    [System.Management.Automation.CompletionResult]::new(
        $CompletionText,
        $ListItemText,
        $ResultType,
        $ToolTip
    )
}

function Get-CopilotUniqueStrings {
    param([string[]]$Items)

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    $results = New-Object System.Collections.Generic.List[string]

    foreach ($item in @($Items)) {
        if ([string]::IsNullOrWhiteSpace($item)) {
            continue
        }

        if ($seen.Add($item)) {
            [void]$results.Add($item)
        }
    }

    @($results.ToArray())
}

function New-CopilotOptionSpec {
    param(
        [string[]]$Tokens,
        [string]$Description,
        [string]$ValueKind,
        [switch]$OptionalValue
    )

    foreach ($token in @($Tokens)) {
        [pscustomobject]@{
            Token         = $token
            Description   = $Description
            ValueKind     = $ValueKind
            OptionalValue = [bool]$OptionalValue
        }
    }
}

function ConvertTo-CopilotQuotedValue {
    param(
        [string]$Value,
        [bool]$AlwaysQuote = $false
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $Value
    }

    if (($AlwaysQuote -or $Value -match '\s') -and -not ($Value.StartsWith('"') -and $Value.EndsWith('"'))) {
        return '"' + ($Value.Replace('`', '``').Replace('"', '`"')) + '"'
    }

    $Value
}

function Get-CopilotTokenText {
    param([System.Management.Automation.Language.Ast]$Element)

    if ($Element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
        return $Element.Value
    }

    if ($Element -is [System.Management.Automation.Language.CommandParameterAst]) {
        return $Element.Extent.Text
    }

    $Element.Extent.Text
}

function Resolve-CopilotExecutablePath {
    if ($script:CopilotCompletionCache.ExecutablePathProbed) {
        return $script:CopilotCompletionCache.ExecutablePath
    }

    $script:CopilotCompletionCache.ExecutablePathProbed = $true
    $script:CopilotCompletionCache.ExecutablePath = $null

    foreach ($commandName in @('copilot.exe', 'copilot')) {
        $command = Get-Command -Name $commandName -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($command) {
            $script:CopilotCompletionCache.ExecutablePath = if ($command.Source) { $command.Source } else { $command.Name }
            break
        }
    }

    $script:CopilotCompletionCache.ExecutablePath
}

function Invoke-CopilotCapture {
    param([string[]]$Arguments)

    $executablePath = Resolve-CopilotExecutablePath
    if ([string]::IsNullOrWhiteSpace($executablePath)) {
        return @()
    }

    try {
        @(& $executablePath @Arguments 2>$null | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' })
    } catch {
        @()
    }
}

function Test-CopilotCacheFresh {
    param(
        [nullable[datetime]]$LoadedAt,
        [int]$TtlSeconds
    )

    if ($null -eq $LoadedAt) {
        return $false
    }

    ((Get-Date) - $LoadedAt).TotalSeconds -lt $TtlSeconds
}

function Get-CopilotModels {
    if (Test-CopilotCacheFresh -LoadedAt $script:CopilotCompletionCache.ModelsLoadedAt -TtlSeconds $script:CopilotCompletionCache.ModelCacheTtlSeconds) {
        return $script:CopilotCompletionCache.Models
    }

    $lines = Invoke-CopilotCapture -Arguments @('help', 'config')
    $models = New-Object System.Collections.Generic.List[string]
    $inModelSection = $false

    foreach ($line in @($lines)) {
        if ($line -match '^\s*`model`:\s') {
            $inModelSection = $true
            continue
        }

        if ($inModelSection -and $line -match '^\s*`[^`]+`:\s') {
            break
        }

        if ($inModelSection -and $line -match '^\s*-\s+"([^"]+)"') {
            [void]$models.Add($matches[1])
        }
    }

    $script:CopilotCompletionCache.Models = Get-CopilotUniqueStrings -Items @($models.ToArray())
    $script:CopilotCompletionCache.ModelsLoadedAt = Get-Date
    $script:CopilotCompletionCache.Models
}

function Get-CopilotMarketplaceNames {
    if (Test-CopilotCacheFresh -LoadedAt $script:CopilotCompletionCache.MarketplacesLoadedAt -TtlSeconds $script:CopilotCompletionCache.RuntimeCacheTtlSeconds) {
        return $script:CopilotCompletionCache.Marketplaces
    }

    $lines = Invoke-CopilotCapture -Arguments @('plugin', 'marketplace', 'list')
    $names = foreach ($line in @($lines)) {
        if ($line -match '^\s+\S+\s+([A-Za-z0-9._-]+)(?:\s+\(|$)') {
            $matches[1]
        }
    }

    $script:CopilotCompletionCache.Marketplaces = Get-CopilotUniqueStrings -Items $names
    $script:CopilotCompletionCache.MarketplacesLoadedAt = Get-Date
    $script:CopilotCompletionCache.Marketplaces
}

function Get-CopilotInstalledPluginNames {
    if (Test-CopilotCacheFresh -LoadedAt $script:CopilotCompletionCache.InstalledPluginsLoadedAt -TtlSeconds $script:CopilotCompletionCache.RuntimeCacheTtlSeconds) {
        return $script:CopilotCompletionCache.InstalledPlugins
    }

    $lines = Invoke-CopilotCapture -Arguments @('plugin', 'list')
    $names = foreach ($line in @($lines)) {
        if ($line -match '^\s+\S+\s+(.+?)(?:\s+\(.*\))?$') {
            $matches[1]
        }
    }

    $script:CopilotCompletionCache.InstalledPlugins = Get-CopilotUniqueStrings -Items $names
    $script:CopilotCompletionCache.InstalledPluginsLoadedAt = Get-Date
    $script:CopilotCompletionCache.InstalledPlugins
}

function Initialize-CopilotStaticMetadata {
    if ($script:CopilotCompletionCache.GlobalOptions -and $script:CopilotCompletionCache.CommandSpecs) {
        return
    }

    $script:CopilotCompletionCache.GlobalOptions = @(
        New-CopilotOptionSpec -Tokens @('--effort', '--reasoning-effort') -Description 'Set the reasoning effort level.' -ValueKind 'ReasoningEffort'
        New-CopilotOptionSpec -Tokens @('--acp') -Description 'Start as Agent Client Protocol server.'
        New-CopilotOptionSpec -Tokens @('--add-dir') -Description 'Add a directory to the allowed list for file access.' -ValueKind 'DirectoryPath'
        New-CopilotOptionSpec -Tokens @('--add-github-mcp-tool') -Description 'Enable an additional GitHub MCP tool.' -ValueKind 'GithubMcpTool'
        New-CopilotOptionSpec -Tokens @('--add-github-mcp-toolset') -Description 'Enable an additional GitHub MCP toolset.' -ValueKind 'GithubMcpToolset'
        New-CopilotOptionSpec -Tokens @('--additional-mcp-config') -Description 'Additional MCP config JSON or @file path.' -ValueKind 'AdditionalMcpConfig'
        New-CopilotOptionSpec -Tokens @('--agent') -Description 'Specify a custom agent to use.' -ValueKind 'AgentName'
        New-CopilotOptionSpec -Tokens @('--allow-all') -Description 'Enable all permissions.'
        New-CopilotOptionSpec -Tokens @('--allow-all-mcp-server-instructions') -Description 'Include initialization instructions from all MCP servers in the system prompt.'
        New-CopilotOptionSpec -Tokens @('--allow-all-paths') -Description 'Allow access to any file path.'
        New-CopilotOptionSpec -Tokens @('--allow-all-tools') -Description 'Allow all tools to run automatically.'
        New-CopilotOptionSpec -Tokens @('--allow-all-urls') -Description 'Allow access to all URLs without confirmation.'
        New-CopilotOptionSpec -Tokens @('--allow-tool') -Description 'Grant permission to a specific tool or tool pattern.' -ValueKind 'ToolPattern' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--allow-url') -Description 'Grant permission to a specific URL or domain.' -ValueKind 'UrlPattern' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--assisted-approval') -Description 'Review tool permission requests with the assisted-approval safety judge instead of approving them outright.'
        New-CopilotOptionSpec -Tokens @('--attachment') -Description 'Attach a file (image or native document) to the initial prompt.' -ValueKind 'FilePath'
        New-CopilotOptionSpec -Tokens @('--autopilot') -Description 'Enable autopilot continuation in prompt mode.'
        New-CopilotOptionSpec -Tokens @('--available-tools') -Description 'Only these tools will be available to the model.' -ValueKind 'ToolPattern' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--banner') -Description 'Show the startup banner.'
        New-CopilotOptionSpec -Tokens @('--bash-env') -Description 'Enable BASH_ENV support for bash shells.' -ValueKind 'OnOff' -OptionalValue
        New-CopilotOptionSpec -Tokens @('-C') -Description 'Change working directory before doing anything else.' -ValueKind 'DirectoryPath'
        New-CopilotOptionSpec -Tokens @('--connect') -Description 'Connect directly to a remote session (optional session or task ID).' -ValueKind 'ResumeSession' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--context') -Description 'Set the context window tier.' -ValueKind 'ContextTier'
        New-CopilotOptionSpec -Tokens @('--continue') -Description 'Resume the most recent session.'
        New-CopilotOptionSpec -Tokens @('--deny-tool') -Description 'Deny a tool or tool pattern.' -ValueKind 'ToolPattern' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--deny-url') -Description 'Deny a URL or domain.' -ValueKind 'UrlPattern' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--disable-builtin-mcps') -Description 'Disable all built-in MCP servers.'
        New-CopilotOptionSpec -Tokens @('--disable-mcp-server') -Description 'Disable a specific MCP server.' -ValueKind 'ServerName'
        New-CopilotOptionSpec -Tokens @('--disallow-temp-dir') -Description 'Prevent automatic access to the system temp directory.'
        New-CopilotOptionSpec -Tokens @('--enable-all-github-mcp-tools') -Description 'Enable all GitHub MCP server tools.'
        New-CopilotOptionSpec -Tokens @('--enable-mcp-server') -Description 'Enable an MCP server disabled in settings for this run only (can be used multiple times).' -ValueKind 'ServerName'
        New-CopilotOptionSpec -Tokens @('--enable-memory') -Description 'Enable memory in prompt mode.'
        New-CopilotOptionSpec -Tokens @('--excluded-tools') -Description 'These tools will not be available to the model.' -ValueKind 'ToolPattern' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--experimental') -Description 'Enable experimental features.'
        New-CopilotOptionSpec -Tokens @('--extension-sdk-path') -Description 'Override the bundled @github/copilot-sdk with a local copilot-sdk/ folder.' -ValueKind 'DirectoryPath'
        New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
        New-CopilotOptionSpec -Tokens @('-i', '--interactive') -Description 'Start interactive mode and execute this prompt.' -ValueKind 'PromptText'
        New-CopilotOptionSpec -Tokens @('--log-dir') -Description 'Set the log file directory.' -ValueKind 'DirectoryPath'
        New-CopilotOptionSpec -Tokens @('--log-level') -Description 'Set the log level.' -ValueKind 'LogLevel'
        New-CopilotOptionSpec -Tokens @('--max-ai-credits') -Description 'Set max AI credits for this session.' -ValueKind 'Count'
        New-CopilotOptionSpec -Tokens @('--max-autopilot-continues') -Description 'Maximum continuation count in autopilot mode.' -ValueKind 'Count'
        New-CopilotOptionSpec -Tokens @('--mode') -Description 'Set the initial agent mode.' -ValueKind 'AgentMode'
        New-CopilotOptionSpec -Tokens @('--model') -Description 'Set the AI model to use.' -ValueKind 'Model'
        New-CopilotOptionSpec -Tokens @('--mouse') -Description 'Enable mouse support in alt screen mode.' -ValueKind 'OnOff' -OptionalValue
        New-CopilotOptionSpec -Tokens @('-n', '--name') -Description 'Set a name for the new session.' -ValueKind 'SessionName'
        New-CopilotOptionSpec -Tokens @('--no-ask-user') -Description 'Disable the ask_user tool.'
        New-CopilotOptionSpec -Tokens @('--no-auto-update') -Description 'Disable automatic update downloads.'
        New-CopilotOptionSpec -Tokens @('--no-bash-env') -Description 'Disable BASH_ENV support for bash shells.'
        New-CopilotOptionSpec -Tokens @('--no-color') -Description 'Disable all color output.'
        New-CopilotOptionSpec -Tokens @('--no-custom-instructions') -Description 'Disable loading custom instructions files.'
        New-CopilotOptionSpec -Tokens @('--no-eager-powershell-resolution') -Description 'Disable background PowerShell prompt resolution on Windows.'
        New-CopilotOptionSpec -Tokens @('--no-experimental') -Description 'Disable experimental features.'
        New-CopilotOptionSpec -Tokens @('--no-mouse') -Description 'Disable mouse support in alt screen mode.'
        New-CopilotOptionSpec -Tokens @('--no-remote') -Description 'Disable remote control of your session from GitHub web and mobile.'
        New-CopilotOptionSpec -Tokens @('--no-remote-export') -Description 'Disable exporting your session to GitHub web and mobile.'
        New-CopilotOptionSpec -Tokens @('--output-format') -Description 'Set output format.' -ValueKind 'OutputFormat'
        New-CopilotOptionSpec -Tokens @('-p', '--prompt') -Description 'Execute a prompt in non-interactive mode.' -ValueKind 'PromptText'
        New-CopilotOptionSpec -Tokens @('--plain-diff') -Description 'Disable rich diff rendering.'
        New-CopilotOptionSpec -Tokens @('--plan') -Description 'Start in plan mode.'
        New-CopilotOptionSpec -Tokens @('--plugin-dir') -Description 'Load a plugin from a local directory.' -ValueKind 'DirectoryPath'
        New-CopilotOptionSpec -Tokens @('-r', '--resume') -Description 'Resume from a previous session or task ID.' -ValueKind 'ResumeSession' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--remote') -Description 'Enable remote control of your session from GitHub web and mobile.'
        New-CopilotOptionSpec -Tokens @('--remote-export') -Description 'Export your session to GitHub web and mobile (read-only).'
        New-CopilotOptionSpec -Tokens @('-s', '--silent') -Description 'Output only the agent response.'
        New-CopilotOptionSpec -Tokens @('--screen-reader') -Description 'Enable screen reader optimizations.'
        New-CopilotOptionSpec -Tokens @('--secret-env-vars') -Description 'Strip and redact selected environment variable values.' -ValueKind 'EnvVarList' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--session-id') -Description 'Resume an existing session or task by ID, or set the UUID for a new session.' -ValueKind 'ResumeSession'
        New-CopilotOptionSpec -Tokens @('--share') -Description 'Share session to a markdown file after completion.' -ValueKind 'SharePath' -OptionalValue
        New-CopilotOptionSpec -Tokens @('--share-gist') -Description 'Share session to a secret GitHub gist after completion.'
        New-CopilotOptionSpec -Tokens @('--stream') -Description 'Enable or disable streaming mode.' -ValueKind 'OnOff'
        New-CopilotOptionSpec -Tokens @('--usage-output-file') -Description 'Write final usage statistics as JSON to the specified file.' -ValueKind 'FilePath'
        New-CopilotOptionSpec -Tokens @('-v', '--version') -Description 'Show version information.'
        New-CopilotOptionSpec -Tokens @('--yolo') -Description 'Enable all permissions.'
    )

    $helpOnly = @(New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.')
    $pluginKindOptions = @(
        New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
        New-CopilotOptionSpec -Tokens @('--mcp') -Description 'Target an MCP server.'
        New-CopilotOptionSpec -Tokens @('--plugin') -Description 'Target a plugin (default).'
        New-CopilotOptionSpec -Tokens @('--skill') -Description 'Target a skill.'
    )

    $script:CopilotCompletionCache.CommandSpecs = @{
        ''                           = @{
            Commands   = [ordered]@{
                'app'        = 'Open the GitHub Copilot app in the current directory.'
                'completion' = 'Generate a shell completion script.'
                'help'       = 'Display help information.'
                'init'       = 'Initialize Copilot instructions.'
                'login'      = 'Authenticate with Copilot.'
                'mcp'        = 'Manage MCP servers.'
                'plugin'     = 'Manage plugins.'
                'plugins'    = 'Inspect configured plugins across kinds.'
                'skill'      = 'Manage skills.'
                'update'     = 'Download the latest version.'
                'version'    = 'Display version information.'
            }
            Options    = @()
            Positionals = @()
        }
        'app'                        = @{
            Commands   = [ordered]@{}
            Options    = $helpOnly
            Positionals = @()
        }
        'completion'                 = @{
            Commands   = [ordered]@{}
            Options    = $helpOnly
            Positionals = @(
                @{ Name = 'shell'; ValueKind = 'CompletionShell'; Description = 'Target shell.' }
            )
        }
        'mcp'                        = @{
            Commands   = [ordered]@{
                'add'    = 'Add an MCP server.'
                'get'    = 'Show server details.'
                'list'   = 'List configured MCP servers.'
                'remove' = 'Remove an MCP server.'
            }
            Options    = $helpOnly
            Positionals = @()
        }
        'mcp add'                    = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('--env') -Description 'Environment variable (KEY=VALUE, can be repeated).' -ValueKind 'EnvAssignment'
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
                New-CopilotOptionSpec -Tokens @('--header') -Description 'HTTP header for remote servers, can be repeated.' -ValueKind 'HttpHeader'
                New-CopilotOptionSpec -Tokens @('--json') -Description 'Output added config as JSON.'
                New-CopilotOptionSpec -Tokens @('--show-secrets') -Description 'Show full environment variable and header values in output.'
                New-CopilotOptionSpec -Tokens @('--timeout') -Description 'Timeout in milliseconds.' -ValueKind 'Count'
                New-CopilotOptionSpec -Tokens @('--tools') -Description 'Tool filter: "*" for all, comma-separated list, or "" for none.' -ValueKind 'ToolPattern'
                New-CopilotOptionSpec -Tokens @('--transport') -Description 'Server transport.' -ValueKind 'McpTransport'
            )
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'ServerName'; Description = 'Server name.' }
            )
        }
        'mcp get'                    = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
                New-CopilotOptionSpec -Tokens @('--json') -Description 'Output as JSON.'
                New-CopilotOptionSpec -Tokens @('--show-secrets') -Description 'Show full environment variable and header values.'
            )
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'ServerName'; Description = 'Server name.' }
            )
        }
        'mcp list'                   = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
                New-CopilotOptionSpec -Tokens @('--json') -Description 'Output as JSON.'
            )
            Positionals = @()
        }
        'mcp remove'                 = @{
            Commands   = [ordered]@{}
            Options    = $helpOnly
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'ServerName'; Description = 'Server name.' }
            )
        }
        'plugins'                    = @{
            Commands   = [ordered]@{
                'disable'      = 'Disable a configured tool.'
                'enable'       = 'Enable a configured tool.'
                'install'      = 'Install a new tool.'
                'add'          = 'Install a new tool (alias of install).'
                'list'         = 'List configured plugins across kinds.'
                'marketplace'  = 'Manage plugin marketplaces.'
                'marketplaces' = 'Manage plugin marketplaces (alias of marketplace).'
                'remove'       = 'Remove an installed tool.'
                'rm'           = 'Remove an installed tool (alias of remove).'
                'update'       = 'Update a plugin.'
            }
            Options    = $helpOnly
            Positionals = @()
        }
        'plugins disable'            = @{
            Commands   = [ordered]@{}
            Options    = $pluginKindOptions
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'InstalledPlugin'; Description = 'Plugin, MCP server, or skill name.' }
            )
        }
        'plugins enable'             = @{
            Commands   = [ordered]@{}
            Options    = $pluginKindOptions
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'InstalledPlugin'; Description = 'Plugin, MCP server, or skill name.' }
            )
        }
        'plugins install'            = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
                New-CopilotOptionSpec -Tokens @('--mcp') -Description 'Install an MCP server (not supported here).'
                New-CopilotOptionSpec -Tokens @('--plugin') -Description 'Install a plugin (default).'
                New-CopilotOptionSpec -Tokens @('--scope') -Description 'Install target for a file/URL --skill: user (default) or project.' -ValueKind 'InstallScope'
                New-CopilotOptionSpec -Tokens @('--skill') -Description 'Install a skill from a local path or URL.'
            )
            Positionals = @(
                @{ Name = 'spec'; ValueKind = 'PluginSource'; Description = 'Plugin source (plugin@marketplace, owner/repo, URL, or local path), or skill source with --skill.' }
            )
        }
        'plugins list'               = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
                New-CopilotOptionSpec -Tokens @('--json') -Description 'Emit machine-readable JSON instead of grouped text.'
                New-CopilotOptionSpec -Tokens @('--kind') -Description 'Filter by kind (plugin, mcp, skill, instruction, lsp).' -ValueKind 'PluginKind'
                New-CopilotOptionSpec -Tokens @('--scope') -Description 'Filter by scope (user, repository, working-directory, organization, plugin, builtin, unknown).' -ValueKind 'PluginScope'
            )
            Positionals = @()
        }
        'plugins remove'             = @{
            Commands   = [ordered]@{}
            Options    = $pluginKindOptions
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'InstalledPlugin'; Description = 'Plugin, MCP server, skill name, or custom skill directory path.' }
            )
        }
        'plugins update'             = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('--all') -Description 'Update all installed plugins.'
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'InstalledPlugin'; Description = 'Plugin name (plugin-name@marketplace-name).' }
            )
        }
        'skill'                      = @{
            Commands   = [ordered]@{
                'add'    = 'Add a skill from a file, URL, or directory.'
                'list'   = 'List available skills.'
                'remove' = 'Remove a skill or custom skill directory.'
            }
            Options    = $helpOnly
            Positionals = @()
        }
        'skill add'                  = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
                New-CopilotOptionSpec -Tokens @('--project') -Description 'Install the skill into the project''s .github/skills directory.'
            )
            Positionals = @(
                @{ Name = 'source'; ValueKind = 'SkillSource'; Description = 'Skill source (file path, URL, or directory).' }
            )
        }
        'skill list'                 = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
                New-CopilotOptionSpec -Tokens @('--json') -Description 'Output as JSON.'
            )
            Positionals = @()
        }
        'skill remove'               = @{
            Commands   = [ordered]@{}
            Options    = $helpOnly
            Positionals = @(
                @{ Name = 'name-or-directory'; ValueKind = 'SkillSource'; Description = 'Skill name or custom skill directory.' }
            )
        }
        'help'                       = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'topic'; ValueKind = 'HelpTopic'; Description = 'Help topic (e.g. environment).' }
            )
        }
        'init'                       = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @()
        }
        'login'                      = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('--device-code') -Description 'Authenticate using the OAuth device code flow (default in remote or headless environments).'
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
                New-CopilotOptionSpec -Tokens @('--host') -Description 'GitHub host URL.' -ValueKind 'Host'
                New-CopilotOptionSpec -Tokens @('--web-flow') -Description 'Authenticate using the browser (web) flow (default on local desktops).'
                New-CopilotOptionSpec -Tokens @('--with-token') -Description 'Read an authentication token from standard input.'
            )
            Positionals = @()
        }
        'plugin'                     = @{
            Commands   = [ordered]@{
                'install'     = 'Install a plugin.'
                'list'        = 'List installed plugins.'
                'marketplace' = 'Manage plugin marketplaces.'
                'uninstall'   = 'Uninstall a plugin.'
                'update'      = 'Update a plugin.'
            }
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @()
        }
        'plugin install'             = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'source'; ValueKind = 'PluginSource'; Description = 'Plugin source: plugin@marketplace, owner/repo, owner/repo:path, URL, or local path.' }
            )
        }
        'plugin list'                = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @()
        }
        'plugin uninstall'           = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'InstalledPlugin'; Description = 'Installed plugin name.' }
            )
        }
        'plugin update'              = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'InstalledPlugin'; Description = 'Installed plugin name.' }
            )
        }
        'plugin marketplace'         = @{
            Commands   = [ordered]@{
                'add'    = 'Add a marketplace.'
                'browse' = 'Browse plugins in a marketplace.'
                'list'   = 'List registered marketplaces.'
                'remove' = 'Remove a marketplace.'
                'update' = 'Update marketplace plugin catalogs.'
            }
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @()
        }
        'plugin marketplace add'     = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'source'; ValueKind = 'MarketplaceSource'; Description = 'Marketplace source: owner/repo, URL, or local path.' }
            )
        }
        'plugin marketplace browse'  = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'MarketplaceName'; Description = 'Registered marketplace name.' }
            )
        }
        'plugin marketplace list'    = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @()
        }
        'plugin marketplace remove'  = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-f', '--force') -Description 'Force removal even if plugins are installed.'
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'MarketplaceName'; Description = 'Registered marketplace name.' }
            )
        }
        'plugin marketplace update'  = @{
            Commands   = [ordered]@{}
            Options    = $helpOnly
            Positionals = @(
                @{ Name = 'name'; ValueKind = 'MarketplaceName'; Description = 'Marketplace name (omit to update all).' }
            )
        }
        'update'                     = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @(
                @{ Name = 'channel'; ValueKind = 'UpdateChannel'; Description = 'Update channel.' }
            )
        }
        'version'                    = @{
            Commands   = [ordered]@{}
            Options    = @(
                New-CopilotOptionSpec -Tokens @('-h', '--help') -Description 'Display help for command.'
            )
            Positionals = @()
        }
    }

    # 'plugins marketplace' mirrors 'plugin marketplace'; register the command
    # aliases from the live help as extra path keys pointing at the same specs.
    $specs = $script:CopilotCompletionCache.CommandSpecs
    $specs['plugins add'] = $specs['plugins install']
    $specs['plugins rm'] = $specs['plugins remove']
    $specs['plugins marketplace'] = @{
        Commands   = [ordered]@{
            'add'     = 'Add a plugin marketplace.'
            'browse'  = 'Browse plugins in a marketplace.'
            'list'    = 'List registered plugin marketplaces.'
            'ls'      = 'List registered plugin marketplaces (alias of list).'
            'remove'  = 'Remove a plugin marketplace.'
            'rm'      = 'Remove a plugin marketplace (alias of remove).'
            'update'  = 'Refresh marketplace plugin catalogs.'
            'refresh' = 'Refresh marketplace plugin catalogs (alias of update).'
        }
        Options    = $helpOnly
        Positionals = @()
    }
    $specs['plugins marketplaces'] = $specs['plugins marketplace']
    foreach ($pair in @(
            @{ Alias = 'plugins marketplace add'; Target = 'plugin marketplace add' }
            @{ Alias = 'plugins marketplace browse'; Target = 'plugin marketplace browse' }
            @{ Alias = 'plugins marketplace list'; Target = 'plugin marketplace list' }
            @{ Alias = 'plugins marketplace ls'; Target = 'plugin marketplace list' }
            @{ Alias = 'plugins marketplace remove'; Target = 'plugin marketplace remove' }
            @{ Alias = 'plugins marketplace rm'; Target = 'plugin marketplace remove' }
            @{ Alias = 'plugins marketplace update'; Target = 'plugin marketplace update' }
            @{ Alias = 'plugins marketplace refresh'; Target = 'plugin marketplace update' }
        )) {
        $specs[$pair.Alias] = $specs[$pair.Target]
        $specs['plugins marketplaces' + $pair.Alias.Substring('plugins marketplace'.Length)] = $specs[$pair.Target]
    }
}

function Get-CopilotPathKey {
    param([string[]]$Path)

    (($Path | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join ' ')
}

function Get-CopilotCommandSpec {
    param([string[]]$Path)

    Initialize-CopilotStaticMetadata

    $key = Get-CopilotPathKey -Path $Path
    if ($script:CopilotCompletionCache.CommandSpecs.ContainsKey($key)) {
        return $script:CopilotCompletionCache.CommandSpecs[$key]
    }

    # An unknown non-empty path must not inherit the root commands.
    @{
        Commands    = [ordered]@{}
        Options     = @()
        Positionals = @()
    }
}

function Get-CopilotMergedOptionSpecs {
    param([object[]]$Options)

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($option in @($Options)) {
        if ($null -eq $option -or [string]::IsNullOrWhiteSpace($option.Token)) {
            continue
        }

        if ($seen.Add([string]$option.Token)) {
            [void]$results.Add($option)
        }
    }

    @($results.ToArray())
}

function Get-CopilotOptionsForPath {
    param([string[]]$Path)

    Initialize-CopilotStaticMetadata

    if (@($Path).Count -eq 0) {
        return $script:CopilotCompletionCache.GlobalOptions
    }

    $localOptions = @((Get-CopilotCommandSpec -Path $Path).Options)
    @(Get-CopilotMergedOptionSpecs -Options @($script:CopilotCompletionCache.GlobalOptions + $localOptions))
}

function Get-CopilotCommandsForPath {
    param([string[]]$Path)

    (Get-CopilotCommandSpec -Path $Path).Commands
}

function Find-CopilotExactOptionSpec {
    param(
        [string]$Token,
        [string[]]$Path
    )

    foreach ($option in @(Get-CopilotOptionsForPath -Path $Path)) {
        if ($option.Token.Equals($Token, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $option
        }
    }

    $null
}

function Find-CopilotInlineOptionSpec {
    param(
        [string]$Token,
        [string[]]$Path
    )

    $candidates = @(Get-CopilotOptionsForPath -Path $Path) |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_.ValueKind) } |
        Sort-Object { $_.Token.Length } -Descending

    foreach ($option in $candidates) {
        $prefix = $option.Token + '='
        if ($Token.Length -ge $prefix.Length -and $Token.Substring(0, $prefix.Length).Equals($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $option
        }
    }

    $null
}

function Test-CopilotLooksLikeOption {
    param([string]$Token)

    -not [string]::IsNullOrWhiteSpace($Token) -and $Token.StartsWith('-')
}

function Test-CopilotLooksLikePath {
    param([string]$Token)

    if ([string]::IsNullOrWhiteSpace($Token)) {
        return $false
    }

    $cleanToken = $Token.Trim([char[]]@([char]34, [char]39))
    $cleanToken.StartsWith('.') -or
    $cleanToken.StartsWith('~') -or
    $cleanToken.StartsWith('\') -or
    $cleanToken.Contains('\') -or
    $cleanToken -match '^[A-Za-z]:'
}

function Get-CopilotPathCompletions {
    param(
        [string]$InputPath,
        [string]$Prefix = '',
        [switch]$DirectoriesOnly
    )

    $cleanInput = if ([string]::IsNullOrWhiteSpace($InputPath)) { '' } else { $InputPath.Trim('"') }
    $alwaysQuote = -not [string]::IsNullOrEmpty($InputPath) -and $InputPath.StartsWith('"')

    # Normalise before splitting: a trailing separator, '.', '..' and '~' all
    # name the parent directory; Split-Path would resolve '.\' to the cwd name.
    if ($cleanInput -eq '~' -or $cleanInput.StartsWith('~\') -or $cleanInput.StartsWith('~/')) {
        $cleanInput = $HOME + $cleanInput.Substring(1)
    }

    if ([string]::IsNullOrWhiteSpace($cleanInput)) {
        $parent = '.'
        $leaf = ''
    } elseif ($cleanInput.EndsWith('\') -or $cleanInput.EndsWith('/') -or $cleanInput -eq '.' -or $cleanInput -eq '..') {
        $parent = $cleanInput
        $leaf = ''
    } else {
        $parent = [System.IO.Path]::GetDirectoryName($cleanInput)
        if ([string]::IsNullOrWhiteSpace($parent)) {
            $parent = '.'
        }

        $leaf = [System.IO.Path]::GetFileName($cleanInput)
    }

    $items = @(Get-ChildItem -LiteralPath $parent -ErrorAction Ignore |
        Where-Object { [string]::IsNullOrEmpty($leaf) -or $_.Name.StartsWith($leaf, [System.StringComparison]::OrdinalIgnoreCase) })
    if ($DirectoriesOnly) {
        $items = @($items | Where-Object { $_.PSIsContainer })
    }

    foreach ($item in $items) {
        $completionText = if ($cleanInput -and -not [System.IO.Path]::IsPathRooted($cleanInput)) {
            if ($parent -eq '.') {
                $item.Name
            } else {
                Join-Path -Path $parent -ChildPath $item.Name
            }
        } else {
            $item.FullName
        }

        if ($item.PSIsContainer -and -not $completionText.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
            $completionText += [System.IO.Path]::DirectorySeparatorChar
        }

        $completionText = ConvertTo-CopilotQuotedValue -Value $completionText -AlwaysQuote $alwaysQuote
        $completionText = $Prefix + $completionText

        New-CopilotCompletionResult -CompletionText $completionText -ToolTip $item.FullName -ListItemText $item.Name
    }
}

function New-CopilotLiteralValueResults {
    param(
        [string]$CurrentValue,
        [string]$Placeholder,
        [string]$ToolTip,
        [string]$Prefix = ''
    )

    # $CurrentValue is the raw typed value; the '--option=' prefix is applied here so the
    # emptiness test still sees an empty value in the attached form.
    if ([string]::IsNullOrWhiteSpace($CurrentValue)) {
        return @(
            New-CopilotCompletionResult -CompletionText ($Prefix + $Placeholder) -ToolTip $ToolTip -ListItemText $Placeholder
        )
    }

    @(
        New-CopilotCompletionResult -CompletionText ($Prefix + $CurrentValue) -ToolTip $ToolTip -ListItemText $CurrentValue
    )
}

function Get-CopilotValueResults {
    param(
        [string]$ValueKind,
        [string]$CurrentValue,
        [string]$Prefix = ''
    )

    $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue }

    switch ($ValueKind) {
        'Model' {
            # 'auto' lets Copilot pick the model and is documented on --model but never listed
            # by 'copilot help config', so it leads the discovered ids.
            $models = @('auto') + @(Get-CopilotModels)
            if ($models.Count -eq 1) {
                $models += '<model>'
            }

            return $models |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object {
                    New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Model name (auto lets Copilot pick; others discovered from `copilot help config`).' -ListItemText $_
                }
        }
        'ReasoningEffort' {
            return @('none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Reasoning effort level.' }
        }
        'OutputFormat' {
            return @('text', 'json') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Output format.' }
        }
        'OnOff' {
            return @('on', 'off') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Boolean on/off value.' }
        }
        'LogLevel' {
            return @('none', 'error', 'warning', 'info', 'debug', 'all', 'default') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'CLI log level.' }
        }
        'DirectoryPath' {
            # A directory with no subdirectories must still yield something, or PowerShell's
            # filename fallback offers files in a directory-only slot.
            $paths = @(Get-CopilotPathCompletions -InputPath $typedValue -Prefix $Prefix -DirectoriesOnly)
            if ($paths.Count -eq 0) {
                return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder '<directory>' -ToolTip 'Directory path.'
            }

            return $paths
        }
        'FilePath' {
            $paths = @(Get-CopilotPathCompletions -InputPath $typedValue -Prefix $Prefix)
            if ($paths.Count -eq 0) {
                return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder '<file>' -ToolTip 'File path.'
            }

            return $paths
        }
        'CompletionShell' {
            return @('bash', 'zsh', 'fish') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Target shell for the completion script.' }
        }
        'UpdateChannel' {
            return @('stable', 'prerelease') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Update channel.' }
        }
        'McpTransport' {
            return @('stdio', 'http', 'sse') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'MCP server transport.' }
        }
        'PluginKind' {
            return @('plugin', 'mcp', 'skill', 'instruction', 'lsp') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Plugin kind filter.' }
        }
        'PluginScope' {
            return @('user', 'session', 'repository', 'working-directory', 'organization', 'plugin', 'builtin', 'unknown') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Configuration scope filter.' }
        }
        'InstallScope' {
            return @('user', 'project') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Skill install target.' }
        }
        'AgentMode' {
            return @('interactive', 'plan', 'autopilot') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Initial agent mode.' }
        }
        'ContextTier' {
            return @('default', 'long_context') |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Context window tier.' }
        }
        'SkillSource' {
            if (Test-CopilotLooksLikePath -Token $typedValue) {
                return @(Get-CopilotPathCompletions -InputPath $typedValue -Prefix $Prefix)
            }

            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('<skill-name-or-path>') -ToolTip 'Skill name, SKILL.md path, URL, or skill directory.'
        }
        'SessionName' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('"<session name>"') -ToolTip 'Display name for the new session.'
        }
        'EnvAssignment' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('KEY=VALUE') -ToolTip 'Environment variable assignment.'
        }
        'HttpHeader' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('"Name: value"') -ToolTip 'HTTP header for remote servers.'
        }
        'SharePath' {
            $results = New-Object System.Collections.Generic.List[object]
            foreach ($result in @(Get-CopilotPathCompletions -InputPath $typedValue -Prefix $Prefix)) {
                [void]$results.Add($result)
            }

            if ($results.Count -eq 0 -or [string]::IsNullOrWhiteSpace($typedValue)) {
                [void]$results.Add((New-CopilotCompletionResult -CompletionText ($Prefix + '.\copilot-session-<id>.md') -ToolTip 'Default markdown share path pattern.' -ListItemText '.\copilot-session-<id>.md'))
            }

            return @($results.ToArray())
        }
        'AdditionalMcpConfig' {
            if ($typedValue.StartsWith('@')) {
                return @(Get-CopilotPathCompletions -InputPath $typedValue.Substring(1) -Prefix ($Prefix + '@'))
            }

            $results = New-Object System.Collections.Generic.List[object]
            if ([string]::IsNullOrWhiteSpace($typedValue) -or '@' -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*')) {
                [void]$results.Add((New-CopilotCompletionResult -CompletionText ($Prefix + '@') -ToolTip 'Prefix a file path with @ to load JSON from disk.'))
            }

            foreach ($result in @(New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder '<json-or-@file>' -ToolTip 'Inline JSON string or @file path.')) {
                [void]$results.Add($result)
            }

            return @($results.ToArray())
        }
        'HelpTopic' {
            return $script:CopilotCompletionCache.HelpTopics |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Copilot help topic.' }
        }
        'InstalledPlugin' {
            $plugins = @(Get-CopilotInstalledPluginNames)
            if ($plugins.Count -eq 0) {
                return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder '<plugin-name>' -ToolTip 'Installed plugin name.'
            }

            return $plugins |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Installed plugin.' }
        }
        'MarketplaceName' {
            $marketplaces = @(Get-CopilotMarketplaceNames)
            if ($marketplaces.Count -eq 0) {
                return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder '<marketplace-name>' -ToolTip 'Registered marketplace name.'
            }

            return $marketplaces |
                Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } |
                ForEach-Object { New-CopilotCompletionResult -CompletionText ($Prefix + $_) -ToolTip 'Registered marketplace.' }
        }
        'PluginSource' {
            if (Test-CopilotLooksLikePath -Token $typedValue) {
                return @(Get-CopilotPathCompletions -InputPath $typedValue -Prefix $Prefix)
            }

            return @(
                New-CopilotCompletionResult -CompletionText ($Prefix + 'plugin-name@marketplace') -ToolTip 'Install from a registered marketplace.'
                New-CopilotCompletionResult -CompletionText ($Prefix + 'owner/repo') -ToolTip 'Install directly from a GitHub repository.'
                New-CopilotCompletionResult -CompletionText ($Prefix + 'owner/repo:path/to/plugin') -ToolTip 'Install from a repository subdirectory.'
                New-CopilotCompletionResult -CompletionText ($Prefix + 'https://example.com/plugin.git') -ToolTip 'Install from a git URL.'
            ) | Where-Object { $_.CompletionText -like "$Prefix$typedValue*" }
        }
        'MarketplaceSource' {
            if (Test-CopilotLooksLikePath -Token $typedValue) {
                return @(Get-CopilotPathCompletions -InputPath $typedValue -Prefix $Prefix)
            }

            return @(
                New-CopilotCompletionResult -CompletionText ($Prefix + 'owner/repo') -ToolTip 'Add a marketplace from a GitHub repository.'
                New-CopilotCompletionResult -CompletionText ($Prefix + 'https://example.com/marketplace.git') -ToolTip 'Add a marketplace from a URL.'
                New-CopilotCompletionResult -CompletionText ($Prefix + '.\path\to\marketplace') -ToolTip 'Add a marketplace from a local path.'
            ) | Where-Object { $_.CompletionText -like "$Prefix$typedValue*" }
        }
        'Host' {
            $results = @(
                New-CopilotCompletionResult -CompletionText ($Prefix + 'https://github.com') -ToolTip 'Default GitHub host.'
                New-CopilotCompletionResult -CompletionText ($Prefix + 'https://example.ghe.com') -ToolTip 'Example GitHub Enterprise Cloud data residency host.'
            ) | Where-Object { $_.CompletionText -like "$Prefix$typedValue*" }

            if (@($results).Count -gt 0) {
                return $results
            }

            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('https://example.ghe.com') -ToolTip 'GitHub host URL.'
        }
        'ResumeSession' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('<session-id>') -ToolTip 'Session ID or task ID.'
        }
        'AgentName' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('<agent>') -ToolTip 'Custom agent name.'
        }
        'PromptText' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('"<prompt>"') -ToolTip 'Prompt text.'
        }
        'ToolPattern' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('shell(git:*)') -ToolTip 'Tool name or permission pattern.'
        }
        'UrlPattern' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('github.com') -ToolTip 'URL, domain, or wildcard domain.'
        }
        'ServerName' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('<server-name>') -ToolTip 'MCP server name.'
        }
        'EnvVarList' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('MY_KEY,OTHER_KEY') -ToolTip 'Comma-separated environment variable names.'
        }
        'GithubMcpTool' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('*') -ToolTip 'GitHub MCP tool name or "*".'
        }
        'GithubMcpToolset' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('all') -ToolTip 'GitHub MCP toolset name or "all".'
        }
        'Count' {
            return New-CopilotLiteralValueResults -CurrentValue $typedValue -Prefix $Prefix -Placeholder ('<count>') -ToolTip 'Numeric count.'
        }
        default {
            return @()
        }
    }
}

function Resolve-CopilotParseState {
    param([string[]]$Tokens)

    $state = @{
        Path              = @()
        PendingOption     = $null
        PositionalsConsumed = 0
    }

    $index = 0
    while ($index -lt @($Tokens).Count) {
        $token = $Tokens[$index]

        if ($state.PendingOption) {
            if ($state.PendingOption.OptionalValue) {
                $commands = Get-CopilotCommandsForPath -Path $state.Path
                $isCommand = $commands.Contains($token.ToLowerInvariant())
                if ((Test-CopilotLooksLikeOption -Token $token) -or $isCommand) {
                    $state.PendingOption = $null
                    continue
                }
            }

            $state.PendingOption = $null
            $index++
            continue
        }

        $inlineOption = Find-CopilotInlineOptionSpec -Token $token -Path $state.Path
        if ($inlineOption) {
            $index++
            continue
        }

        $exactOption = Find-CopilotExactOptionSpec -Token $token -Path $state.Path
        if ($exactOption) {
            if (-not [string]::IsNullOrWhiteSpace($exactOption.ValueKind)) {
                $state.PendingOption = $exactOption
            }

            $index++
            continue
        }

        $commandsForPath = Get-CopilotCommandsForPath -Path $state.Path
        $commandMatch = $null
        foreach ($commandName in $commandsForPath.Keys) {
            if ($commandName.Equals($token, [System.StringComparison]::OrdinalIgnoreCase)) {
                $commandMatch = $commandName
                break
            }
        }

        if ($commandMatch) {
            $state.Path = @($state.Path + $commandMatch)
            $index++
            continue
        }

        $state.PositionalsConsumed++
        $index++
    }

    $state
}

function Get-CopilotSuggestions {
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    Initialize-CopilotStaticMetadata

    # Select tokens by extent (offsets are line-absolute, like $CursorPosition):
    # committed tokens end before the cursor, the current word contains it, and
    # anything to the right of the cursor is ignored.
    $elements = @($CommandAst.CommandElements | Select-Object -Skip 1)
    $tokensBeforeCurrent = @(
        $elements |
            Where-Object { $_.Extent.EndOffset -lt $CursorPosition } |
            ForEach-Object { Get-CopilotTokenText -Element $_ }
    )
    $currentElement = $elements |
        Where-Object { $_.Extent.StartOffset -lt $CursorPosition -and $_.Extent.EndOffset -ge $CursorPosition } |
        Select-Object -First 1

    $currentWord = if ($null -eq $currentElement) {
        ''
    } elseif (-not [string]::IsNullOrEmpty($WordToComplete)) {
        $WordToComplete
    } else {
        Get-CopilotTokenText -Element $currentElement
    }

    $state = Resolve-CopilotParseState -Tokens $tokensBeforeCurrent
    $spec = Get-CopilotCommandSpec -Path $state.Path

    $inlineOption = $null
    if (-not [string]::IsNullOrWhiteSpace($currentWord)) {
        $inlineOption = Find-CopilotInlineOptionSpec -Token $currentWord -Path $state.Path
    }

    if ($inlineOption) {
        $prefix = $inlineOption.Token + '='
        $typedValue = $currentWord.Substring($prefix.Length)
        return @(Get-CopilotValueResults -ValueKind $inlineOption.ValueKind -CurrentValue $typedValue -Prefix $prefix)
    }

    if ($state.PendingOption) {
        return @(Get-CopilotValueResults -ValueKind $state.PendingOption.ValueKind -CurrentValue $currentWord)
    }

    if ($currentWord.StartsWith('-')) {
        $optionResults = @(
            Get-CopilotOptionsForPath -Path $state.Path |
                Where-Object { $_.Token -like ([System.Management.Automation.WildcardPattern]::Escape($currentWord) + '*') } |
                ForEach-Object {
                    New-CopilotCompletionResult -CompletionText $_.Token -ResultType 'ParameterName' -ToolTip $_.Description
                }
        )

        # A combined short-flag cluster ('-sp') matches no single token; offer the option list
        # rather than nothing so the slot does not fall back to filenames.
        if ($optionResults.Count -eq 0 -and $currentWord -match '^-[A-Za-z]{2,}$') {
            $optionResults = @(
                Get-CopilotOptionsForPath -Path $state.Path |
                    ForEach-Object {
                        New-CopilotCompletionResult -CompletionText $_.Token -ResultType 'ParameterName' -ToolTip $_.Description
                    }
            )
        }

        return $optionResults
    }

    $results = New-Object System.Collections.Generic.List[object]

    if ($state.PositionalsConsumed -lt @($spec.Positionals).Count) {
        $positional = $spec.Positionals[$state.PositionalsConsumed]
        foreach ($result in @(Get-CopilotValueResults -ValueKind $positional.ValueKind -CurrentValue $currentWord)) {
            [void]$results.Add($result)
        }
    }

    foreach ($commandName in $spec.Commands.Keys) {
        if ($commandName -like ([System.Management.Automation.WildcardPattern]::Escape($currentWord) + '*')) {
            [void]$results.Add((New-CopilotCompletionResult -CompletionText $commandName -ToolTip $spec.Commands[$commandName]))
        }
    }

    if ([string]::IsNullOrEmpty($currentWord) -or $currentWord.StartsWith('-')) {
        foreach ($option in @(Get-CopilotOptionsForPath -Path $state.Path)) {
            if ($option.Token -like ([System.Management.Automation.WildcardPattern]::Escape($currentWord) + '*')) {
                [void]$results.Add((New-CopilotCompletionResult -CompletionText $option.Token -ResultType 'ParameterName' -ToolTip $option.Description))
            }
        }
    }

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($result in @($results.ToArray())) {
        if ($null -eq $result) {
            continue
        }

        if ($seen.Add($result.CompletionText)) {
            $result
        }
    }
}

Register-ArgumentCompleter -Native -CommandName @('copilot', 'copilot.exe') -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    Get-CopilotSuggestions -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition
}