completers/opencode_completer/opencode_completer.ps1

# opencode_completer.ps1
# PowerShell argument completer for opencode and opencode.exe.
# Static-first: the command tree, per-command options and enum values are transcribed from the yargs help of
# opencode 1.18.30 ('opencode --help' and 'opencode <command> --help').

Set-StrictMode -Version Latest

function New-OpencodeOption {
    param(
        [string[]]$Tokens,
        [string]$Description,
        [string]$Kind = 'Flag',
        [string[]]$Values = @(),
        [string]$Placeholder = ''
    )

    [pscustomobject]@{
        Tokens      = @($Tokens)
        Description = $Description
        Kind        = $Kind
        Values      = @($Values)
        Placeholder = $Placeholder
    }
}

function New-OpencodePositional {
    param(
        [string]$Name,
        [string]$Description,
        [string]$Kind = 'Placeholder',
        [string]$Placeholder = ''
    )

    [pscustomobject]@{
        Name        = $Name
        Description = $Description
        Kind        = $Kind
        Placeholder = if ($Placeholder) { $Placeholder } else { "<$Name>" }
    }
}

function New-OpencodeCommand {
    param(
        [string]$Name,
        [string]$Description,
        [string[]]$Aliases = @(),
        [object[]]$Options = @(),
        [object[]]$Positionals = @(),
        [object[]]$Subcommands = @()
    )

    [pscustomobject]@{
        Name        = $Name
        Description = $Description
        Aliases     = @($Aliases)
        Options     = @($Options)
        Positionals = @($Positionals)
        Subcommands = @($Subcommands)
    }
}

function Get-OpencodeCompletionCatalog {
    $existing = Get-Variable -Name OpencodeCompletionCatalog -Scope Script -ErrorAction Ignore
    if ($existing) {
        return $existing.Value
    }

    $logLevel = New-OpencodeOption -Tokens @('--log-level') -Description 'log level' -Kind 'Enum' -Values @('DEBUG', 'INFO', 'WARN', 'ERROR')
    $common = @(
        New-OpencodeOption -Tokens @('-h', '--help') -Description 'show help'
        New-OpencodeOption -Tokens @('-v', '--version') -Description 'show version number'
        New-OpencodeOption -Tokens @('--print-logs') -Description 'print logs to stderr'
        $logLevel
        New-OpencodeOption -Tokens @('--pure') -Description 'run without external plugins'
    )
    $server = @(
        New-OpencodeOption -Tokens @('--port') -Description 'port to listen on [default: 0]' -Kind 'Number' -Placeholder '<port>'
        New-OpencodeOption -Tokens @('--hostname') -Description 'hostname to listen on [default: "127.0.0.1"]' -Kind 'Enum' -Values @('127.0.0.1', '0.0.0.0', 'localhost')
        New-OpencodeOption -Tokens @('--mdns') -Description 'enable mDNS service discovery (defaults hostname to 0.0.0.0)'
        New-OpencodeOption -Tokens @('--mdns-domain') -Description 'custom domain name for mDNS service (default: opencode.local)' -Kind 'String' -Placeholder '<domain>'
        New-OpencodeOption -Tokens @('--cors') -Description 'additional domains to allow for CORS' -Kind 'String' -Placeholder '<domain>'
    )
    $session = @(
        New-OpencodeOption -Tokens @('-c', '--continue') -Description 'continue the last session'
        New-OpencodeOption -Tokens @('-s', '--session') -Description 'session id to continue' -Kind 'String' -Placeholder '<session-id>'
        New-OpencodeOption -Tokens @('--fork') -Description 'fork the session when continuing (use with --continue or --session)'
    )
    $model = New-OpencodeOption -Tokens @('-m', '--model') -Description 'model to use in the format of provider/model' -Kind 'String' -Placeholder '<provider/model>'
    $agent = New-OpencodeOption -Tokens @('--agent') -Description 'agent to use' -Kind 'String' -Placeholder '<agent>'
    $auto = New-OpencodeOption -Tokens @('--auto') -Description 'auto-approve permissions that are not explicitly denied (dangerous!)'
    $mini = @(
        New-OpencodeOption -Tokens @('--mini') -Description 'start the minimal interactive interface'
        New-OpencodeOption -Tokens @('--no-replay') -Description 'disable mini session history replay on resume and after resize'
        New-OpencodeOption -Tokens @('--replay-limit') -Description 'cap visible mini replay to the newest N messages' -Kind 'Number' -Placeholder '<n>'
    )
    $auth = @(
        New-OpencodeOption -Tokens @('-p', '--password') -Description 'basic auth password (defaults to OPENCODE_SERVER_PASSWORD)' -Kind 'String' -Placeholder '<password>'
        New-OpencodeOption -Tokens @('-u', '--username') -Description "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')" -Kind 'String' -Placeholder '<username>'
    )

    $rootOptions = @($common) + @($server) + @($model) + @($session) + @(
        New-OpencodeOption -Tokens @('--prompt') -Description 'prompt to use' -Kind 'String' -Placeholder '<prompt>'
        $agent
        $auto
    ) + @($mini)

    $commands = @(
        New-OpencodeCommand -Name 'completion' -Description 'generate shell completion script' -Options $common
        New-OpencodeCommand -Name 'acp' -Description 'start ACP (Agent Client Protocol) server' -Options (@($common) + @($server) + @(
            New-OpencodeOption -Tokens @('--cwd') -Description 'working directory' -Kind 'Directory'
        ))
        New-OpencodeCommand -Name 'mcp' -Description 'manage MCP (Model Context Protocol) servers' -Options $common -Subcommands @(
            New-OpencodeCommand -Name 'add' -Description 'add an MCP server' -Options (@($common) + @(
                New-OpencodeOption -Tokens @('--url') -Description 'URL for a remote MCP server' -Kind 'String' -Placeholder '<url>'
                New-OpencodeOption -Tokens @('--env') -Description 'environment variable for a local MCP server (KEY=VALUE)' -Kind 'String' -Placeholder '<KEY=VALUE>'
                New-OpencodeOption -Tokens @('--header') -Description 'HTTP header for a remote MCP server (KEY=VALUE)' -Kind 'String' -Placeholder '<KEY=VALUE>'
            )) -Positionals @(New-OpencodePositional -Name 'name' -Description 'name of the MCP server')
            New-OpencodeCommand -Name 'list' -Description 'list MCP servers and their status' -Aliases @('ls') -Options $common
            New-OpencodeCommand -Name 'auth' -Description 'authenticate with an OAuth-enabled MCP server' -Options $common -Positionals @(New-OpencodePositional -Name 'name' -Description 'name of the MCP server') -Subcommands @(
                New-OpencodeCommand -Name 'list' -Description 'list OAuth-capable MCP servers and their auth status' -Aliases @('ls') -Options $common
            )
            New-OpencodeCommand -Name 'logout' -Description 'remove OAuth credentials for an MCP server' -Options $common -Positionals @(New-OpencodePositional -Name 'name' -Description 'name of the MCP server')
            New-OpencodeCommand -Name 'debug' -Description 'debug OAuth connection for an MCP server' -Options $common -Positionals @(New-OpencodePositional -Name 'name' -Description 'name of the MCP server')
        )
        New-OpencodeCommand -Name 'attach' -Description 'attach to a running opencode server' -Options (@($common) + @(
            New-OpencodeOption -Tokens @('--dir') -Description 'directory to run in' -Kind 'Directory'
        ) + @($session) + @($auth) + @($mini)) -Positionals @(New-OpencodePositional -Name 'url' -Description 'http://localhost:4096' -Placeholder 'http://localhost:4096')
        New-OpencodeCommand -Name 'run' -Description 'run opencode with a message' -Options (@($common) + @(
            New-OpencodeOption -Tokens @('--command') -Description 'the command to run, use message for args' -Kind 'String' -Placeholder '<command>'
        ) + @($session) + @(
            New-OpencodeOption -Tokens @('--share') -Description 'share the session'
            $model
            $agent
            New-OpencodeOption -Tokens @('--format') -Description 'format: default (formatted) or json (raw JSON events)' -Kind 'Enum' -Values @('default', 'json')
            New-OpencodeOption -Tokens @('-f', '--file') -Description 'file(s) to attach to message' -Kind 'Path'
            New-OpencodeOption -Tokens @('--title') -Description 'title for the session (uses truncated prompt if no value provided)' -Kind 'String' -Placeholder '<title>'
            New-OpencodeOption -Tokens @('--attach') -Description 'attach to a running opencode server (e.g., http://localhost:4096)' -Kind 'String' -Placeholder 'http://localhost:4096'
        ) + @($auth) + @(
            New-OpencodeOption -Tokens @('--dir') -Description 'directory to run in, path on remote server if attaching' -Kind 'Directory'
            New-OpencodeOption -Tokens @('--port') -Description 'port for the local server (defaults to random port if no value provided)' -Kind 'Number' -Placeholder '<port>'
            New-OpencodeOption -Tokens @('--variant') -Description 'model variant (provider-specific reasoning effort, e.g., high, max, minimal)' -Kind 'Enum' -Values @('high', 'max', 'minimal')
            New-OpencodeOption -Tokens @('--thinking') -Description 'show thinking blocks'
            New-OpencodeOption -Tokens @('-i', '--interactive') -Description 'run in direct interactive split-footer mode'
            $auto
        )) -Positionals @(New-OpencodePositional -Name 'message' -Description 'message to send' -Kind 'Text' -Placeholder '<message>')
        New-OpencodeCommand -Name 'debug' -Description 'debugging and troubleshooting tools' -Options $common -Subcommands @(
            New-OpencodeCommand -Name 'config' -Description 'show resolved configuration' -Options $common
            New-OpencodeCommand -Name 'lsp' -Description 'LSP debugging utilities' -Options $common -Subcommands @(
                New-OpencodeCommand -Name 'diagnostics' -Description 'get diagnostics for a file' -Options $common -Positionals @(New-OpencodePositional -Name 'file' -Description 'file to diagnose' -Kind 'Path')
                New-OpencodeCommand -Name 'symbols' -Description 'search workspace symbols' -Options $common -Positionals @(New-OpencodePositional -Name 'query' -Description 'symbol query')
                New-OpencodeCommand -Name 'document-symbols' -Description 'get symbols from a document' -Options $common -Positionals @(New-OpencodePositional -Name 'uri' -Description 'document uri')
            )
            New-OpencodeCommand -Name 'rg' -Description 'ripgrep debugging utilities' -Options $common -Subcommands @(
                New-OpencodeCommand -Name 'files' -Description 'list files using ripgrep' -Options $common
                New-OpencodeCommand -Name 'search' -Description 'search file contents using ripgrep' -Options $common -Positionals @(New-OpencodePositional -Name 'pattern' -Description 'search pattern')
            )
            New-OpencodeCommand -Name 'file' -Description 'file system debugging utilities' -Options $common -Subcommands @(
                New-OpencodeCommand -Name 'read' -Description 'read file contents as JSON' -Options $common -Positionals @(New-OpencodePositional -Name 'path' -Description 'file to read' -Kind 'Path')
                New-OpencodeCommand -Name 'list' -Description 'list files in a directory' -Options $common -Positionals @(New-OpencodePositional -Name 'path' -Description 'directory to list' -Kind 'Directory')
                New-OpencodeCommand -Name 'search' -Description 'search files by query' -Options $common -Positionals @(New-OpencodePositional -Name 'query' -Description 'file query')
            )
            New-OpencodeCommand -Name 'scrap' -Description 'list all known projects' -Options $common
            New-OpencodeCommand -Name 'skill' -Description 'list all available skills' -Options $common
            New-OpencodeCommand -Name 'snapshot' -Description 'snapshot debugging utilities' -Options $common -Subcommands @(
                New-OpencodeCommand -Name 'track' -Description 'track current snapshot state' -Options $common
                New-OpencodeCommand -Name 'patch' -Description 'show patch for a snapshot hash' -Options $common -Positionals @(New-OpencodePositional -Name 'hash' -Description 'snapshot hash')
                New-OpencodeCommand -Name 'diff' -Description 'show diff for a snapshot hash' -Options $common -Positionals @(New-OpencodePositional -Name 'hash' -Description 'snapshot hash')
            )
            New-OpencodeCommand -Name 'startup' -Description 'print startup timing' -Options $common
            New-OpencodeCommand -Name 'agent' -Description 'show agent configuration details' -Options (@($common) + @(
                New-OpencodeOption -Tokens @('--tool') -Description 'Tool id to execute' -Kind 'String' -Placeholder '<tool-id>'
                New-OpencodeOption -Tokens @('--params') -Description 'Tool params as JSON or a JS object literal' -Kind 'String' -Placeholder '<json>'
            )) -Positionals @(New-OpencodePositional -Name 'name' -Description 'Agent name')
            New-OpencodeCommand -Name 'v2' -Description 'debug v2 catalog and built-in plugins' -Options $common
            New-OpencodeCommand -Name 'info' -Description 'show debug information' -Options $common
            New-OpencodeCommand -Name 'paths' -Description 'show global paths (data, config, cache, state)' -Options $common
            New-OpencodeCommand -Name 'wait' -Description 'wait indefinitely (for debugging)' -Options $common
        )
        New-OpencodeCommand -Name 'providers' -Description 'manage AI providers and credentials' -Aliases @('auth') -Options $common -Subcommands @(
            New-OpencodeCommand -Name 'list' -Description 'list providers and credentials' -Aliases @('ls') -Options $common
            New-OpencodeCommand -Name 'login' -Description 'log in to a provider' -Options (@($common) + @(
                New-OpencodeOption -Tokens @('-p', '--provider') -Description 'provider id or name to log in to (skips provider selection)' -Kind 'String' -Placeholder '<provider>'
                New-OpencodeOption -Tokens @('-m', '--method') -Description 'login method label (skips method selection)' -Kind 'String' -Placeholder '<method>'
            )) -Positionals @(New-OpencodePositional -Name 'url' -Description 'opencode auth provider')
            New-OpencodeCommand -Name 'logout' -Description 'log out from a configured provider' -Options $common -Positionals @(New-OpencodePositional -Name 'provider' -Description 'provider id or name to log out from')
        )
        New-OpencodeCommand -Name 'agent' -Description 'manage agents' -Options $common -Subcommands @(
            New-OpencodeCommand -Name 'create' -Description 'create a new agent' -Options (@($common) + @(
                New-OpencodeOption -Tokens @('--path') -Description 'directory path to generate the agent file' -Kind 'Directory'
                New-OpencodeOption -Tokens @('--description') -Description 'what the agent should do' -Kind 'String' -Placeholder '<description>'
                New-OpencodeOption -Tokens @('--mode') -Description 'agent mode' -Kind 'Enum' -Values @('all', 'primary', 'subagent')
                New-OpencodeOption -Tokens @('--permissions', '--tools') -Description 'comma-separated list of permissions to allow (default: all)' -Kind 'Enum' -Values @('bash', 'read', 'edit', 'glob', 'grep', 'webfetch', 'task', 'todowrite', 'websearch', 'lsp', 'skill')
                $model
            ))
            New-OpencodeCommand -Name 'list' -Description 'list all available agents' -Options $common
        )
        New-OpencodeCommand -Name 'upgrade' -Description 'upgrade opencode to the latest or a specific version' -Options (@($common) + @(
            New-OpencodeOption -Tokens @('-m', '--method') -Description 'installation method to use' -Kind 'Enum' -Values @('curl', 'npm', 'pnpm', 'bun', 'brew', 'choco', 'scoop')
        )) -Positionals @(New-OpencodePositional -Name 'target' -Description "version to upgrade to, for ex '0.1.48' or 'v0.1.48'" -Placeholder '<version>')
        New-OpencodeCommand -Name 'uninstall' -Description 'uninstall opencode and remove all related files' -Options (@($common) + @(
            New-OpencodeOption -Tokens @('-c', '--keep-config') -Description 'keep configuration files'
            New-OpencodeOption -Tokens @('-d', '--keep-data') -Description 'keep session data and snapshots'
            New-OpencodeOption -Tokens @('--dry-run') -Description 'show what would be removed without removing'
            New-OpencodeOption -Tokens @('-f', '--force') -Description 'skip confirmation prompts'
        ))
        New-OpencodeCommand -Name 'serve' -Description 'starts a headless opencode server' -Options (@($common) + @($server))
        New-OpencodeCommand -Name 'web' -Description 'start opencode server and open web interface' -Options (@($common) + @($server))
        New-OpencodeCommand -Name 'models' -Description 'list all available models' -Options (@($common) + @(
            New-OpencodeOption -Tokens @('--verbose') -Description 'use more verbose model output (includes metadata like costs)'
            New-OpencodeOption -Tokens @('--refresh') -Description 'refresh the models cache from models.dev'
        )) -Positionals @(New-OpencodePositional -Name 'provider' -Description 'provider ID to filter models by')
        New-OpencodeCommand -Name 'stats' -Description 'show token usage and cost statistics' -Options (@($common) + @(
            New-OpencodeOption -Tokens @('--days') -Description 'show stats for the last N days (default: all time)' -Kind 'Number' -Placeholder '<days>'
            New-OpencodeOption -Tokens @('--tools') -Description 'number of tools to show (default: all)' -Kind 'Number' -Placeholder '<n>'
            New-OpencodeOption -Tokens @('--models') -Description 'show model statistics (default: hidden). Pass a number to show top N, otherwise shows all'
            New-OpencodeOption -Tokens @('--project') -Description 'filter by project (default: all projects, empty string: current project)' -Kind 'String' -Placeholder '<project>'
        ))
        New-OpencodeCommand -Name 'export' -Description 'export session data as JSON' -Options (@($common) + @(
            New-OpencodeOption -Tokens @('--sanitize') -Description 'redact sensitive transcript and file data'
        )) -Positionals @(New-OpencodePositional -Name 'sessionID' -Description 'session id to export' -Placeholder '<session-id>')
        New-OpencodeCommand -Name 'import' -Description 'import session data from JSON file or URL' -Options $common -Positionals @(New-OpencodePositional -Name 'file' -Description 'path to JSON file or share URL' -Kind 'Path')
        New-OpencodeCommand -Name 'github' -Description 'manage GitHub agent' -Options $common -Subcommands @(
            New-OpencodeCommand -Name 'install' -Description 'install the GitHub agent' -Options $common
            New-OpencodeCommand -Name 'run' -Description 'run the GitHub agent' -Options (@($common) + @(
                New-OpencodeOption -Tokens @('--event') -Description 'GitHub mock event to run the agent for' -Kind 'String' -Placeholder '<event>'
                New-OpencodeOption -Tokens @('--token') -Description 'GitHub personal access token (github_pat_********)' -Kind 'String' -Placeholder '<token>'
            ))
        )
        New-OpencodeCommand -Name 'pr' -Description 'fetch and checkout a GitHub PR branch, then run opencode' -Options $common -Positionals @(New-OpencodePositional -Name 'number' -Description 'PR number to checkout')
        New-OpencodeCommand -Name 'session' -Description 'manage sessions' -Options $common -Subcommands @(
            New-OpencodeCommand -Name 'list' -Description 'list sessions' -Options (@($common) + @(
                New-OpencodeOption -Tokens @('-n', '--max-count') -Description 'limit to N most recent sessions' -Kind 'Number' -Placeholder '<n>'
                New-OpencodeOption -Tokens @('--format') -Description 'output format' -Kind 'Enum' -Values @('table', 'json')
            ))
            New-OpencodeCommand -Name 'delete' -Description 'delete a session' -Options $common -Positionals @(New-OpencodePositional -Name 'sessionID' -Description 'session ID to delete' -Placeholder '<session-id>')
        )
        New-OpencodeCommand -Name 'plugin' -Description 'install plugin and update config' -Aliases @('plug') -Options (@($common) + @(
            New-OpencodeOption -Tokens @('-g', '--global') -Description 'install in global config'
            New-OpencodeOption -Tokens @('-f', '--force') -Description 'replace existing plugin version'
        )) -Positionals @(New-OpencodePositional -Name 'module' -Description 'npm module name')
        New-OpencodeCommand -Name 'db' -Description 'database tools' -Options (@($common) + @(
            New-OpencodeOption -Tokens @('--format') -Description 'Output format [default: "tsv"]' -Kind 'Enum' -Values @('json', 'tsv')
        )) -Positionals @(New-OpencodePositional -Name 'query' -Description 'SQL query to execute' -Kind 'Text' -Placeholder '<query>') -Subcommands @(
            New-OpencodeCommand -Name 'path' -Description 'print the database path' -Options $common
        )
    )

    $root = New-OpencodeCommand -Name '' -Description 'start opencode tui' -Options $rootOptions -Positionals @(
        New-OpencodePositional -Name 'project' -Description 'path to start opencode in' -Kind 'Directory'
    ) -Subcommands $commands

    $catalog = @{ Root = $root }
    Set-Variable -Name OpencodeCompletionCatalog -Scope Script -Value $catalog
    $catalog
}

function New-OpencodeCompletionResult {
    param([string]$CompletionText, [string]$ListItemText, [string]$ResultType, [string]$ToolTip)

    if ([string]::IsNullOrWhiteSpace($ListItemText)) { $ListItemText = $CompletionText }
    if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText }
    [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $ToolTip)
}

function Find-OpencodeSubcommand {
    param([object]$Command, [string]$Token)

    foreach ($subcommand in @($Command.Subcommands)) {
        if ([string]::Equals($subcommand.Name, $Token, [System.StringComparison]::Ordinal)) {
            return $subcommand
        }
        foreach ($alias in @($subcommand.Aliases)) {
            if ([string]::Equals($alias, $Token, [System.StringComparison]::Ordinal)) {
                return $subcommand
            }
        }
    }

    $null
}

function Find-OpencodeOption {
    param([object]$Command, [string]$Token)

    foreach ($option in @($Command.Options)) {
        foreach ($optionToken in @($option.Tokens)) {
            if ([string]::Equals($optionToken, $Token, [System.StringComparison]::Ordinal)) {
                return $option
            }
        }
    }

    $null
}

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

    # Elements that end before the cursor are completed tokens; the element under the cursor is the word being
    # typed, cut at the cursor; anything to the right of the cursor is ignored.
    $tokens = New-Object System.Collections.Generic.List[string]
    $currentWord = ''
    foreach ($element in @($CommandAst.CommandElements | Select-Object -Skip 1)) {
        $extent = $element.Extent
        if ($extent.EndOffset -lt $CursorPosition) {
            $text = if ($element -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $element.Value } else { $extent.Text }
            [void]$tokens.Add($text)
            continue
        }

        if ($extent.StartOffset -le $CursorPosition) {
            $currentWord = $extent.Text.Substring(0, $CursorPosition - $extent.StartOffset)
        }
        break
    }

    if ([string]::IsNullOrEmpty($currentWord) -and -not [string]::IsNullOrEmpty($WordToComplete)) {
        $currentWord = $WordToComplete
    }

    [pscustomobject]@{
        CurrentWord = $currentWord
        Tokens      = @($tokens.ToArray())
    }
}

function Get-OpencodeContext {
    param([string[]]$Tokens)

    $catalog = Get-OpencodeCompletionCatalog
    $command = $catalog.Root
    $pendingOption = $null
    $positionalCount = 0

    foreach ($token in @($Tokens)) {
        if ($null -ne $pendingOption) {
            $pendingOption = $null
            continue
        }

        if ($token.StartsWith('-')) {
            $optionToken = $token
            $attached = $false
            if ($token -match '^(--?[A-Za-z0-9][A-Za-z0-9-]*)=') {
                $optionToken = $matches[1]
                $attached = $true
            }

            $option = Find-OpencodeOption -Command $command -Token $optionToken
            if ($option -and $option.Kind -ne 'Flag' -and -not $attached) {
                $pendingOption = $option
            }
            continue
        }

        if ($positionalCount -eq 0) {
            $subcommand = Find-OpencodeSubcommand -Command $command -Token $token
            if ($subcommand) {
                $command = $subcommand
                continue
            }
        }

        $positionalCount++
    }

    [pscustomobject]@{
        Command         = $command
        PendingOption   = $pendingOption
        PositionalCount = $positionalCount
    }
}

function Get-OpencodePathCompletions {
    param([string]$CurrentWord, [bool]$DirectoryOnly, [string]$InlinePrefix = '')

    $results = New-Object System.Collections.Generic.List[object]
    foreach ($item in [System.Management.Automation.CompletionCompleters]::CompleteFilename($CurrentWord)) {
        if ($DirectoryOnly -and $item.ResultType -ne [System.Management.Automation.CompletionResultType]::ProviderContainer) {
            continue
        }

        [void]$results.Add((New-OpencodeCompletionResult -CompletionText ($InlinePrefix + $item.CompletionText) -ListItemText $item.ListItemText -ResultType $item.ResultType.ToString() -ToolTip $item.ToolTip))
    }

    @($results.ToArray())
}

function Get-OpencodeValueCompletions {
    param([object]$Option, [string]$CurrentWord, [string]$InlinePrefix = '')

    switch ($Option.Kind) {
        'Enum' {
            return @(foreach ($value in @($Option.Values)) {
                if ($value.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
                    New-OpencodeCompletionResult -CompletionText ($InlinePrefix + $value) -ListItemText $value -ResultType 'ParameterValue' -ToolTip $Option.Description
                }
            })
        }
        'Path' {
            return @(Get-OpencodePathCompletions -CurrentWord $CurrentWord -DirectoryOnly $false -InlinePrefix $InlinePrefix)
        }
        'Directory' {
            return @(Get-OpencodePathCompletions -CurrentWord $CurrentWord -DirectoryOnly $true -InlinePrefix $InlinePrefix)
        }
        default {
            if (-not [string]::IsNullOrEmpty($CurrentWord)) {
                return @()
            }

            $placeholder = if ($Option.Placeholder) { $Option.Placeholder } else { '<value>' }
            return @(New-OpencodeCompletionResult -CompletionText ($InlinePrefix + $placeholder) -ListItemText $placeholder -ResultType 'ParameterValue' -ToolTip $Option.Description)
        }
    }
}

function Get-OpencodeOptionCompletions {
    param([object]$Command, [string]$CurrentWord)

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
    foreach ($option in @($Command.Options)) {
        foreach ($token in @($option.Tokens)) {
            if (-not $token.StartsWith($CurrentWord, [System.StringComparison]::Ordinal)) {
                continue
            }
            if ($seen.Add($token)) {
                New-OpencodeCompletionResult -CompletionText $token -ResultType 'ParameterName' -ToolTip $option.Description
            }
        }
    }
}

function Get-OpencodeSubcommandCompletions {
    param([object]$Command, [string]$CurrentWord)

    foreach ($subcommand in @($Command.Subcommands)) {
        if ($subcommand.Name.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
            New-OpencodeCompletionResult -CompletionText $subcommand.Name -ResultType 'ParameterValue' -ToolTip $subcommand.Description
        }
        foreach ($alias in @($subcommand.Aliases)) {
            if ($alias.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
                New-OpencodeCompletionResult -CompletionText $alias -ResultType 'ParameterValue' -ToolTip ('alias of {0}: {1}' -f $subcommand.Name, $subcommand.Description)
            }
        }
    }
}

function Get-OpencodePositionalCompletions {
    param([object]$Positional, [string]$CurrentWord)

    switch ($Positional.Kind) {
        'Path' { return @(Get-OpencodePathCompletions -CurrentWord $CurrentWord -DirectoryOnly $false) }
        'Directory' { return @(Get-OpencodePathCompletions -CurrentWord $CurrentWord -DirectoryOnly $true) }
        default {
            if (-not [string]::IsNullOrEmpty($CurrentWord)) {
                return @()
            }

            return @(New-OpencodeCompletionResult -CompletionText $Positional.Placeholder -ResultType 'ParameterValue' -ToolTip $Positional.Description)
        }
    }
}

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

    $state = Get-OpencodeTokenState -CommandAst $CommandAst -CursorPosition $CursorPosition -WordToComplete $WordToComplete
    $currentWord = $state.CurrentWord
    $context = Get-OpencodeContext -Tokens $state.Tokens
    $command = $context.Command

    # Attached --option=value form.
    if ($currentWord -match '^(?<name>--?[A-Za-z0-9][A-Za-z0-9-]*)=(?<value>.*)$') {
        $option = Find-OpencodeOption -Command $command -Token $matches.name
        if ($option -and $option.Kind -ne 'Flag') {
            return @(Get-OpencodeValueCompletions -Option $option -CurrentWord $matches.value -InlinePrefix ($matches.name + '='))
        }

        return @()
    }

    if ($null -ne $context.PendingOption) {
        return @(Get-OpencodeValueCompletions -Option $context.PendingOption -CurrentWord $currentWord)
    }

    if ($currentWord.StartsWith('-')) {
        return @(Get-OpencodeOptionCompletions -Command $command -CurrentWord $currentWord)
    }

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

    if ($context.PositionalCount -eq 0) {
        foreach ($item in @(Get-OpencodeSubcommandCompletions -Command $command -CurrentWord $currentWord)) {
            [void]$results.Add($item)
        }
    }

    $positionals = @($command.Positionals)
    if ($context.PositionalCount -lt $positionals.Count) {
        $positional = $positionals[$context.PositionalCount]
        # The root [project] operand is only offered once a partial path is typed, so a bare 'opencode ' shows commands.
        if (-not ($command.Name -eq '' -and [string]::IsNullOrEmpty($currentWord))) {
            foreach ($item in @(Get-OpencodePositionalCompletions -Positional $positional -CurrentWord $currentWord)) {
                [void]$results.Add($item)
            }
        }
    } elseif ($positionals.Count -gt 0 -and $positionals[-1].Kind -eq 'Text' -and [string]::IsNullOrEmpty($currentWord)) {
        # Variadic free text (run [message..]) keeps accepting words.
        [void]$results.Add((New-OpencodeCompletionResult -CompletionText $positionals[-1].Placeholder -ResultType 'ParameterValue' -ToolTip $positionals[-1].Description))
    }

    if ([string]::IsNullOrEmpty($currentWord) -and $command.Name -ne '') {
        foreach ($item in @(Get-OpencodeOptionCompletions -Command $command -CurrentWord '')) {
            [void]$results.Add($item)
        }
    }

    @($results.ToArray())
}

# Register the completer for both opencode and opencode.exe
# Using a literal script block to satisfy Import-CompleterScript requirements
Register-ArgumentCompleter -Native -CommandName 'opencode', 'opencode.exe' -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    Complete-Opencode -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition
}