completers/ollama_completer/ollama_completer.ps1

Set-StrictMode -Version Latest

function New-OllamaCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ListItemText = $CompletionText,
        [System.Management.Automation.CompletionResultType]$ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue,
        [string]$ToolTip = $CompletionText
    )

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

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

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

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

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

function New-OllamaCommandSpec {
    param(
        [string]$Name,
        [string]$Description,
        [string[]]$Positionals,
        [object[]]$Options,
        [string[]]$Aliases
    )

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

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

    $defaultHelpOptions = @(
        New-OllamaOptionSpec -Tokens @('-h', '--help') -Description 'Show help.'
    )

    $commands = @(
        New-OllamaCommandSpec -Name 'serve' -Description 'Start Ollama.' -Positionals @() -Options $defaultHelpOptions -Aliases @('start')
        New-OllamaCommandSpec -Name 'create' -Description 'Create a model.' -Positionals @('Model') -Options @(
            New-OllamaOptionSpec -Tokens @('--draft-quantize') -Description 'Quantize draft model to this level.' -ValueKind 'Quantization'
            New-OllamaOptionSpec -Tokens @('--experimental') -Description 'Enable experimental safetensors model creation.'
            New-OllamaOptionSpec -Tokens @('-f', '--file') -Description 'Name of the Modelfile.' -ValueKind 'FilePath'
            New-OllamaOptionSpec -Tokens @('-q', '--quantize') -Description 'Quantize model to this level.' -ValueKind 'Quantization'
            $defaultHelpOptions
        ) -Aliases @()
        New-OllamaCommandSpec -Name 'show' -Description 'Show information for a model.' -Positionals @('Model') -Options @(
            New-OllamaOptionSpec -Tokens @('--license') -Description 'Show license of a model.'
            New-OllamaOptionSpec -Tokens @('--modelfile') -Description 'Show Modelfile of a model.'
            New-OllamaOptionSpec -Tokens @('--parameters') -Description 'Show parameters of a model.'
            New-OllamaOptionSpec -Tokens @('--system') -Description 'Show system message of a model.'
            New-OllamaOptionSpec -Tokens @('--template') -Description 'Show template of a model.'
            New-OllamaOptionSpec -Tokens @('-v', '--verbose') -Description 'Show detailed model information.'
            $defaultHelpOptions
        ) -Aliases @()
        New-OllamaCommandSpec -Name 'run' -Description 'Run a model.' -Positionals @('Model', 'Prompt') -Options @(
            New-OllamaOptionSpec -Tokens @('--dimensions') -Description 'Truncate output embeddings to specified dimension.' -ValueKind 'Number'
            New-OllamaOptionSpec -Tokens @('--experimental') -Description 'Enable experimental agent loop with tools.'
            New-OllamaOptionSpec -Tokens @('--experimental-websearch') -Description 'Enable web search tool in experimental mode.'
            New-OllamaOptionSpec -Tokens @('--experimental-yolo') -Description 'Skip all tool approval prompts.'
            New-OllamaOptionSpec -Tokens @('--format') -Description 'Response format.' -ValueKind 'Format'
            New-OllamaOptionSpec -Tokens @('--hidethinking') -Description 'Hide thinking output.'
            New-OllamaOptionSpec -Tokens @('--insecure') -Description 'Use an insecure registry.'
            New-OllamaOptionSpec -Tokens @('--keepalive') -Description 'Duration to keep a model loaded.' -ValueKind 'Duration'
            New-OllamaOptionSpec -Tokens @('--nowordwrap') -Description 'Don''t wrap words to the next line automatically.'
            New-OllamaOptionSpec -Tokens @('--think') -Description 'Enable thinking mode.' -ValueKind 'Think' -OptionalValue -InlineOnly
            New-OllamaOptionSpec -Tokens @('--truncate') -Description 'Control truncate behavior.' -ValueKind 'Boolean' -OptionalValue -InlineOnly
            New-OllamaOptionSpec -Tokens @('--verbose') -Description 'Show timings for response.'
            New-OllamaOptionSpec -Tokens @('--width') -Description 'Image width.' -ValueKind 'Number'
            New-OllamaOptionSpec -Tokens @('--height') -Description 'Image height.' -ValueKind 'Number'
            New-OllamaOptionSpec -Tokens @('--steps') -Description 'Denoising steps.' -ValueKind 'Number'
            New-OllamaOptionSpec -Tokens @('--seed') -Description 'Random seed.' -ValueKind 'Number'
            New-OllamaOptionSpec -Tokens @('--negative') -Description 'Negative prompt.' -ValueKind 'NegativePrompt'
            $defaultHelpOptions
        ) -Aliases @()
        New-OllamaCommandSpec -Name 'stop' -Description 'Stop a running model.' -Positionals @('Model') -Options $defaultHelpOptions -Aliases @()
        New-OllamaCommandSpec -Name 'pull' -Description 'Pull a model from a registry.' -Positionals @('Model') -Options @(
            New-OllamaOptionSpec -Tokens @('--insecure') -Description 'Use an insecure registry.'
            $defaultHelpOptions
        ) -Aliases @()
        New-OllamaCommandSpec -Name 'push' -Description 'Push a model to a registry.' -Positionals @('Model') -Options @(
            New-OllamaOptionSpec -Tokens @('--insecure') -Description 'Use an insecure registry.'
            $defaultHelpOptions
        ) -Aliases @()
        New-OllamaCommandSpec -Name 'signin' -Description 'Sign in to ollama.com.' -Positionals @() -Options $defaultHelpOptions -Aliases @()
        New-OllamaCommandSpec -Name 'signout' -Description 'Sign out from ollama.com.' -Positionals @() -Options $defaultHelpOptions -Aliases @()
        New-OllamaCommandSpec -Name 'list' -Description 'List models.' -Positionals @() -Options $defaultHelpOptions -Aliases @('ls')
        New-OllamaCommandSpec -Name 'ps' -Description 'List running models.' -Positionals @() -Options $defaultHelpOptions -Aliases @()
        New-OllamaCommandSpec -Name 'cp' -Description 'Copy a model.' -Positionals @('SourceModel', 'DestinationModel') -Options $defaultHelpOptions -Aliases @()
        New-OllamaCommandSpec -Name 'rm' -Description 'Remove a model.' -Positionals @('Model') -Options $defaultHelpOptions -Aliases @()
        New-OllamaCommandSpec -Name 'launch' -Description 'Launch the Ollama menu or an integration.' -Positionals @('Integration') -Options @(
            New-OllamaOptionSpec -Tokens @('--config') -Description 'Configure without launching.'
            New-OllamaOptionSpec -Tokens @('--model') -Description 'Model to use.' -ValueKind 'Model'
            New-OllamaOptionSpec -Tokens @('--restore') -Description 'Restore an integration to its default profile.'
            New-OllamaOptionSpec -Tokens @('-y', '--yes') -Description 'Automatically answer yes to confirmation prompts.'
            $defaultHelpOptions
        ) -Aliases @()
        New-OllamaCommandSpec -Name 'help' -Description 'Help about any command.' -Positionals @('CommandName') -Options $defaultHelpOptions -Aliases @()
    )

    $commandMap = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($command in $commands) {
        $commandMap[$command.Name] = $command
    }

    $catalog = @{
        Initialized         = $false
        ParsedCommands      = @{}
        CommandPath         = $null
        Commands            = $commands
        CommandMap          = $commandMap
        AliasMap            = @{
            'ls'    = 'list'
            'start' = 'serve'
        }
        RootOptions         = @(
            New-OllamaOptionSpec -Tokens @('-h', '--help') -Description 'Help for ollama.'
            New-OllamaOptionSpec -Tokens @('--nowordwrap') -Description 'Don''t wrap words to the next line automatically.'
            New-OllamaOptionSpec -Tokens @('--verbose') -Description 'Show timings for response.'
            New-OllamaOptionSpec -Tokens @('-v', '--version') -Description 'Show version information.'
        )
        LaunchIntegrations  = @(
            [pscustomobject]@{ Name = 'claude'; Description = 'Claude Code' }
            [pscustomobject]@{ Name = 'cline'; Description = 'Cline' }
            [pscustomobject]@{ Name = 'codex'; Description = 'Codex' }
            [pscustomobject]@{ Name = 'copilot'; Description = 'Copilot CLI' }
            [pscustomobject]@{ Name = 'droid'; Description = 'Droid' }
            [pscustomobject]@{ Name = 'hermes'; Description = 'Hermes Agent' }
            [pscustomobject]@{ Name = 'kimi'; Description = 'Kimi Code CLI' }
            [pscustomobject]@{ Name = 'opencode'; Description = 'OpenCode' }
            [pscustomobject]@{ Name = 'openclaw'; Description = 'OpenClaw' }
            [pscustomobject]@{ Name = 'pi'; Description = 'Pi' }
            [pscustomobject]@{ Name = 'pool'; Description = 'Pool' }
            [pscustomobject]@{ Name = 'vscode'; Description = 'VS Code' }
        )
        LaunchIntegrationAliases = @{
            'copilot-cli' = 'copilot'
            'clawdbot'    = 'openclaw'
            'moltbot'     = 'openclaw'
            'code'        = 'vscode'
        }
        EnumValues          = @{
            'run.--think'      = @('true', 'false', 'high', 'medium', 'low')
            'run.--format'     = @('json')
            'run.--truncate'   = @('true', 'false')
            'run.--keepalive'  = @('5m', '10m', '30m', '1h', '0', '-1')
            'create.--quantize' = @('q4_0', 'q4_1', 'q5_0', 'q5_1', 'q8_0', 'q3_K_S', 'q3_K_M', 'q3_K_L', 'q4_K_S', 'q4_K_M', 'q5_K_S', 'q5_K_M', 'q6_K')
        }
        Placeholders        = @{
            'Model'            = @('<model>')
            'SourceModel'      = @('<source-model>')
            'DestinationModel' = @('<destination-model>')
            'Prompt'           = @('<prompt>')
            'ExtraArgs'        = @('<extra-args>')
            'Duration'         = @('<duration>')
            'Quantization'     = @('<quantization>')
            'NegativePrompt'   = @('<negative-prompt>')
            'Number'           = @('<number>')
            'String'           = @('<value>')
            'FilePath'         = @('<path>')
            'CommandName'      = @('<command>')
        }
    }

    Set-Variable -Name OllamaCompletionCatalog -Scope Script -Value $catalog
    $catalog
}

function Resolve-OllamaCommandPath {
    $catalog = Get-OllamaCompletionCatalog
    if (-not [string]::IsNullOrWhiteSpace($catalog.CommandPath)) {
        return $catalog.CommandPath
    }

    $command = Get-Command -Name 'ollama.exe', 'ollama' -ErrorAction Ignore | Select-Object -First 1
    if (-not $command) {
        return $null
    }

    $catalog.CommandPath = if ($command.Source) {
        $command.Source
    } elseif ($command.Path) {
        $command.Path
    } else {
        $command.Name
    }

    $catalog.CommandPath
}

function Invoke-OllamaCaptureText {
    param([string[]]$Arguments, [int]$TimeoutMilliseconds = 1500)

    $commandPath = Resolve-OllamaCommandPath
    if ([string]::IsNullOrWhiteSpace($commandPath)) {
        return ''
    }

    try {
        $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
        $startInfo.FileName = $commandPath
        $startInfo.UseShellExecute = $false
        $startInfo.RedirectStandardInput = $true
        $startInfo.RedirectStandardOutput = $true
        $startInfo.RedirectStandardError = $true
        $startInfo.CreateNoWindow = $true
        $startInfo.Environment['NO_COLOR'] = '1'
        $startInfo.Environment['TERM'] = 'dumb'

        foreach ($argument in @($Arguments)) {
            [void]$startInfo.ArgumentList.Add($argument)
        }

        $process = [System.Diagnostics.Process]::new()
        $process.StartInfo = $startInfo
        [void]$process.Start()
        $process.StandardInput.Close()

        # Read both streams concurrently and never wait longer than the timeout, so a hung ollama.exe cannot block
        # the prompt and a large stderr cannot fill its pipe while stdout is being read.
        $outputTask = $process.StandardOutput.ReadToEndAsync()
        $errorTask = $process.StandardError.ReadToEndAsync()
        if (-not $process.WaitForExit($TimeoutMilliseconds)) {
            try { $process.Kill($true) } catch { Write-Debug -Message $_.Exception.Message }
            return ''
        }

        $standardOutput = ($outputTask.Result -replace '\e\[[0-9;?]*[ -/]*[@-~]', '')
        $standardError = ($errorTask.Result -replace '\e\[[0-9;?]*[ -/]*[@-~]', '')

        $text = ($standardOutput + [Environment]::NewLine + $standardError).Trim()
        if ([string]::IsNullOrWhiteSpace($text)) {
            return ''
        }

        $text
    } catch {
        ''
    }
}

function Split-OllamaLines {
    param([string]$Text)

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

    $Text -split '\r?\n'
}

function Get-OllamaSectionLines {
    param(
        [string]$Text,
        [string]$SectionHeader,
        [string[]]$StopHeaders
    )

    $lines = Split-OllamaLines -Text $Text
    $results = New-Object System.Collections.Generic.List[string]
    $inSection = $false

    foreach ($line in $lines) {
        $trimmed = $line.Trim()

        if (-not $inSection) {
            if ($trimmed -eq $SectionHeader.Trim()) {
                $inSection = $true
            }

            continue
        }

        if (($StopHeaders -contains $trimmed) -or
            (($trimmed -match '^[A-Za-z].*:$') -and ($trimmed -ne $SectionHeader.Trim()))) {
            break
        }

        [void]$results.Add($line)
    }

    @($results.ToArray())
}

function Get-OllamaCommandEntriesFromLines {
    param([string[]]$Lines)

    $results = New-Object System.Collections.Generic.List[object]
    foreach ($line in @($Lines)) {
        if ($line -match '^\s{2,}([a-z][a-z0-9-]+)\s{2,}(.+?)\s*$') {
            [void]$results.Add([pscustomobject]@{
                Name        = $matches[1]
                Description = $matches[2]
            })
        }
    }

    @($results.ToArray())
}

function Get-OllamaFlagEntriesFromLines {
    param([string[]]$Lines)

    # The optional pflag type token is anchored to the pflag vocabulary (with the [="default"] NoOptDefVal suffix)
    # so a flag without a type never loses the first word of its description; the type is kept to derive the
    # value kind.
    $typePattern = '(?:\s+(?<type>(?:string|int|uint\d*|float\d*|bool|duration|stringArray|stringSlice|strings|ints)(?:\[="?[^"\]]*"?\])?))?'
    $results = New-Object System.Collections.Generic.List[object]
    foreach ($line in @($Lines)) {
        if ($line -match ('^\s*(?<short>-[A-Za-z])\s*,\s*(?<long>--[a-z][a-z0-9-]*)' + $typePattern + '\s{2,}(?<desc>.+?)\s*$')) {
            $type = if ($matches.ContainsKey('type')) { $matches.type } else { '' }
            [void]$results.Add([pscustomobject]@{ Token = $matches.short; Description = $matches.desc; Type = $type })
            [void]$results.Add([pscustomobject]@{ Token = $matches.long; Description = $matches.desc; Type = $type })
            continue
        }

        if ($line -match ('^\s*(?<long>--[a-z][a-z0-9-]*)' + $typePattern + '\s{2,}(?<desc>.+?)\s*$')) {
            $type = if ($matches.ContainsKey('type')) { $matches.type } else { '' }
            [void]$results.Add([pscustomobject]@{ Token = $matches.long; Description = $matches.desc; Type = $type })
        }
    }

    @($results.ToArray())
}

function Get-OllamaValueKindFromType {
    param([string]$Type, [string]$OverlayKind)

    # Absent type = bool (only '--flag=false' takes a value); 'type[=default]' = NoOptDefVal, inline form only.
    if ([string]::IsNullOrEmpty($Type)) {
        $kind = if ($OverlayKind -eq 'Boolean') { 'Boolean' } else { $null }
        return [pscustomobject]@{ ValueKind = $kind; InlineOnly = $true }
    }

    $inlineOnly = $Type -match '\[='
    $kind = $OverlayKind
    if ([string]::IsNullOrEmpty($kind)) {
        $kind = switch -Regex ($Type) {
            '^(int|uint|float)' { 'Number'; break }
            '^duration' { 'Duration'; break }
            default { 'String' }
        }
    }

    [pscustomobject]@{ ValueKind = $kind; InlineOnly = $inlineOnly }
}

function Merge-OllamaOptionOverlay {
    param(
        [object[]]$OverlayOptions,
        [object[]]$ParsedOptions
    )

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

    foreach ($parsed in @($ParsedOptions)) {
        $matched = $null
        foreach ($overlay in @($OverlayOptions)) {
            if ($overlay.Token.Equals($parsed.Token, [System.StringComparison]::OrdinalIgnoreCase)) {
                $matched = $overlay
                break
            }
        }

        $type = if ($parsed.PSObject.Properties['Type']) { $parsed.Type } else { '' }
        if ($matched) {
            # Clone so the shared -h/--help objects are never mutated across commands; the overlay only enriches
            # the value kind, the help type token decides whether a value exists and in which form.
            $derived = Get-OllamaValueKindFromType -Type $type -OverlayKind $matched.ValueKind
            $description = if ([string]::IsNullOrWhiteSpace($parsed.Description)) { $matched.Description } else { $parsed.Description }
            $spec = New-OllamaOptionSpec -Tokens @($matched.Token) -Description $description -ValueKind $derived.ValueKind -OptionalValue:$matched.OptionalValue -InlineOnly:$derived.InlineOnly
            if ($seen.Add($spec.Token)) {
                [void]$results.Add($spec)
            }

            continue
        }

        if ($seen.Add($parsed.Token)) {
            $derived = Get-OllamaValueKindFromType -Type $type -OverlayKind $null
            [void]$results.Add((New-OllamaOptionSpec -Tokens @($parsed.Token) -Description $parsed.Description -ValueKind $derived.ValueKind -InlineOnly:$derived.InlineOnly))
        }
    }

    # A successful parse is authoritative: overlay-only flags are stale for the installed build and are dropped.
    if ($results.Count -eq 0) {
        foreach ($overlay in @($OverlayOptions)) {
            if ($seen.Add($overlay.Token)) {
                [void]$results.Add($overlay)
            }
        }
    }

    @($results.ToArray())
}

function Initialize-OllamaCompletionCatalog {
    $catalog = Get-OllamaCompletionCatalog
    if ($catalog.Initialized) {
        return
    }

    $rootHelpText = Invoke-OllamaCaptureText -Arguments @('--help')
    if (-not [string]::IsNullOrWhiteSpace($rootHelpText)) {
        $rootCommandEntries = Get-OllamaCommandEntriesFromLines -Lines (Get-OllamaSectionLines -Text $rootHelpText -SectionHeader 'Available Commands:' -StopHeaders @('Flags:'))
        if ($rootCommandEntries.Count -gt 0) {
            $parsedCommands = New-Object System.Collections.Generic.List[object]
            foreach ($entry in @($rootCommandEntries)) {
                if ($catalog.CommandMap.ContainsKey($entry.Name)) {
                    $spec = $catalog.CommandMap[$entry.Name]
                    $spec.Description = $entry.Description
                    [void]$parsedCommands.Add($spec)
                } else {
                    [void]$parsedCommands.Add((New-OllamaCommandSpec -Name $entry.Name -Description $entry.Description -Positionals @() -Options @() -Aliases @()))
                }
            }

            $catalog.Commands = @($parsedCommands.ToArray())
        }

        $rootFlagEntries = Get-OllamaFlagEntriesFromLines -Lines (Get-OllamaSectionLines -Text $rootHelpText -SectionHeader 'Flags:' -StopHeaders @('Use "ollama [command] --help" for more information about a command.'))
        if ($rootFlagEntries.Count -gt 0) {
            $catalog.RootOptions = Merge-OllamaOptionOverlay -OverlayOptions $catalog.RootOptions -ParsedOptions $rootFlagEntries
        }
    }

    $catalog.Initialized = $true
}

function Resolve-OllamaCommandHelp {
    param([string]$CommandName)

    # Subcommand help is parsed lazily, once per command, the first time completion needs that command.
    $catalog = Get-OllamaCompletionCatalog
    if ([string]::IsNullOrWhiteSpace($CommandName) -or $catalog.ParsedCommands.ContainsKey($CommandName) -or -not $catalog.CommandMap.ContainsKey($CommandName)) {
        return
    }

    $catalog.ParsedCommands[$CommandName] = $true
    $helpText = Invoke-OllamaCaptureText -Arguments @($CommandName, '--help')
    if ([string]::IsNullOrWhiteSpace($helpText)) {
        return
    }

    $flagEntries = New-Object System.Collections.Generic.List[object]
    foreach ($entry in @(Get-OllamaFlagEntriesFromLines -Lines (Get-OllamaSectionLines -Text $helpText -SectionHeader 'Flags:' -StopHeaders @('Image Generation Flags (experimental):', 'Environment Variables:', 'Examples:')))) {
        [void]$flagEntries.Add($entry)
    }

    foreach ($entry in @(Get-OllamaFlagEntriesFromLines -Lines (Get-OllamaSectionLines -Text $helpText -SectionHeader 'Image Generation Flags (experimental):' -StopHeaders @('Environment Variables:', 'Examples:')))) {
        [void]$flagEntries.Add($entry)
    }

    if ($flagEntries.Count -gt 0) {
        $catalog.CommandMap[$CommandName].Options =
            Merge-OllamaOptionOverlay -OverlayOptions $catalog.CommandMap[$CommandName].Options -ParsedOptions @($flagEntries.ToArray())
    }

    if ($CommandName -eq 'launch') {
        $integrationEntries = Get-OllamaCommandEntriesFromLines -Lines (Get-OllamaSectionLines -Text $helpText -SectionHeader 'Supported integrations:' -StopHeaders @('Examples:'))
        if ($integrationEntries.Count -gt 0) {
            $catalog.LaunchIntegrations = @($integrationEntries)
        }
    }
}

function Resolve-OllamaCommandAlias {
    param([string]$Token)

    $catalog = Get-OllamaCompletionCatalog
    if ([string]::IsNullOrWhiteSpace($Token)) {
        return $Token
    }

    if ($catalog.AliasMap.ContainsKey($Token)) {
        return $catalog.AliasMap[$Token]
    }

    $Token
}

function Resolve-OllamaIntegrationAlias {
    param([string]$Token)

    $catalog = Get-OllamaCompletionCatalog
    if ([string]::IsNullOrWhiteSpace($Token)) {
        return $Token
    }

    if ($catalog.LaunchIntegrationAliases.ContainsKey($Token)) {
        return $catalog.LaunchIntegrationAliases[$Token]
    }

    $Token
}

function Get-OllamaQuoteCharacter {
    param([string]$InputText)

    if ([string]::IsNullOrEmpty($InputText)) {
        return $null
    }

    if ($InputText.StartsWith("'", [System.StringComparison]::Ordinal)) {
        return "'"
    }

    if ($InputText.StartsWith('"', [System.StringComparison]::Ordinal)) {
        return '"'
    }

    $null
}

function Remove-OllamaOuterQuotes {
    param([string]$InputText)

    if ([string]::IsNullOrEmpty($InputText)) {
        return ''
    }

    $quoteCharacter = Get-OllamaQuoteCharacter -InputText $InputText
    if ($null -eq $quoteCharacter) {
        return $InputText
    }

    $unquoted = $InputText.Substring(1)
    if ($unquoted.EndsWith($quoteCharacter, [System.StringComparison]::Ordinal)) {
        $unquoted = $unquoted.Substring(0, $unquoted.Length - 1)
    }

    if ($quoteCharacter -eq "'") {
        return $unquoted.Replace("''", "'")
    }

    $unquoted.Replace('`"', '"')
}

function ConvertTo-OllamaQuotedValue {
    param(
        [string]$Value,
        [string]$QuoteCharacter
    )

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

    $effectiveQuote = $QuoteCharacter
    if ([string]::IsNullOrEmpty($effectiveQuote)) {
        $effectiveQuote = if ($Value -match '\s') { '"' } else { '' }
    }

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

    if (($effectiveQuote -eq "'") -and $Value.Contains("'")) {
        $effectiveQuote = '"'
    }

    if ($effectiveQuote -eq '"') {
        return '"' + $Value.Replace('`', '``').Replace('$', '`$').Replace('"', '`"') + '"'
    }

    "'" + $Value.Replace("'", "''") + "'"
}

function Get-OllamaPathCompletions {
    param(
        [string]$InputText,
        [string]$InlinePrefix = '',
        [string]$Placeholder = '<path>'
    )

    $quoteCharacter = Get-OllamaQuoteCharacter -InputText $InputText
    $cleanInput = Remove-OllamaOuterQuotes -InputText $InputText

    $results = New-Object System.Collections.Generic.List[object]
    foreach ($item in [System.Management.Automation.CompletionCompleters]::CompleteFilename($cleanInput)) {
        # CompleteFilename quotes paths with spaces itself; re-quote from the bare path in the user's quote style.
        $completionText = ConvertTo-OllamaQuotedValue -Value (Remove-OllamaOuterQuotes -InputText $item.CompletionText) -QuoteCharacter $quoteCharacter
        if (-not [string]::IsNullOrEmpty($InlinePrefix)) {
            $completionText = $InlinePrefix + $completionText
        }

        [void]$results.Add((
            New-OllamaCompletionResult -CompletionText $completionText -ListItemText $item.ListItemText -ResultType $item.ResultType -ToolTip $item.ToolTip
        ))
    }

    if ($results.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($Placeholder)) {
        [void]$results.AddRange(@(
            Get-OllamaPlaceholderCompletions -Values @($Placeholder) -CurrentWord $InputText -InlinePrefix $InlinePrefix
        ))
    }

    @($results.ToArray())
}

function Get-OllamaPlaceholderCompletions {
    param(
        [string[]]$Values,
        [string]$CurrentWord,
        [string]$InlinePrefix = ''
    )

    $results = New-Object System.Collections.Generic.List[object]
    foreach ($value in @($Values)) {
        $completionText = if ([string]::IsNullOrEmpty($InlinePrefix)) { $value } else { $InlinePrefix + $value }
        $matchPrefix = if ([string]::IsNullOrEmpty($InlinePrefix)) { $CurrentWord } else { $InlinePrefix + $CurrentWord }
        if (-not [string]::IsNullOrEmpty($matchPrefix) -and
            -not $completionText.StartsWith($matchPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        [void]$results.Add((New-OllamaCompletionResult -CompletionText $completionText -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip $value))
    }

    if ($results.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($CurrentWord)) {
        $typedValue = if ([string]::IsNullOrEmpty($InlinePrefix)) { $CurrentWord } else { $InlinePrefix + $CurrentWord }
        [void]$results.Add((New-OllamaCompletionResult -CompletionText $typedValue -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip $typedValue))
    }

    @($results.ToArray())
}

function Get-OllamaEnumCompletions {
    param(
        [string[]]$Values,
        [string]$CurrentWord,
        [string]$InlinePrefix = ''
    )

    $results = New-Object System.Collections.Generic.List[object]
    foreach ($value in @($Values)) {
        $completionText = if ([string]::IsNullOrEmpty($InlinePrefix)) { $value } else { $InlinePrefix + $value }
        $matchPrefix = if ([string]::IsNullOrEmpty($InlinePrefix)) { $CurrentWord } else { $InlinePrefix + $CurrentWord }
        if (-not [string]::IsNullOrEmpty($matchPrefix) -and
            -not $completionText.StartsWith($matchPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        [void]$results.Add((New-OllamaCompletionResult -CompletionText $completionText -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip $value))
    }

    @($results.ToArray())
}

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

    $line = if ($CommandAst.Extent -and $null -ne $CommandAst.Extent.Text) {
        $CommandAst.Extent.Text
    } else {
        ''
    }

    $relativeCursor = if ($CommandAst.Extent) {
        $CursorPosition - $CommandAst.Extent.StartOffset
    } else {
        $CursorPosition
    }

    $line + (' ' * [Math]::Max(0, $relativeCursor - $line.Length))
}

function Get-OllamaTokenState {
    param(
        [string]$Line,
        [int]$CursorPosition,
        [string]$Fallback = ''
    )

    if ([string]::IsNullOrWhiteSpace($Line)) {
        return [pscustomobject]@{
            CurrentWord        = $Fallback
            TokensBeforeCurrent = @()
            AllTokens          = @()
        }
    }

    $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length)
    $prefix = $Line.Substring(0, $safeCursor)
    $hasTrailingSpace = $prefix -match '\s$'
    # A quote still being typed has no partner yet, so both quoted forms accept a missing closing quote.
    $allTokens = @([regex]::Matches($prefix, '"[^"]*"?|''[^'']*''?|\S+') | ForEach-Object { $_.Value })

    if ($hasTrailingSpace) {
        return [pscustomobject]@{
            CurrentWord         = ''
            TokensBeforeCurrent = if ($allTokens.Count -gt 1) { @($allTokens[1..($allTokens.Count - 1)]) } else { @() }
            AllTokens           = $allTokens
        }
    }

    $currentWord = if ($allTokens.Count -gt 0) {
        $allTokens[-1]
    } else {
        $Fallback
    }

    [pscustomobject]@{
        CurrentWord         = $currentWord
        TokensBeforeCurrent = if ($allTokens.Count -gt 2) { @($allTokens[1..($allTokens.Count - 2)]) } else { @() }
        AllTokens           = $allTokens
    }
}

function Get-OllamaCommandSpec {
    param([string]$CommandName)

    $catalog = Get-OllamaCompletionCatalog
    if ([string]::IsNullOrWhiteSpace($CommandName)) {
        return $null
    }

    $canonicalName = Resolve-OllamaCommandAlias -Token $CommandName
    if ($catalog.CommandMap.ContainsKey($canonicalName)) {
        Resolve-OllamaCommandHelp -CommandName $canonicalName
        return $catalog.CommandMap[$canonicalName]
    }

    $null
}

function Get-OllamaOptionSpec {
    param(
        [string]$CommandName,
        [string]$Token
    )

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

    $optionSet = if ([string]::IsNullOrWhiteSpace($CommandName)) {
        (Get-OllamaCompletionCatalog).RootOptions
    } else {
        $commandSpec = Get-OllamaCommandSpec -CommandName $CommandName
        if ($null -eq $commandSpec) {
            @()
        } else {
            $commandSpec.Options
        }
    }

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

    $null
}

function Get-OllamaCommandContext {
    param([string[]]$TokensBeforeCurrent)

    $commandName = $null
    $pendingOption = $null
    $positionals = New-Object System.Collections.Generic.List[string]
    $integration = $null
    $afterDoubleDash = $false
    $helpTarget = $null

    foreach ($token in @($TokensBeforeCurrent)) {
        if ($pendingOption) {
            if ($pendingOption.OptionalValue -and (($token -eq '--') -or $token.StartsWith('-'))) {
                $pendingOption = $null
            } else {
                $pendingOption = $null
                continue
            }
        }

        if (-not $commandName) {
            if ($token -eq '--') {
                continue
            }

            if ($token -match '^(--[^=]+)=') {
                continue
            }

            if ($token.StartsWith('-')) {
                $rootOption = Get-OllamaOptionSpec -CommandName $null -Token $token
                if ($rootOption -and $rootOption.ValueKind -and ($token -notlike '*=*')) {
                    $pendingOption = $rootOption
                }

                continue
            }

            $commandName = Resolve-OllamaCommandAlias -Token $token
            # Resolving the spec parses that command's help lazily on first use.
            [void](Get-OllamaCommandSpec -CommandName $commandName)
            continue
        }

        if ($commandName -eq 'launch' -and $token -eq '--') {
            $afterDoubleDash = $true
            break
        }

        $inlineToken = $null
        if ($token -match '^(--[^=]+)=') {
            $inlineToken = $matches[1]
        }

        $optionToken = if ($inlineToken) { $inlineToken } else { $token }
        $optionSpec = Get-OllamaOptionSpec -CommandName $commandName -Token $optionToken
        if ($optionSpec) {
            # NoOptDefVal flags (--think, --truncate) never consume the next token; only --flag=value is legal.
            if (($null -eq $inlineToken) -and $optionSpec.ValueKind -and -not $optionSpec.InlineOnly) {
                $pendingOption = $optionSpec
            }

            continue
        }

        if ($token.StartsWith('-')) {
            continue
        }

        switch ($commandName) {
            'help' {
                if (-not $helpTarget) {
                    $helpTarget = Resolve-OllamaCommandAlias -Token $token
                }
            }
            'launch' {
                if (-not $integration) {
                    $integration = Resolve-OllamaIntegrationAlias -Token $token
                }
            }
            default {
                [void]$positionals.Add($token)
            }
        }
    }

    [pscustomobject]@{
        CommandName      = $commandName
        PendingOption    = $pendingOption
        Positionals      = @($positionals.ToArray())
        Integration      = $integration
        AfterDoubleDash  = $afterDoubleDash
        HelpTarget       = $helpTarget
    }
}

function Add-OllamaUniqueResults {
    param(
        [System.Collections.Generic.List[object]]$Target,
        [object[]]$Items
    )

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($existing in @($Target.ToArray())) {
        [void]$seen.Add($existing.CompletionText)
    }

    foreach ($item in @($Items)) {
        if ($null -eq $item) {
            continue
        }

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

function Get-OllamaOptionCompletions {
    param(
        [string]$CommandName,
        [string]$CurrentWord
    )

    $optionSet = if ([string]::IsNullOrWhiteSpace($CommandName)) {
        (Get-OllamaCompletionCatalog).RootOptions
    } else {
        $commandSpec = Get-OllamaCommandSpec -CommandName $CommandName
        if ($null -eq $commandSpec) {
            @()
        } else {
            $commandSpec.Options
        }
    }

    foreach ($option in @($optionSet)) {
        if (-not [string]::IsNullOrEmpty($CurrentWord) -and
            -not $option.Token.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        New-OllamaCompletionResult -CompletionText $option.Token -ResultType ([System.Management.Automation.CompletionResultType]::ParameterName) -ToolTip $option.Description

        # An inline-only flag typed in full also offers its legal --flag=value forms.
        if ($option.InlineOnly -and $option.ValueKind -and $option.Token.Equals($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
            foreach ($item in @(Get-OllamaValueCompletions -CommandName $CommandName -ValueKind $option.ValueKind -CurrentWord '' -InlinePrefix ($option.Token + '='))) {
                $item
            }
        }
    }
}

function Get-OllamaRootCommandCompletions {
    param([string]$CurrentWord)

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

    foreach ($command in @($catalog.Commands)) {
        if ([string]::IsNullOrEmpty($CurrentWord) -or
            $command.Name.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
            [void]$results.Add((
                New-OllamaCompletionResult -CompletionText $command.Name -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip $command.Description
            ))
        }

        foreach ($alias in @($command.Aliases)) {
            if (-not [string]::IsNullOrEmpty($CurrentWord) -and
                -not $alias.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
                continue
            }

            [void]$results.Add((
                New-OllamaCompletionResult -CompletionText $alias -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip ('Alias for {0} - {1}' -f $command.Name, $command.Description)
            ))
        }
    }

    @($results.ToArray())
}

function Get-OllamaLaunchIntegrationCompletions {
    param([string]$CurrentWord)

    foreach ($integration in @((Get-OllamaCompletionCatalog).LaunchIntegrations)) {
        if (-not [string]::IsNullOrEmpty($CurrentWord) -and
            -not $integration.Name.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        New-OllamaCompletionResult -CompletionText $integration.Name -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -ToolTip $integration.Description
    }
}

function Get-OllamaValueCompletions {
    param(
        [string]$CommandName,
        [string]$ValueKind,
        [string]$CurrentWord,
        [string]$InlinePrefix = ''
    )

    $catalog = Get-OllamaCompletionCatalog

    switch ($ValueKind) {
        'Think' {
            return @(Get-OllamaEnumCompletions -Values $catalog.EnumValues['run.--think'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
        'Format' {
            return @(Get-OllamaEnumCompletions -Values $catalog.EnumValues['run.--format'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
        'Boolean' {
            return @(Get-OllamaEnumCompletions -Values @('true', 'false') -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
        'FilePath' {
            return @(Get-OllamaPathCompletions -InputText $CurrentWord -InlinePrefix $InlinePrefix -Placeholder '<path>')
        }
        'Model' {
            return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Model'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
        'Duration' {
            $durations = @(Get-OllamaEnumCompletions -Values $catalog.EnumValues['run.--keepalive'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
            if ($durations.Count -gt 0) { return $durations }
            return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Duration'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
        'Quantization' {
            $levels = @(Get-OllamaEnumCompletions -Values $catalog.EnumValues['create.--quantize'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
            if ($levels.Count -gt 0) { return $levels }
            return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Quantization'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
        'String' {
            return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['String'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
        'NegativePrompt' {
            return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['NegativePrompt'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
        'Number' {
            return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Number'] -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix)
        }
    }

    @()
}

function Get-OllamaPositionalCompletions {
    param(
        [string]$CommandName,
        [string]$CurrentWord,
        [string[]]$Positionals,
        [string]$Integration
    )

    $catalog = Get-OllamaCompletionCatalog

    switch ($CommandName) {
        'run' {
            if ($Positionals.Count -eq 0) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Model'] -CurrentWord $CurrentWord)
            }

            if ($Positionals.Count -eq 1) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Prompt'] -CurrentWord $CurrentWord)
            }
        }
        'create' {
            if ($Positionals.Count -eq 0) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Model'] -CurrentWord $CurrentWord)
            }
        }
        'show' {
            if ($Positionals.Count -eq 0) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Model'] -CurrentWord $CurrentWord)
            }
        }
        'stop' {
            if ($Positionals.Count -eq 0) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Model'] -CurrentWord $CurrentWord)
            }
        }
        'pull' {
            if ($Positionals.Count -eq 0) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Model'] -CurrentWord $CurrentWord)
            }
        }
        'push' {
            if ($Positionals.Count -eq 0) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Model'] -CurrentWord $CurrentWord)
            }
        }
        'cp' {
            if ($Positionals.Count -eq 0) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['SourceModel'] -CurrentWord $CurrentWord)
            }

            if ($Positionals.Count -eq 1) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['DestinationModel'] -CurrentWord $CurrentWord)
            }
        }
        'rm' {
            if ($Positionals.Count -eq 0) {
                return @(Get-OllamaPlaceholderCompletions -Values $catalog.Placeholders['Model'] -CurrentWord $CurrentWord)
            }
        }
        'help' {
            return @(Get-OllamaRootCommandCompletions -CurrentWord $CurrentWord)
        }
        'launch' {
            if (-not $Integration) {
                return @(Get-OllamaLaunchIntegrationCompletions -CurrentWord $CurrentWord)
            }
        }
    }

    @()
}

function Complete-Ollama {
    param(
        [string]$wordToComplete,
        [System.Management.Automation.Language.CommandAst]$commandAst,
        [int]$cursorPosition
    )

    Initialize-OllamaCompletionCatalog

    $completionLine = Get-OllamaCompletionLine -CommandAst $commandAst -CursorPosition $cursorPosition
    $relativeCursor = if ($commandAst.Extent) {
        $cursorPosition - $commandAst.Extent.StartOffset
    } else {
        $cursorPosition
    }

    $tokenState = Get-OllamaTokenState -Line $completionLine -CursorPosition $relativeCursor -Fallback $wordToComplete
    $currentWord = $tokenState.CurrentWord
    $tokensBeforeCurrent = @($tokenState.TokensBeforeCurrent)

    if (($tokensBeforeCurrent.Count -eq 0) -and ($currentWord -match '^(?i)ollama(?:\.exe)?$')) {
        $currentWord = ''
    }

    $context = Get-OllamaCommandContext -TokensBeforeCurrent $tokensBeforeCurrent
    if ($context.AfterDoubleDash) {
        return @(Get-OllamaPlaceholderCompletions -Values (Get-OllamaCompletionCatalog).Placeholders['ExtraArgs'] -CurrentWord $currentWord)
    }

    if ($currentWord -match '^(--[^=]+)=(.*)$') {
        $optionToken = $matches[1]
        $valuePrefix = $matches[2]
        $optionSpec = Get-OllamaOptionSpec -CommandName $context.CommandName -Token $optionToken
        if ($optionSpec -and $optionSpec.ValueKind) {
            return @(Get-OllamaValueCompletions -CommandName $context.CommandName -ValueKind $optionSpec.ValueKind -CurrentWord $valuePrefix -InlinePrefix ($optionToken + '='))
        }
    }

    $currentWordStartsOption = (-not [string]::IsNullOrEmpty($currentWord)) -and $currentWord.StartsWith('-')
    if ($context.PendingOption -and -not ($context.PendingOption.OptionalValue -and $currentWordStartsOption)) {
        return @(Get-OllamaValueCompletions -CommandName $context.CommandName -ValueKind $context.PendingOption.ValueKind -CurrentWord $currentWord)
    }

    if ($currentWord -eq '--' -and $context.CommandName -eq 'launch') {
        return @()
    }

    if ($currentWordStartsOption) {
        return @(Get-OllamaOptionCompletions -CommandName $context.CommandName -CurrentWord $currentWord)
    }

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

    if (-not $context.CommandName) {
        Add-OllamaUniqueResults -Target $results -Items @(Get-OllamaRootCommandCompletions -CurrentWord $currentWord)
        if ([string]::IsNullOrEmpty($currentWord)) {
            Add-OllamaUniqueResults -Target $results -Items @(Get-OllamaOptionCompletions -CommandName $null -CurrentWord '')
        }

        return @($results.ToArray())
    }

    Add-OllamaUniqueResults -Target $results -Items @(
        Get-OllamaPositionalCompletions -CommandName $context.CommandName -CurrentWord $currentWord -Positionals $context.Positionals -Integration $context.Integration
    )

    if ([string]::IsNullOrEmpty($currentWord)) {
        Add-OllamaUniqueResults -Target $results -Items @(Get-OllamaOptionCompletions -CommandName $context.CommandName -CurrentWord '')
    }

    @($results.ToArray())
}

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

    Complete-Ollama -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}