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 } |