completers/claude_completer/claude_completer.ps1

<#
.SYNOPSIS
    Argument completer for claude / claude.exe (Claude Code CLI).
.DESCRIPTION
    Provides tab completion for claude subcommands, flags, flag values, and the
    --flag=value inline syntax. Dot-source this file from your $PROFILE to
    enable completion for both invocation forms.

    Safe to source multiple times (re-registration simply replaces the completer).
.EXAMPLE
    . "$PSScriptRoot\claude_completer.ps1"
#>


Set-StrictMode -Version Latest

function Initialize-ClaudeCompleterData {
    if (Get-Variable -Name ClaudeTopCommands -Scope Script -ErrorAction Ignore) {
        return
    }

    # Top-level subcommands.
    $script:ClaudeTopCommands = @(
        'agents', 'attach', 'auth', 'auto-mode', 'doctor', 'gateway', 'import',
        'install', 'logs', 'mcp', 'plugin', 'purge', 'respawn', 'rm',
        'setup-token', 'stop', 'ultrareview', 'update'
    )

    # Command alias normalisation (alias -> canonical) applied when walking args.
    # Top-level: plugins == plugin, upgrade == update, kill == stop.
    $script:ClaudeCmdAliases = @{
        'plugins' = 'plugin'
        'upgrade' = 'update'
        'kill'    = 'stop'
    }

    # Level-2 subcommand aliases keyed by "cmd": alias -> canonical.
    $script:ClaudeSubAliases = @{
        'plugin' = @{
            'i'          = 'install'
            'new'        = 'init'
            'remove'     = 'uninstall'
            'autoremove' = 'prune'
        }
    }

    # Level-3 subcommand aliases keyed by "cmd.subcmd": alias -> canonical.
    $script:ClaudeL3Aliases = @{
        'plugin.marketplace' = @{
            'rm' = 'remove'
        }
    }

    # Level-2 subcommands per top-level command.
    $script:ClaudeSubSubcommands = @{
        'auth'      = @('login', 'logout', 'status', 'help')
        'auto-mode' = @('config', 'critique', 'defaults', 'reset', 'help')
        'mcp'       = @('add', 'add-json', 'add-from-claude-desktop', 'get',
                         'list', 'login', 'logout', 'remove', 'reset-project-choices',
                         'serve', 'help')
        'plugin'    = @('configure', 'details', 'disable', 'enable', 'eval', 'init', 'install',
                         'list', 'marketplace', 'prune', 'tag', 'test', 'uninstall', 'update',
                         'validate', 'help')
        # Commands with no L2 subcommands (own options or positionals only).
        'agents'      = @()
        'attach'      = @()
        'doctor'      = @()
        'gateway'     = @()
        'import'      = @()
        'install'     = @()
        'logs'        = @()
        'purge'       = @()
        'respawn'     = @()
        'rm'          = @()
        'setup-token' = @()
        'stop'        = @()
        'ultrareview' = @()
        'update'      = @()
    }

    # Level-3 subcommands: "cmd.subcmd" -> @(subsubcmds).
    $script:ClaudeL3Subcommands = @{
        'plugin.marketplace' = @('add', 'list', 'remove', 'update', 'help')
    }

    # ---- Flag categories -----------------------------------------------------------------------

    # Global enum flags with fixed choice sets.
    $script:ClaudeEnumFlags = @{
        '--effort'                 = @('low', 'medium', 'high', 'xhigh', 'max')
        '--permission-mode'        = @('acceptEdits', 'auto', 'bypassPermissions',
                                        'manual', 'dontAsk', 'plan')
        '--output-format'          = @('text', 'json', 'stream-json')
        '--input-format'           = @('text', 'stream-json')
        '--prompt-suggestions'     = @('true', 'false', '1', '0', 'yes', 'no', 'on', 'off')
        '--autocompact'            = @('auto')
        '--permission-prompts'     = @('host', 'none')
        '--system-prompt-snapshot' = @('on', 'off')
    }

    # Boolean / switch flags (accept no value).
    $script:ClaudeBoolFlags = @(
        '--allow-dangerously-skip-permissions'
        '--ax-screen-reader'
        '--background'
        '--bare'
        '--bg'
        '--brief'
        '--chrome'
        '--continue'
        '--dangerously-skip-permissions'
        '--disable-slash-commands'
        '--exclude-dynamic-system-prompt-sections'
        '--fork-session'
        '--forward-subagent-text'
        '--help'
        '--ide'
        '--include-hook-events'
        '--include-partial-messages'
        '--no-chrome'
        '--no-session-persistence'
        '--print'
        '--replay-user-messages'
        '--restricted'
        '--safe-mode'
        '--strict-mcp-config'
        '--tmux'
        '--verbose'
        '--version'
    )

    # Optional-value flags: behave like booleans for token-consumption purposes
    # (do NOT consume the next token as a value), but still offer enum/value
    # completion when the user explicitly types --flag=.
    $script:ClaudeOptionalValueFlags = @(
        '--cloud'
        '--debug'
        '--resume'
        '--teleport'
        '--worktree'
        '--remote-control'
        '--from-pr'
        '--prompt-suggestions'
    )

    # Free-form string flags (single value, no fixed choices).
    $script:ClaudeStringFlags = @(
        '--agent'
        '--agents'
        '--append-system-prompt'
        '--environment'
        '--json-schema'
        '--name'
        '--remote-control-session-name-prefix'
        '--session-id'
        '--system-prompt'
    )

    # Numeric flags.
    $script:ClaudeNumberFlags = @(
        '--max-budget-usd'
    )

    # Array / multi-value flags.
    $script:ClaudeArrayFlags = @(
        '--add-dir'
        '--allowedTools'
        '--allowed-tools'
        '--betas'
        '--disallowedTools'
        '--disallowed-tools'
        '--file'
        '--mcp-config'
        '--plugin-dir'
        '--plugin-url'
        '--setting-sources'
        '--settings'
        '--tools'
    )

    # Flags whose value is a model name (free-form, but with hints). The aliases and the
    # full-name example come from 'claude --help' ('fable', 'opus', 'sonnet', 'claude-fable-5').
    $script:ClaudeModelFlags = @('--model', '--fallback-model')
    $script:ClaudeModelHints = @(
        'fable', 'opus', 'sonnet', 'haiku',
        'claude-fable-5'
    )

    # Flags whose value has comma-separated hints (free-form).
    $script:ClaudeHintFlags = @{
        '--setting-sources' = @('user', 'project', 'local')
    }

    # Flags that produce file-path completion.
    $script:ClaudePathFlags = @(
        '--debug-file'
        '--mcp-config'
        '--settings'
    )

    # Flags that produce directory-path completion.
    $script:ClaudeDirFlags = @(
        '--add-dir'
        '--plugin-dir'
    )

    # ---- Context tables ------------------------------------------------------------------------

    # L2/L3 context-specific flags (beyond the global set): "cmd.subcmd" or
    # "cmd.subcmd.subsubcmd" -> @(flags).
    $script:ClaudeContextFlags = @{
        'auth.login'                  = @('--claudeai', '--console', '--email', '--sso')
        'auth.status'                 = @('--json', '--text')
        'auto-mode.critique'          = @('--model')
        'mcp.add'                     = @('--callback-port', '--client-id', '--client-secret',
                                           '--env', '--header', '--scope', '--transport')
        'mcp.add-json'                = @('--client-secret', '--scope')
        'mcp.add-from-claude-desktop' = @('--scope')
        'mcp.login'                   = @('--no-browser')
        'mcp.remove'                  = @('--scope')
        'mcp.serve'                   = @('--debug', '--verbose')
        'plugin.configure'            = @('--json', '--values-stdin')
        'plugin.disable'              = @('--all', '--json', '--scope')
        'plugin.enable'               = @('--json', '--scope')
        'plugin.eval'                 = @('--ablation', '--allow-real-servers', '--allow-tools', '--case',
                                           '--concurrency', '--eval-dir', '--json', '--judge-model',
                                           '--keep-temp', '--max-cost-usd', '--mocks', '--model',
                                           '--no-publish', '--no-scaffold', '--output-dir',
                                           '--publish-report', '--report', '--runs', '--scaffold',
                                           '--tag', '--threshold', '--trust-plugin', '--verbose')
        'plugin.init'                 = @('--author', '--author-email', '--description', '--force', '--with')
        'plugin.install'              = @('--accept-command', '--config', '--json', '--marketplace',
                                           '--registry', '--scope', '--yes')
        'plugin.list'                 = @('--available', '--data-size', '--json')
        'plugin.prune'                = @('--dry-run', '--scope', '--yes')
        'plugin.tag'                  = @('--dry-run', '--force', '--message', '--push', '--remote')
        'plugin.uninstall'            = @('--json', '--keep-data', '--prune', '--scope', '--yes')
        'plugin.update'               = @('--accept-command', '--json', '--scope', '--yes')
        'plugin.validate'             = @('--json', '--strict')
        'plugin.marketplace.add'      = @('--claudeai', '--json', '--scope', '--sparse')
        'plugin.marketplace.list'     = @('--json')
        'plugin.marketplace.remove'   = @('--json', '--scope')
        'plugin.marketplace.update'   = @('--json')
    }

    # Flags for commands that take flags directly (no L2 subcommand): "cmd" -> @(flags).
    $script:ClaudeCmdContextFlags = @{
        'agents'      = @('--add-dir', '--agent', '--all', '--allow-dangerously-skip-permissions',
                           '--cwd', '--dangerously-skip-permissions', '--effort', '--json',
                           '--mcp-config', '--model', '--permission-mode', '--plugin-dir',
                           '--restricted', '--setting-sources', '--settings', '--strict-mcp-config')
        'gateway'     = @('--config')
        'import'      = @('--dry-run', '--yes')
        'install'     = @('--force')
        'purge'       = @('--all', '--dry-run', '--interactive', '--yes')
        'respawn'     = @('--all')
        'rm'          = @('--discard-unpushed', '--force-remove-worktree')
        'ultrareview' = @('--json', '--no-post', '--post', '--timeout')
    }

    # Context-specific boolean flags (take no value when used in a subcommand).
    # 'plugin list --data-size [plugin]' takes an optional value, so like the global
    # optional-value flags it never consumes the next word and stays unregistered.
    $script:ClaudeContextBoolFlagSet = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::Ordinal
    )
    @('--claudeai', '--console', '--sso', '--json', '--text', '--client-secret',
      '--verbose', '--debug', '--all', '--available', '--dry-run', '--yes',
      '--force', '--push', '--strict', '--interactive', '--no-browser', '--post',
      '--no-post', '--restricted', '--allow-real-servers', '--keep-temp',
      '--no-publish', '--no-scaffold', '--publish-report', '--scaffold',
      '--trust-plugin', '--keep-data', '--prune', '--values-stdin') |
        ForEach-Object { $null = $script:ClaudeContextBoolFlagSet.Add($_) }

    # Context-specific enum values: "cmd.subcmd.--flag" or "cmd.subcmd.subsubcmd.--flag" -> @(choices).
    $script:ClaudeContextEnumFlags = @{
        'mcp.add.--scope'                     = @('local', 'user', 'project')
        'mcp.add.--transport'                 = @('stdio', 'sse', 'http')
        'mcp.add-json.--scope'                = @('local', 'user', 'project')
        'mcp.add-from-claude-desktop.--scope' = @('local', 'user', 'project')
        'mcp.remove.--scope'                  = @('local', 'user', 'project')
        'plugin.disable.--scope'              = @('user', 'project', 'local')
        'plugin.enable.--scope'               = @('user', 'project', 'local')
        'plugin.eval.--ablation'              = @('none', 'with-without')
        'plugin.eval.--mocks'                 = @('record', 'off')
        'plugin.install.--scope'              = @('user', 'project', 'local')
        'plugin.prune.--scope'                = @('user', 'project', 'local')
        'plugin.uninstall.--scope'            = @('user', 'project', 'local')
        'plugin.update.--scope'               = @('user', 'project', 'local', 'managed')
        'plugin.marketplace.add.--scope'      = @('user', 'project', 'local')
        'plugin.marketplace.remove.--scope'   = @('user', 'project', 'local')
    }

    # Context-specific value-taking (string/number) flags.
    $script:ClaudeContextValueFlagSet = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::Ordinal
    )
    @('--callback-port', '--client-id', '--email', '--config', '--message',
      '--remote', '--timeout', '--discard-unpushed', '--force-remove-worktree',
      '--allow-tools', '--case', '--concurrency', '--eval-dir', '--judge-model',
      '--max-cost-usd', '--output-dir', '--report', '--runs', '--tag',
      '--threshold', '--author', '--author-email', '--description', '--with',
      '--accept-command', '--marketplace', '--registry', '--sparse',
      '--env', '--header') |
        ForEach-Object { $null = $script:ClaudeContextValueFlagSet.Add($_) }

    # Context-specific numeric flags.
    $script:ClaudeContextNumberFlagSet = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::Ordinal
    )
    @('--callback-port', '--timeout', '--max-budget-usd', '--concurrency',
      '--max-cost-usd', '--runs', '--threshold') |
        ForEach-Object { $null = $script:ClaudeContextNumberFlagSet.Add($_) }

    # Context short aliases keyed by "cmd.subcmd" or "cmd".
    $script:ClaudeContextShortAliases = @{
        'mcp.add'      = @{ '-e' = '--env'; '-H' = '--header'; '-s' = '--scope'; '-t' = '--transport' }
        'mcp.add-json' = @{ '-s' = '--scope' }
        'mcp.add-from-claude-desktop' = @{ '-s' = '--scope' }
        'mcp.remove'   = @{ '-s' = '--scope' }
        'mcp.serve'    = @{ '-d' = '--debug' }
        'plugin.disable'  = @{ '-a' = '--all'; '-s' = '--scope' }
        'plugin.enable'   = @{ '-s' = '--scope' }
        'plugin.eval'     = @{ '-j' = '--concurrency' }
        'plugin.init'     = @{ '-f' = '--force' }
        'plugin.install'  = @{ '-s' = '--scope'; '-y' = '--yes' }
        'plugin.prune'    = @{ '-s' = '--scope'; '-y' = '--yes' }
        'plugin.tag'      = @{ '-f' = '--force'; '-m' = '--message' }
        'plugin.uninstall' = @{ '-s' = '--scope'; '-y' = '--yes' }
        'plugin.update'   = @{ '-s' = '--scope'; '-y' = '--yes' }
        'purge'           = @{ '-i' = '--interactive'; '-y' = '--yes' }
    }

    # Short flags are case-distinct ('mcp add -H' is --header, '-h' is --help), so the
    # context alias maps compare keys ordinally.
    foreach ($ctxKey in @($script:ClaudeContextShortAliases.Keys)) {
        $ordinal = [System.Collections.Generic.Dictionary[string, string]]::new(
            [System.StringComparer]::Ordinal
        )
        foreach ($alias in $script:ClaudeContextShortAliases[$ctxKey].GetEnumerator()) {
            $ordinal[$alias.Key] = $alias.Value
        }
        $script:ClaudeContextShortAliases[$ctxKey] = $ordinal
    }

    # Positional path completion for specific subcommand paths (first positional).
    $script:ClaudePathPositionalL2 = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::Ordinal
    )
    @('plugin.tag', 'plugin.test', 'plugin.validate', 'purge') |
        ForEach-Object { $null = $script:ClaudePathPositionalL2.Add($_) }

    # The subset of those path slots that take a directory ('plugin test [dir]', 'purge [path]').
    $script:ClaudeDirPositionalL2 = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::Ordinal
    )
    @('plugin.test', 'purge') |
        ForEach-Object { $null = $script:ClaudeDirPositionalL2.Add($_) }

    # Positional enum values for specific subcommand paths.
    $script:ClaudePositionalEnums = @{
        'install' = @('stable', 'latest')
        'import'  = @('codex', 'gemini', 'cursor')
    }

    # Placeholder values for free-form positional slots.
    $script:ClaudePositionalPlaceholders = @{
        'attach'                        = @('<id>')
        'install'                       = @('<target>')
        'logs'                          = @('<id>')
        'respawn'                       = @('<id>')
        'rm'                            = @('<id>')
        'stop'                          = @('<id>')
        'ultrareview'                   = @('<target>')
        'mcp.add'                       = @('<name>', '<commandOrUrl>', '<arg>')
        'mcp.add-json'                  = @('<name>', '<json>')
        'mcp.get'                       = @('<name>')
        'mcp.login'                     = @('<name>')
        'mcp.logout'                    = @('<name>')
        'mcp.remove'                    = @('<name>')
        'plugin.configure'              = @('<plugin>')
        'plugin.details'                = @('<name>')
        'plugin.disable'                = @('<plugin>')
        'plugin.enable'                 = @('<plugin>')
        'plugin.eval'                   = @('<target>')
        'plugin.init'                   = @('<name>')
        'plugin.install'                = @('<plugin>')
        'plugin.uninstall'              = @('<plugin>')
        'plugin.update'                 = @('<plugin>')
        'plugin.marketplace.add'        = @('<source>')
        'plugin.marketplace.remove'     = @('<name>')
        'plugin.marketplace.update'     = @('<name>')
    }

    # ---- Global short alias maps ---------------------------------------------------------------

    $script:ClaudeShortAliases = @{
        '-c' = '--continue'
        '-d' = '--debug'
        '-h' = '--help'
        '-n' = '--name'
        '-p' = '--print'
        '-r' = '--resume'
        '-v' = '--version'
        '-w' = '--worktree'
    }

    # Reverse map (long -> short) built once at load time.
    $script:ClaudeAliasReverse = @{}
    $script:ClaudeShortAliases.GetEnumerator() |
        ForEach-Object { $script:ClaudeAliasReverse[$_.Value] = $_.Key }

    $script:ClaudeContextAliasReverse = @{}
    foreach ($entry in $script:ClaudeContextShortAliases.GetEnumerator()) {
        $reverse = @{}
        foreach ($alias in $entry.Value.GetEnumerator()) {
            $reverse[$alias.Value] = $alias.Key
        }
        $script:ClaudeContextAliasReverse[$entry.Key] = $reverse
    }

    # ---- Descriptions --------------------------------------------------------------------------

    $script:ClaudeCmdDesc = @{
        'agents'      = 'Manage background agents'
        'attach'      = 'Open a background session in this terminal'
        'auth'        = 'Manage authentication'
        'auto-mode'   = 'Inspect or reset auto mode classifier configuration'
        'doctor'      = 'Diagnose and verify the installation'
        'gateway'     = 'Run the enterprise auth/telemetry gateway'
        'import'      = 'Import config from another AI coding agent'
        'install'     = 'Install the native Claude Code build'
        'logs'        = 'Print a background session''s recent terminal output'
        'mcp'         = 'Configure and manage MCP servers'
        'plugin'      = 'Manage plugins and marketplaces'
        'purge'       = 'Delete all Claude Code state for a project (transcripts, tasks, file history, config entry)'
        'respawn'     = 'Restart a background session (or all with --all)'
        'rm'          = 'Delete a background session and its worktree'
        'setup-token' = 'Set up a long-lived authentication token'
        'stop'        = 'Stop a background session (alias: kill)'
        'ultrareview' = 'Run an ultra-deep code review'
        'update'      = 'Update Claude Code to the latest version'
    }

    $script:ClaudeSubcmdDesc = @{
        'auth.login'                  = 'Log in to Claude'
        'auth.logout'                 = 'Log out of Claude'
        'auth.status'                 = 'Show current authentication status'
        'auth.help'                   = 'Show help for auth'
        'auto-mode.config'            = 'Configure auto mode'
        'auto-mode.critique'          = 'Run an auto-mode critique'
        'auto-mode.defaults'          = 'Show or reset auto-mode defaults'
        'auto-mode.reset'             = 'Reset auto mode configuration to the shipped defaults'
        'auto-mode.help'              = 'Show help for auto-mode'
        'mcp.add'                     = 'Add an MCP server'
        'mcp.add-json'                = 'Add an MCP server from a JSON definition'
        'mcp.add-from-claude-desktop' = 'Import MCP servers from Claude Desktop'
        'mcp.get'                     = 'Show details for an MCP server'
        'mcp.list'                    = 'List configured MCP servers'
        'mcp.login'                   = 'Authenticate with an MCP server'
        'mcp.logout'                  = 'Clear stored OAuth credentials for an MCP server'
        'mcp.remove'                  = 'Remove an MCP server'
        'mcp.reset-project-choices'   = 'Reset per-project MCP trust choices'
        'mcp.serve'                   = 'Run Claude Code as an MCP server'
        'mcp.help'                    = 'Show help for mcp'
        'plugin.configure'            = 'Show a plugin''s options and which are unset, or save values from stdin'
        'plugin.details'              = 'Show details for a plugin'
        'plugin.disable'              = 'Disable a plugin'
        'plugin.enable'               = 'Enable a plugin'
        'plugin.eval'                 = 'Run eval cases against a plugin and report scored results'
        'plugin.init'                 = 'Scaffold a new plugin (alias: new)'
        'plugin.install'              = 'Install a plugin (alias: i)'
        'plugin.list'                 = 'List plugins'
        'plugin.marketplace'          = 'Manage plugin marketplaces'
        'plugin.prune'                = 'Remove orphaned plugins (alias: autoremove)'
        'plugin.tag'                  = 'Tag a plugin release'
        'plugin.test'                 = 'Run a mod''s tests'
        'plugin.uninstall'            = 'Uninstall a plugin (alias: remove)'
        'plugin.update'               = 'Update a plugin'
        'plugin.validate'             = 'Validate a plugin or marketplace manifest, or a directory'
        'plugin.help'                 = 'Show help for plugin'
        'plugin.marketplace.add'      = 'Add a marketplace source'
        'plugin.marketplace.list'     = 'List configured marketplaces'
        'plugin.marketplace.remove'   = 'Remove a marketplace (alias: rm)'
        'plugin.marketplace.update'   = 'Update marketplace metadata'
        'plugin.marketplace.help'     = 'Show help for marketplace'
    }

    $script:ClaudeFlagDesc = @{
        # Global boolean flags
        '--allow-dangerously-skip-permissions'      = '[boolean] Allow bypassing all permission checks'
        '--ax-screen-reader'                         = '[boolean] Render screen-reader friendly output'
        '--background'                               = '[boolean] Start the session in the background and print its id (--bg)'
        '--bg'                                       = '[boolean] Start the session in the background and print its id (--background)'
        '--bare'                                     = '[boolean] Minimal output mode'
        '--brief'                                    = '[boolean] Brief output mode'
        '--chrome'                                   = '[boolean] Use Chrome integration'
        '--continue'                                 = '[boolean] Continue the most recent conversation'
        '--dangerously-skip-permissions'            = '[boolean] Skip permission prompts (dangerous)'
        '--disable-slash-commands'                  = '[boolean] Disable slash commands'
        '--exclude-dynamic-system-prompt-sections'  = '[boolean] Exclude dynamic system prompt sections'
        '--fork-session'                            = '[boolean] Fork into a new session'
        '--forward-subagent-text'                   = '[boolean] Forward subagent text and thinking blocks (stream-json only)'
        '--help'                                     = '[boolean] Show help'
        '--ide'                                      = '[boolean] Connect to an IDE'
        '--include-hook-events'                     = '[boolean] Include hook events in output'
        '--include-partial-messages'                = '[boolean] Include partial assistant messages'
        '--no-chrome'                                = '[boolean] Disable Chrome integration'
        '--no-session-persistence'                  = '[boolean] Do not persist the session'
        '--print'                                    = '[boolean] Print response and exit (-p)'
        '--replay-user-messages'                    = '[boolean] Replay user messages'
        '--safe-mode'                                = '[boolean] Start with all customizations disabled'
        '--strict-mcp-config'                       = '[boolean] Use only the specified MCP config'
        '--tmux'                                     = '[boolean] Use tmux integration'
        '--verbose'                                  = '[boolean] Verbose output'
        '--version'                                  = '[boolean] Show version'
        # Optional-value flags
        '--cloud'                                    = '[optional] Create a cloud session (description) or attach to one (session id or URL)'
        '--debug'                                    = '[optional] Enable debug mode (optional filter)'
        '--resume'                                   = '[optional] Resume a session (optional id)'
        '--teleport'                                 = '[optional] Resume a teleport session (optional session id)'
        '--worktree'                                 = '[optional] Use a git worktree (optional name)'
        '--remote-control'                          = '[optional] Enable remote control (optional name)'
        '--from-pr'                                  = '[optional] Start from a PR (optional value)'
        '--prompt-suggestions'                      = '[optional] Prompt suggestions: true|false|...'
        # Global enum flags
        '--autocompact'                              = '[string] Auto-compact window size: auto, or 100k-1M tokens'
        '--effort'                                   = '[string] Reasoning effort: low|medium|high|xhigh|max'
        '--permission-mode'                          = '[string] Permission mode: acceptEdits|auto|bypassPermissions|manual|dontAsk|plan'
        '--permission-prompts'                       = '[string] Who answers permission prompts with --print: host|none'
        '--output-format'                            = '[string] Output format: text|json|stream-json'
        '--input-format'                             = '[string] Input format: text|stream-json'
        '--system-prompt-snapshot'                   = '[string] Record the system prompt once per conversation: on|off'
        # Global string flags
        '--agent'                                    = '[string] Agent to run'
        '--agents'                                   = '[string] Agents definition (json)'
        '--append-system-prompt'                    = '[string] Append text to the system prompt'
        '--environment'                              = '[string] Self-hosted cloud environment id (ccpool_...)'
        '--fallback-model'                           = '[string] Fallback model(s), comma-separated, tried in order'
        '--json-schema'                              = '[string] JSON schema for structured output'
        '--name'                                     = '[string] Session or run name (-n)'
        '--remote-control-session-name-prefix'      = '[string] Remote-control session name prefix'
        '--session-id'                               = '[string] Session ID (uuid)'
        '--system-prompt'                            = '[string] Override the system prompt'
        '--model'                                    = '[string] Model to use'
        # Numeric flags
        '--max-budget-usd'                           = '[number] Maximum budget in USD'
        # Array flags
        '--add-dir'                                  = '[array] Additional working directory'
        '--allowedTools'                            = '[array] Allowed tools'
        '--allowed-tools'                            = '[array] Allowed tools'
        '--betas'                                    = '[array] API beta features'
        '--disallowedTools'                         = '[array] Disallowed tools'
        '--disallowed-tools'                        = '[array] Disallowed tools'
        '--file'                                     = '[array] Input file(s)'
        '--mcp-config'                               = '[array] MCP config file or JSON'
        '--plugin-dir'                               = '[array] Plugin directory'
        '--plugin-url'                               = '[array] Plugin URL'
        '--setting-sources'                          = '[array] Setting sources: user,project,local'
        '--settings'                                 = '[array] Settings file or JSON'
        '--tools'                                    = '[array] Tools definition'
        # Context-specific flags
        '--claudeai'                                 = '[boolean] Use claude.ai login'
        '--console'                                  = '[boolean] Use Anthropic Console login'
        '--email'                                    = '[string] Email address for login'
        '--sso'                                      = '[boolean] Use SSO login'
        '--json'                                     = '[boolean] JSON output'
        '--text'                                     = '[boolean] Text output'
        '--callback-port'                            = '[number] OAuth callback port'
        '--client-id'                                = '[string] OAuth client ID'
        '--client-secret'                            = '[boolean] Prompt for an OAuth client secret'
        '--env'                                      = '[array] Environment variable: KEY=value'
        '--header'                                   = '[array] HTTP header'
        '--scope'                                    = '[string] Configuration scope'
        '--transport'                                = '[string] MCP transport: stdio|sse|http'
        '--all'                                      = '[boolean] Apply to all'
        '--available'                                = '[boolean] Show available plugins'
        '--config'                                   = '[string] Plugin config: key=value'
        '--dry-run'                                  = '[boolean] Show what would happen without doing it'
        '--yes'                                      = '[boolean] Assume yes to prompts'
        '--force'                                    = '[boolean] Force the operation'
        '--message'                                  = '[string] Tag message'
        '--push'                                     = '[boolean] Push the tag'
        '--remote'                                   = '[string] Git remote name'
        '--strict'                                   = '[boolean] Strict validation'
        '--interactive'                              = '[boolean] Interactive mode'
        '--cwd'                                      = '[string] Working directory'
        '--timeout'                                  = '[number] Timeout in minutes'
        '--no-browser'                               = '[boolean] Print the authorization URL instead of opening a browser'
        '--post'                                     = '[boolean] Post the findings to the PR'
        '--no-post'                                  = '[boolean] Do not post the findings to the PR'
        '--restricted'                               = '[boolean] Restricted mode: remove the command/code-running tools and WebFetch (agents: start dispatched sessions restricted)'
        '--discard-unpushed'                         = '[string] <commit>@<worktree-id> to discard unpushed commits'
        '--force-remove-worktree'                    = '[string] <worktree-id> to delete even if removal failed'
        '--ablation'                                 = '[string] Baseline arm: none|with-without'
        '--mocks'                                    = '[string] MCP mock stand-ins: record|off'
        '--concurrency'                              = '[number] Concurrent agent runs (1-8, -j)'
        '--eval-dir'                                 = '[string] Directory holding the eval cases'
        '--trust-plugin'                             = '[boolean] Skip the first-run trust prompt'
        '--author'                                   = '[string] Author name'
        '--author-email'                             = '[string] Author email'
        '--description'                              = '[string] Manifest description'
        '--with'                                     = '[array] Components to scaffold: skills, agents, hooks, mcp, ...'
        '--accept-command'                           = '[string] sha256 of the marketplace-declared command to accept'
        '--marketplace'                              = '[string] Marketplace source to install from: owner/repo, git/https URL, or path'
        '--registry'                                 = '[string] npm registry URL for a <package>@npm install'
        '--keep-data'                                = '[boolean] Preserve the plugin''s persistent data directory'
        '--prune'                                    = '[boolean] Also remove auto-installed dependencies no longer needed'
        '--data-size'                                = '[optional] Measure installed plugins'' data directories, or one plugin''s (requires --json)'
        '--values-stdin'                             = '[boolean] Read option values from stdin as a JSON object'
        '--sparse'                                   = '[array] Limit checkout to these directories (git sparse-checkout)'
    }
}

#region -- Helpers ------------------------------------------------------------------------------

function New-ClaudeCompletion {
    param(
        [Parameter(Mandatory)]
        [string]$CompletionText,

        [string]$ListItemText  = '',
        [string]$Tooltip       = '',

        [System.Management.Automation.CompletionResultType]
        $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue
    )
    if ([string]::IsNullOrEmpty($ListItemText)) { $ListItemText = $CompletionText }
    if ([string]::IsNullOrEmpty($Tooltip))      { $Tooltip      = $ListItemText  }
    [System.Management.Automation.CompletionResult]::new(
        $CompletionText, $ListItemText, $ResultType, $Tooltip
    )
}

# The value of a typed word. A word opened with a quote (ASCII or typographic) is read by
# the PowerShell tokenizer, which drops the quotes and undoes that quote style's escapes.
function ConvertFrom-ClaudeTypedWord {
    param([string]$Value)

    if ($Value -notmatch '^[''"\u2018-\u201E]') {
        return $Value
    }

    $tokens    = $null
    $parseErrs = $null
    [void][System.Management.Automation.Language.Parser]::ParseInput($Value, [ref]$tokens, [ref]$parseErrs)
    $tokens[0].Value
}

# Renders a value as one PowerShell argument: bare when safe and no quote was typed,
# otherwise in the quote the user typed (single by default). PowerShell reads ' and
# U+2018-U+201B as single quotes and " and U+201C-U+201E as double quotes.
function ConvertTo-ClaudeQuotedValue {
    param(
        [string]$Value,
        [string]$QuoteChar = ''
    )

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

    if (-not $QuoteChar) {
        if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]') { return $Value }
        $QuoteChar = "'"
    }

    if ($QuoteChar -match '^[''\u2018-\u201B]$') {
        return $QuoteChar + ($Value -replace '([''\u2018-\u201B])', '$1$1') + $QuoteChar
    }

    $QuoteChar + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + $QuoteChar
}

# File-system candidates for a path slot. CompleteFilename output is already quoted for
# PowerShell and wildcard-escaped (tick``x.txt), so each result is unwrapped by the parser
# and unescaped, then quoted once in the style the user typed. An inline '--flag=' prefix
# stays bare in front of the quoted value, which keeps the word one constant argument.
function Get-ClaudePathCompletions {
    param(
        [string]$WordToComplete,
        [string]$InlinePrefix = '',
        [switch]$DirectoriesOnly,
        [switch]$AllowZip
    )

    $quoteChar = if ($WordToComplete -match '^[''"\u2018-\u201E]') { $WordToComplete.Substring(0, 1) } else { '' }
    $value     = ConvertFrom-ClaudeTypedWord -Value $WordToComplete

    foreach ($item in [System.Management.Automation.CompletionCompleters]::CompleteFilename($value)) {
        $path = $item.CompletionText
        if ($path -match '^[''"\u2018-\u201E]') {
            $ast      = [System.Management.Automation.Language.Parser]::ParseInput($path, [ref]$null, [ref]$null)
            $constant = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true)
            if ($constant) { $path = $constant.Value }
        }
        $path = [System.Management.Automation.WildcardPattern]::Unescape($path)

        $isContainer = $item.ResultType -eq [System.Management.Automation.CompletionResultType]::ProviderContainer
        if ($DirectoriesOnly -and -not $isContainer -and -not ($AllowZip -and $path -like '*.zip')) { continue }

        New-ClaudeCompletion "$InlinePrefix$(ConvertTo-ClaudeQuotedValue -Value $path -QuoteChar $quoteChar)" `
            -ListItemText $item.ListItemText `
            -ResultType   $item.ResultType `
            -Tooltip      $item.ToolTip
    }
}

# Normalises a command/subcommand alias to its canonical name.
function Resolve-ClaudeCmdAlias {
    param([string]$Token)
    $script:ClaudeCmdAliases[$Token] ?? $Token
}

function Resolve-ClaudeSubAlias {
    param([string]$Sub, [string]$Token)
    $map = $script:ClaudeSubAliases[$Sub]
    if ($map -and $map.ContainsKey($Token)) { return $map[$Token] }
    return $Token
}

function Resolve-ClaudeL3Alias {
    param([string]$Sub, [string]$SubSub, [string]$Token)
    $map = $script:ClaudeL3Aliases["$Sub.$SubSub"]
    if ($map -and $map.ContainsKey($Token)) { return $map[$Token] }
    return $Token
}

function Resolve-ClaudeFlagName {
    param(
        [string]$FlagName,
        [string]$Sub,
        [string]$SubSub
    )
    foreach ($ctxKey in @(
        if ($Sub -and $SubSub) { "$Sub.$SubSub" }
        if ($Sub) { $Sub }
    )) {
        $ctxMap = $script:ClaudeContextShortAliases[$ctxKey]
        if ($ctxMap -and $ctxMap.ContainsKey($FlagName)) {
            return $ctxMap[$FlagName]
        }
    }
    $script:ClaudeShortAliases[$FlagName] ?? $FlagName
}

function Get-ClaudeShortAlias {
    param(
        [string]$FlagName,
        [string]$Sub,
        [string]$SubSub
    )
    foreach ($ctxKey in @(
        if ($Sub -and $SubSub) { "$Sub.$SubSub" }
        if ($Sub) { $Sub }
    )) {
        $ctxReverse = $script:ClaudeContextAliasReverse[$ctxKey]
        if ($ctxReverse -and $ctxReverse.ContainsKey($FlagName)) {
            return $ctxReverse[$FlagName]
        }
    }
    return $script:ClaudeAliasReverse[$FlagName]
}

# Returns $true when the flag consumes the next token as its value.
function Test-ClaudeFlagTakesValue {
    param(
        [string]$FlagName,
        [string]$Sub,
        [string]$SubSub,
        [string]$Sub3
    )
    $r = Resolve-ClaudeFlagName -FlagName $FlagName -Sub $Sub -SubSub $SubSub

    # Optional-value flags never consume the next token (conservative).
    if ($script:ClaudeOptionalValueFlags -contains $r) { return $false }

    # Global enum flags always take a value.
    if ($null -ne $script:ClaudeEnumFlags[$r]) { return $true }

    # Global value-taking flag categories.
    if ($script:ClaudeStringFlags -contains $r) { return $true }
    if ($script:ClaudeNumberFlags -contains $r) { return $true }
    if ($script:ClaudeArrayFlags  -contains $r) { return $true }
    if ($script:ClaudeModelFlags  -contains $r) { return $true }
    if ($script:ClaudePathFlags   -contains $r) { return $true }
    if ($script:ClaudeDirFlags    -contains $r) { return $true }

    # Global boolean flags never take a value.
    if ($script:ClaudeBoolFlags   -contains $r) { return $false }

    # Context-specific enum flags.
    if ($Sub -and $SubSub) {
        $k2 = if ($Sub3) { "$Sub.$SubSub.$Sub3.$r" } else { "$Sub.$SubSub.$r" }
        if ($null -ne $script:ClaudeContextEnumFlags[$k2]) { return $true }
    }

    # Context-specific value-taking (string/number) flags.
    if ($script:ClaudeContextValueFlagSet.Contains($r)) { return $true }

    # Context-specific boolean flags.
    if ($script:ClaudeContextBoolFlagSet.Contains($r)) { return $false }

    return $false
}

# Returns the known enum choices for a flag in the given context, or $null.
function Get-ClaudeEnumValues {
    param(
        [string]$FlagName,
        [string]$Sub,
        [string]$SubSub,
        [string]$Sub3
    )
    $r = Resolve-ClaudeFlagName -FlagName $FlagName -Sub $Sub -SubSub $SubSub

    # Global enum first.
    $vals = $script:ClaudeEnumFlags[$r]
    if ($vals) { return $vals }

    # L2 / L3 context.
    if ($Sub -and $SubSub) {
        $ctxKey = if ($Sub3) { "$Sub.$SubSub.$Sub3" } else { "$Sub.$SubSub" }
        $vals = $script:ClaudeContextEnumFlags["$ctxKey.$r"]
        if ($vals) { return $vals }
    }

    return $null
}

# Builds the full flag set for the current command context.
function Get-ClaudeFlagSet {
    param([string]$Sub, [string]$SubSub, [string]$Sub3)

    $set = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::Ordinal
    )

    # Root command: the full global flag surface.
    if (-not $Sub) {
        $script:ClaudeEnumFlags.Keys      | ForEach-Object { $null = $set.Add($_) }
        $script:ClaudeBoolFlags            | ForEach-Object { $null = $set.Add($_) }
        $script:ClaudeOptionalValueFlags   | ForEach-Object { $null = $set.Add($_) }
        $script:ClaudeStringFlags          | ForEach-Object { $null = $set.Add($_) }
        $script:ClaudeNumberFlags          | ForEach-Object { $null = $set.Add($_) }
        $script:ClaudeArrayFlags           | ForEach-Object { $null = $set.Add($_) }
        $script:ClaudeModelFlags           | ForEach-Object { $null = $set.Add($_) }
        $script:ClaudePathFlags            | ForEach-Object { $null = $set.Add($_) }
        $script:ClaudeDirFlags             | ForEach-Object { $null = $set.Add($_) }
        return $set
    }

    # Commander subcommands do not inherit root options: only the context
    # table for this exact command level plus --help applies.
    $null = $set.Add('--help')
    if ($SubSub) {
        $ctxKey = if ($Sub3) { "$Sub.$SubSub.$Sub3" } else { "$Sub.$SubSub" }
        $ctxFlags = $script:ClaudeContextFlags[$ctxKey]
        if ($ctxFlags) { $ctxFlags | ForEach-Object { $null = $set.Add($_) } }
    } else {
        $cmdFlags = $script:ClaudeCmdContextFlags[$Sub]
        if ($cmdFlags) { $cmdFlags | ForEach-Object { $null = $set.Add($_) } }
    }
    return $set
}

function Get-ClaudePositionalPlaceholder {
    param(
        [string]$ContextKey,
        [int]$PositionIndex
    )
    $placeholders = $script:ClaudePositionalPlaceholders[$ContextKey]
    if (-not $placeholders) { return $null }
    if ($PositionIndex -lt $placeholders.Count) {
        return $placeholders[$PositionIndex]
    }
    return $placeholders[-1]
}

function Write-ClaudeLongFlagResults {
    param(
        [string]$WordToComplete,
        [string]$Sub,
        [string]$SubSub,
        [string]$Sub3
    )
    $flagSet = Get-ClaudeFlagSet -Sub $Sub -SubSub $SubSub -Sub3 $Sub3
    foreach ($flag in $flagSet) {
        if ($flag -notlike ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) { continue }
        $desc = $script:ClaudeFlagDesc[$flag] ?? ''
        $short = Get-ClaudeShortAlias -FlagName $flag -Sub $Sub -SubSub $SubSub
        $tip = if ($short) { "$flag (alias: $short): $desc" } else { "${flag}: $desc" }
        New-ClaudeCompletion $flag -ResultType ParameterName -Tooltip $tip
    }
}

# Emits value completion for a value-taking flag (enum / model / hint / path / number / generic).
function Write-ClaudeFlagValue {
    param(
        [string]$Flag,
        [string]$WordToComplete,
        [string]$Sub,
        [string]$SubSub,
        [string]$Sub3,
        [string]$InlinePrefix = ''   # e.g. "--flag=" for inline syntax, '' for space-separated
    )
    $enumVals = Get-ClaudeEnumValues -FlagName $Flag -Sub $Sub -SubSub $SubSub -Sub3 $Sub3
    if ($enumVals) {
        $enumVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
            $tip = ($script:ClaudeFlagDesc[$Flag] ?? $_)
            if ($InlinePrefix) {
                New-ClaudeCompletion "$InlinePrefix$_" -ListItemText $_ -Tooltip $tip
            } else {
                New-ClaudeCompletion $_ -Tooltip $tip
            }
        }
        return
    }

    # Model hints.
    if ($script:ClaudeModelFlags -contains $Flag) {
        $script:ClaudeModelHints | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
            if ($InlinePrefix) {
                New-ClaudeCompletion "$InlinePrefix$_" -ListItemText $_ -Tooltip "Model: $_"
            } else {
                New-ClaudeCompletion $_ -Tooltip "Model: $_"
            }
        }
        return
    }

    # Comma-separated hint flags.
    $hints = $script:ClaudeHintFlags[$Flag]
    if ($hints) {
        $hints | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
            if ($InlinePrefix) {
                New-ClaudeCompletion "$InlinePrefix$_" -ListItemText $_ -Tooltip "Value: $_"
            } else {
                New-ClaudeCompletion $_ -Tooltip "Value: $_"
            }
        }
        return
    }

    # Path / dir completion. --add-dir takes directories only; --plugin-dir a directory or a .zip.
    if ($script:ClaudePathFlags -contains $Flag -or $script:ClaudeDirFlags -contains $Flag) {
        Get-ClaudePathCompletions -WordToComplete $WordToComplete -InlinePrefix $InlinePrefix `
            -DirectoriesOnly:($script:ClaudeDirFlags -contains $Flag) `
            -AllowZip:($Flag -eq '--plugin-dir')
        return
    }

    # Number placeholder.
    if ($script:ClaudeNumberFlags -contains $Flag -or
        $script:ClaudeContextNumberFlagSet.Contains($Flag)) {
        if ('' -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-ClaudeCompletion '<n>' -Tooltip "Numeric value for $Flag"
        }
        return
    }

    # Generic placeholder — suppresses filesystem fallback for string/array flags.
    if ('' -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
        New-ClaudeCompletion '<value>' -Tooltip "Value for $Flag"
    }
}

#endregion

#region -- Completer scriptblock ----------------------------------------------------------------

function Complete-ClaudeNative {
    param(
        $CommandName,
        $ParameterName,
        $WordToComplete,
        $CommandAst,
        $FakeBoundParameter
    )

    Initialize-ClaudeCompleterData

    # -------------------------------------------------------------------------
    # Detect the native ReadLine calling convention vs TabExpansion2.
    #
    # Native convention (ReadLine):
    # A) after trailing space: $CommandName='', $ParameterName='<full line>',
    # $WordToComplete='<cursor col>'
    # B) mid-token: $CommandName='<partial>', $ParameterName='<full line>',
    # $WordToComplete='<cursor col>'
    #
    # TabExpansion2 native path:
    # $CommandName='<wordToComplete>', $ParameterName=<CommandAst object>,
    # $WordToComplete='<cursorPosition int>'
    #
    # Heuristic: $WordToComplete is a pure integer AND $ParameterName coerces
    # to something that looks like a command line (starts with a non-space word).
    # -------------------------------------------------------------------------
    $isNativeConvention = $WordToComplete -match '^\d+$' -and
                          $ParameterName  -match '^\s*\S+'

    # Line-absolute cursor offset (native convention only); $null otherwise.
    $cursorCol = $null
    # True when the cursor sits inside (or at the end of) the last element left of it: that
    # element is the word being completed, never an already-committed argument.
    $cursorInsideWord = $false

    if ($isNativeConvention) {
        $nativePartialWord = $CommandName
        $cursorCol         = [int]$WordToComplete

        # $ParameterName is a CommandAst (TabExpansion2) or a string (ReadLine).
        if ($ParameterName -is [System.Management.Automation.Language.CommandAst]) {
            # TabExpansion2: we already have the CommandAst — reuse it directly.
            $CommandAst = $ParameterName
        } else {
            $tokens  = $null
            $parseErrs = $null
            $parsedAst = [System.Management.Automation.Language.Parser]::ParseInput(
                             [string]$ParameterName, [ref]$tokens, [ref]$parseErrs)
            if ($parsedAst.EndBlock.Statements.Count -gt 0) {
                $pipeline = $parsedAst.EndBlock.Statements[0]
                if ($pipeline -is [System.Management.Automation.Language.PipelineAst] -and
                    $pipeline.PipelineElements.Count -gt 0 -and
                    $pipeline.PipelineElements[0] -is [System.Management.Automation.Language.CommandAst]) {
                    $CommandAst = $pipeline.PipelineElements[0]
                }
            }
        }

        if ($null -ne $CommandAst -and $CommandAst.CommandElements.Count -gt 1) {
            # Everything is derived from the text LEFT of the cursor. $cursorCol is
            # line-absolute while Extent.Text is command-relative, so rebase first.
            $line      = $CommandAst.Extent.Text
            $relCursor = $cursorCol - $CommandAst.Extent.StartOffset
            $hasTrailingSpace = ($relCursor -le 0) -or
                                ($relCursor -gt $line.Length) -or
                                [char]::IsWhiteSpace($line[$relCursor - 1])

            $cursorInsideWord = -not $hasTrailingSpace

            if ($hasTrailingSpace) {
                $WordToComplete = ''
            } elseif (-not [string]::IsNullOrEmpty($nativePartialWord)) {
                $WordToComplete = $nativePartialWord
            } else {
                $pfx = $line.Substring(0, $relCursor)
                $spc = $pfx.LastIndexOf(' ')
                $WordToComplete = if ($spc -ge 0) { $pfx.Substring($spc + 1) } else { '' }
            }
        } else {
            $WordToComplete = ''
        }
    }

    if ($null -eq $WordToComplete) { $WordToComplete = '' }
    if ($null -eq $CommandAst)     { return }

    $allElements = @($CommandAst.CommandElements)
    if ($null -ne $cursorCol) {
        # Ignore every element that starts at or after the cursor.
        $allElements = @($allElements | Where-Object { $_.Extent.StartOffset -lt $cursorCol })
    }
    if ($allElements.Count -eq 0) { return }

    # -------------------------------------------------------------------------
    # Build the committed argument list. Use Extent.Text for all nodes to stay
    # safe under StrictMode (CommandParameterAst may lack a .Value property).
    # -------------------------------------------------------------------------
    function Get-ClaudeTokenText {
        param($el)
        $el.Extent.Text
    }

    $allArgs = @(foreach ($el in ($allElements | Select-Object -Skip 1)) {
        Get-ClaudeTokenText -el $el
    })

    # The word being completed is never a committed argument, flag or not: keeping a
    # value-taking flag such as '--effort' in the list made the walk expect its value
    # and the flag name itself could no longer be completed. Once the user types the
    # space after it, the flag is left of the cursor and becomes committed normally.
    $committedArgs = $allArgs
    if ($allArgs.Count -gt 0) {
        $lastIsCurrentWord = if ($null -ne $cursorCol) {
            $cursorInsideWord
        } else {
            $allArgs[-1] -eq $WordToComplete
        }

        if ($lastIsCurrentWord) {
            $committedArgs = @($allArgs | Select-Object -First ($allArgs.Count - 1))
        }
    }

    # -------------------------------------------------------------------------
    # State machine: walk committed args to determine context.
    # Tracks up to 3 command levels plus expectingValue / currentFlag.
    # Command aliases are normalised as they are matched.
    # -------------------------------------------------------------------------
    $sub    = $null   # e.g. 'mcp'
    $subsub = $null   # e.g. 'add'
    $sub3   = $null   # e.g. 'add' under plugin.marketplace
    $expectingValue  = $false
    $currentFlag     = $null
    $positionalCount = 0     # non-flag positionals seen after sub/subsub/sub3

    foreach ($token in $committedArgs) {
        if ($expectingValue) {
            $expectingValue = $false
            $currentFlag    = $null
            continue
        }

        if ($token -like '-*') {
            if ($token -like '*=*') { continue }    # inline --flag=value; value already consumed
            if (Test-ClaudeFlagTakesValue -FlagName $token -Sub $sub -SubSub $subsub -Sub3 $sub3) {
                $expectingValue = $true
                $currentFlag    = Resolve-ClaudeFlagName -FlagName $token -Sub $sub -SubSub $subsub
            }
            continue
        }

        # Positional token — try to match command levels first.
        $matched = $false

        if ($null -eq $sub) {
            $canon = Resolve-ClaudeCmdAlias -Token $token
            if ($script:ClaudeTopCommands -contains $canon) {
                $sub     = $canon
                $matched = $true
            }
        } elseif ($null -eq $subsub) {
            $subs = $script:ClaudeSubSubcommands[$sub]
            $canon = Resolve-ClaudeSubAlias -Sub $sub -Token $token
            if ($subs -and $subs -contains $canon) {
                $subsub  = $canon
                $matched = $true
            }
        } elseif ($null -eq $sub3) {
            $l3k  = "$sub.$subsub"
            $l3cs = $script:ClaudeL3Subcommands[$l3k]
            $canon = Resolve-ClaudeL3Alias -Sub $sub -SubSub $subsub -Token $token
            if ($l3cs -and $l3cs -contains $canon) {
                $sub3    = $canon
                $matched = $true
            }
        }

        if (-not $matched) { $positionalCount++ }
    }

    # =========================================================================
    # 1. Inline --flag=value completion
    # =========================================================================
    if ($WordToComplete -like '*=*') {
        $eqIdx    = $WordToComplete.IndexOf('=')
        $flagPart = $WordToComplete.Substring(0, $eqIdx)
        $valPfx   = $WordToComplete.Substring($eqIdx + 1)
        $resolved = Resolve-ClaudeFlagName -FlagName $flagPart -Sub $sub -SubSub $subsub

        Write-ClaudeFlagValue -Flag $resolved -WordToComplete $valPfx `
            -Sub $sub -SubSub $subsub -Sub3 $sub3 -InlinePrefix "$flagPart="
        return
    }

    # =========================================================================
    # 2. Value completion after space-separated value-taking flag
    # =========================================================================
    if ($expectingValue) {
        Write-ClaudeFlagValue -Flag $currentFlag -WordToComplete $WordToComplete `
            -Sub $sub -SubSub $subsub -Sub3 $sub3
        return
    }

    # =========================================================================
    # 3. Flag completion (word starts with - or --)
    # =========================================================================
    if ($WordToComplete -like '-*') {
        $flagSet = Get-ClaudeFlagSet -Sub $sub -SubSub $subsub -Sub3 $sub3
        $isShort = $WordToComplete -notlike '--*'

        foreach ($flag in $flagSet) {
            $desc  = $script:ClaudeFlagDesc[$flag] ?? ''
            $short = Get-ClaudeShortAlias -FlagName $flag -Sub $sub -SubSub $subsub

            if ($isShort) {
                if (-not $short) { continue }
                if ($short -notlike ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) { continue }
                New-ClaudeCompletion $short `
                    -ResultType ParameterName `
                    -Tooltip    "$short -> ${flag}: $desc"
            } else {
                if ($flag -notlike ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) { continue }
                $tip = if ($short) { "$flag (alias: $short): $desc" } else { "${flag}: $desc" }
                New-ClaudeCompletion $flag -ResultType ParameterName -Tooltip $tip
            }
        }
        return
    }

    # =========================================================================
    # 4. Subcommand / positional completion
    # =========================================================================

    # --- 4a. Have sub + subsub → offer L3 subcommands (or positional values). ---
    if ($sub -and $subsub -and $null -eq $sub3) {
        $l3k  = "$sub.$subsub"
        $l3cs = $script:ClaudeL3Subcommands[$l3k]
        if ($l3cs) {
            $l3cs | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                $tip = $script:ClaudeSubcmdDesc["$sub.$subsub.$_"] ?? $_
                New-ClaudeCompletion $_ -Tooltip $tip
            }
            return
        }

        # No L3 commands → offer positional enum / path if applicable.
        $emitted = $false
        $posVals = $script:ClaudePositionalEnums[$l3k]
        if ($posVals) {
            $posVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                New-ClaudeCompletion $_ -Tooltip "Positional: $_"
                $emitted = $true
            }
        }
        if ($script:ClaudePathPositionalL2.Contains($l3k) -and $positionalCount -eq 0) {
            Get-ClaudePathCompletions -WordToComplete $WordToComplete `
                -DirectoriesOnly:($script:ClaudeDirPositionalL2.Contains($l3k))
            $emitted = $true
        }

        $placeholder = Get-ClaudePositionalPlaceholder -ContextKey $l3k -PositionIndex $positionalCount
        if ($placeholder -and $placeholder -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-ClaudeCompletion $placeholder -Tooltip "Positional value for $l3k"
            $emitted = $true
        }

        if ([string]::IsNullOrEmpty($WordToComplete)) {
            Write-ClaudeLongFlagResults -WordToComplete '' -Sub $sub -SubSub $subsub
            return
        }

        if ($emitted) {
            return
        }
        return
    }

    # --- 4b. Have sub + subsub + sub3 → positional values only. ---
    if ($sub -and $subsub -and $sub3) {
        $posKey  = "$sub.$subsub.$sub3"
        $emitted = $false
        $posVals = $script:ClaudePositionalEnums[$posKey]
        if ($posVals) {
            $posVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                New-ClaudeCompletion $_ -Tooltip "Positional: $_"
                $emitted = $true
            }
        }
        $placeholder = Get-ClaudePositionalPlaceholder -ContextKey $posKey -PositionIndex $positionalCount
        if ($placeholder -and $placeholder -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-ClaudeCompletion $placeholder -Tooltip "Positional value for $posKey"
            $emitted = $true
        }
        if ([string]::IsNullOrEmpty($WordToComplete)) {
            Write-ClaudeLongFlagResults -WordToComplete '' -Sub $sub -SubSub $subsub -Sub3 $sub3
            return
        }
        if ($emitted) {
            return
        }
        return
    }

    # --- 4c. Have sub only → offer L2 subcommands (or positional / cmd flags). ---
    if ($sub -and $null -eq $subsub) {
        $subs = $script:ClaudeSubSubcommands[$sub]
        if ($subs -and $subs.Count -gt 0) {
            $subs | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                $tip = $script:ClaudeSubcmdDesc["$sub.$_"] ?? $_
                New-ClaudeCompletion $_ -Tooltip $tip
            }
            return
        }

        # Command with no L2 subcommands → positional enum / path / placeholder.
        $emitted = $false
        $posVals = $script:ClaudePositionalEnums[$sub]
        if ($posVals) {
            $posVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                New-ClaudeCompletion $_ -Tooltip "Positional: $_"
                $emitted = $true
            }
        }
        if ($script:ClaudePathPositionalL2.Contains($sub) -and $positionalCount -eq 0) {
            Get-ClaudePathCompletions -WordToComplete $WordToComplete `
                -DirectoriesOnly:($script:ClaudeDirPositionalL2.Contains($sub))
            $emitted = $true
        }
        $placeholder = Get-ClaudePositionalPlaceholder -ContextKey $sub -PositionIndex $positionalCount
        if ($placeholder -and $placeholder -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-ClaudeCompletion $placeholder -Tooltip "Positional value for $sub"
            $emitted = $true
        }

        if ([string]::IsNullOrEmpty($WordToComplete)) {
            Write-ClaudeLongFlagResults -WordToComplete '' -Sub $sub -SubSub $subsub
            return
        }
        if ($emitted) { return }
        return
    }

    # --- 4d. Top level → offer L1 subcommands. ---
    $script:ClaudeTopCommands | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
        $tip = $script:ClaudeCmdDesc[$_] ?? $_
        New-ClaudeCompletion $_ -Tooltip $tip
    }
}

#endregion

#region -- Registration -------------------------------------------------------------------------

Register-ArgumentCompleter -CommandName @('claude', 'claude.exe') -Native -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Complete-ClaudeNative -CommandName $commandName -ParameterName $parameterName -WordToComplete $wordToComplete -CommandAst $commandAst -FakeBoundParameter $fakeBoundParameter
}

#endregion