completers/opencode_completer/opencode_completer.ps1
|
# opencode_completer.ps1 # PowerShell argument completer for opencode and opencode.exe. # Static-first: the command tree, per-command options and enum values are transcribed from the yargs help of # opencode 1.18.30 ('opencode --help' and 'opencode <command> --help'). Set-StrictMode -Version Latest function New-OpencodeOption { param( [string[]]$Tokens, [string]$Description, [string]$Kind = 'Flag', [string[]]$Values = @(), [string]$Placeholder = '' ) [pscustomobject]@{ Tokens = @($Tokens) Description = $Description Kind = $Kind Values = @($Values) Placeholder = $Placeholder } } function New-OpencodePositional { param( [string]$Name, [string]$Description, [string]$Kind = 'Placeholder', [string]$Placeholder = '' ) [pscustomobject]@{ Name = $Name Description = $Description Kind = $Kind Placeholder = if ($Placeholder) { $Placeholder } else { "<$Name>" } } } function New-OpencodeCommand { param( [string]$Name, [string]$Description, [string[]]$Aliases = @(), [object[]]$Options = @(), [object[]]$Positionals = @(), [object[]]$Subcommands = @() ) [pscustomobject]@{ Name = $Name Description = $Description Aliases = @($Aliases) Options = @($Options) Positionals = @($Positionals) Subcommands = @($Subcommands) } } function Get-OpencodeCompletionCatalog { $existing = Get-Variable -Name OpencodeCompletionCatalog -Scope Script -ErrorAction Ignore if ($existing) { return $existing.Value } $logLevel = New-OpencodeOption -Tokens @('--log-level') -Description 'log level' -Kind 'Enum' -Values @('DEBUG', 'INFO', 'WARN', 'ERROR') $common = @( New-OpencodeOption -Tokens @('-h', '--help') -Description 'show help' New-OpencodeOption -Tokens @('-v', '--version') -Description 'show version number' New-OpencodeOption -Tokens @('--print-logs') -Description 'print logs to stderr' $logLevel New-OpencodeOption -Tokens @('--pure') -Description 'run without external plugins' ) $server = @( New-OpencodeOption -Tokens @('--port') -Description 'port to listen on [default: 0]' -Kind 'Number' -Placeholder '<port>' New-OpencodeOption -Tokens @('--hostname') -Description 'hostname to listen on [default: "127.0.0.1"]' -Kind 'Enum' -Values @('127.0.0.1', '0.0.0.0', 'localhost') New-OpencodeOption -Tokens @('--mdns') -Description 'enable mDNS service discovery (defaults hostname to 0.0.0.0)' New-OpencodeOption -Tokens @('--mdns-domain') -Description 'custom domain name for mDNS service (default: opencode.local)' -Kind 'String' -Placeholder '<domain>' New-OpencodeOption -Tokens @('--cors') -Description 'additional domains to allow for CORS' -Kind 'String' -Placeholder '<domain>' ) $session = @( New-OpencodeOption -Tokens @('-c', '--continue') -Description 'continue the last session' New-OpencodeOption -Tokens @('-s', '--session') -Description 'session id to continue' -Kind 'Session' -Placeholder '<session-id>' New-OpencodeOption -Tokens @('--fork') -Description 'fork the session when continuing (use with --continue or --session)' ) $model = New-OpencodeOption -Tokens @('-m', '--model') -Description 'model to use in the format of provider/model' -Kind 'Model' -Placeholder '<provider/model>' $agent = New-OpencodeOption -Tokens @('--agent') -Description 'agent to use' -Kind 'Agent' -Placeholder '<agent>' $auto = New-OpencodeOption -Tokens @('--auto') -Description 'auto-approve permissions that are not explicitly denied (dangerous!)' $mini = @( New-OpencodeOption -Tokens @('--mini') -Description 'start the minimal interactive interface' New-OpencodeOption -Tokens @('--no-replay') -Description 'disable mini session history replay on resume and after resize' New-OpencodeOption -Tokens @('--replay-limit') -Description 'cap visible mini replay to the newest N messages' -Kind 'Number' -Placeholder '<n>' ) $auth = @( New-OpencodeOption -Tokens @('-p', '--password') -Description 'basic auth password (defaults to OPENCODE_SERVER_PASSWORD)' -Kind 'String' -Placeholder '<password>' New-OpencodeOption -Tokens @('-u', '--username') -Description "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')" -Kind 'String' -Placeholder '<username>' ) $rootOptions = @($common) + @($server) + @($model) + @($session) + @( New-OpencodeOption -Tokens @('--prompt') -Description 'prompt to use' -Kind 'String' -Placeholder '<prompt>' $agent $auto ) + @($mini) $commands = @( New-OpencodeCommand -Name 'completion' -Description 'generate shell completion script' -Options $common New-OpencodeCommand -Name 'acp' -Description 'start ACP (Agent Client Protocol) server' -Options (@($common) + @($server) + @( New-OpencodeOption -Tokens @('--cwd') -Description 'working directory' -Kind 'Directory' )) New-OpencodeCommand -Name 'mcp' -Description 'manage MCP (Model Context Protocol) servers' -Options $common -Subcommands @( New-OpencodeCommand -Name 'add' -Description 'add an MCP server' -Options (@($common) + @( New-OpencodeOption -Tokens @('--url') -Description 'URL for a remote MCP server' -Kind 'String' -Placeholder '<url>' New-OpencodeOption -Tokens @('--env') -Description 'environment variable for a local MCP server (KEY=VALUE)' -Kind 'String' -Placeholder '<KEY=VALUE>' New-OpencodeOption -Tokens @('--header') -Description 'HTTP header for a remote MCP server (KEY=VALUE)' -Kind 'String' -Placeholder '<KEY=VALUE>' )) -Positionals @(New-OpencodePositional -Name 'name' -Description 'name of the MCP server') New-OpencodeCommand -Name 'list' -Description 'list MCP servers and their status' -Aliases @('ls') -Options $common New-OpencodeCommand -Name 'auth' -Description 'authenticate with an OAuth-enabled MCP server' -Options $common -Positionals @(New-OpencodePositional -Name 'name' -Description 'name of the MCP server' -Kind 'Mcp') -Subcommands @( New-OpencodeCommand -Name 'list' -Description 'list OAuth-capable MCP servers and their auth status' -Aliases @('ls') -Options $common ) New-OpencodeCommand -Name 'logout' -Description 'remove OAuth credentials for an MCP server' -Options $common -Positionals @(New-OpencodePositional -Name 'name' -Description 'name of the MCP server' -Kind 'McpCredential') New-OpencodeCommand -Name 'debug' -Description 'debug OAuth connection for an MCP server' -Options $common -Positionals @(New-OpencodePositional -Name 'name' -Description 'name of the MCP server' -Kind 'Mcp') ) New-OpencodeCommand -Name 'attach' -Description 'attach to a running opencode server' -Options (@($common) + @( New-OpencodeOption -Tokens @('--dir') -Description 'directory to run in' -Kind 'Directory' ) + @($session) + @($auth) + @($mini)) -Positionals @(New-OpencodePositional -Name 'url' -Description 'http://localhost:4096' -Placeholder 'http://localhost:4096') New-OpencodeCommand -Name 'run' -Description 'run opencode with a message' -Options (@($common) + @( New-OpencodeOption -Tokens @('--command') -Description 'the command to run, use message for args' -Kind 'String' -Placeholder '<command>' ) + @($session) + @( New-OpencodeOption -Tokens @('--share') -Description 'share the session' $model $agent New-OpencodeOption -Tokens @('--format') -Description 'format: default (formatted) or json (raw JSON events)' -Kind 'Enum' -Values @('default', 'json') New-OpencodeOption -Tokens @('-f', '--file') -Description 'file(s) to attach to message' -Kind 'Path' New-OpencodeOption -Tokens @('--title') -Description 'title for the session (uses truncated prompt if no value provided)' -Kind 'String' -Placeholder '<title>' New-OpencodeOption -Tokens @('--attach') -Description 'attach to a running opencode server (e.g., http://localhost:4096)' -Kind 'String' -Placeholder 'http://localhost:4096' ) + @($auth) + @( New-OpencodeOption -Tokens @('--dir') -Description 'directory to run in, path on remote server if attaching' -Kind 'Directory' New-OpencodeOption -Tokens @('--port') -Description 'port for the local server (defaults to random port if no value provided)' -Kind 'Number' -Placeholder '<port>' New-OpencodeOption -Tokens @('--variant') -Description 'model variant (provider-specific reasoning effort, e.g., high, max, minimal)' -Kind 'Enum' -Values @('high', 'max', 'minimal') New-OpencodeOption -Tokens @('--thinking') -Description 'show thinking blocks' New-OpencodeOption -Tokens @('-i', '--interactive') -Description 'run in direct interactive split-footer mode' $auto )) -Positionals @(New-OpencodePositional -Name 'message' -Description 'message to send' -Kind 'Text' -Placeholder '<message>') New-OpencodeCommand -Name 'debug' -Description 'debugging and troubleshooting tools' -Options $common -Subcommands @( New-OpencodeCommand -Name 'config' -Description 'show resolved configuration' -Options $common New-OpencodeCommand -Name 'lsp' -Description 'LSP debugging utilities' -Options $common -Subcommands @( New-OpencodeCommand -Name 'diagnostics' -Description 'get diagnostics for a file' -Options $common -Positionals @(New-OpencodePositional -Name 'file' -Description 'file to diagnose' -Kind 'Path') New-OpencodeCommand -Name 'symbols' -Description 'search workspace symbols' -Options $common -Positionals @(New-OpencodePositional -Name 'query' -Description 'symbol query') New-OpencodeCommand -Name 'document-symbols' -Description 'get symbols from a document' -Options $common -Positionals @(New-OpencodePositional -Name 'uri' -Description 'document uri') ) New-OpencodeCommand -Name 'rg' -Description 'ripgrep debugging utilities' -Options $common -Subcommands @( New-OpencodeCommand -Name 'files' -Description 'list files using ripgrep' -Options $common New-OpencodeCommand -Name 'search' -Description 'search file contents using ripgrep' -Options $common -Positionals @(New-OpencodePositional -Name 'pattern' -Description 'search pattern') ) New-OpencodeCommand -Name 'file' -Description 'file system debugging utilities' -Options $common -Subcommands @( New-OpencodeCommand -Name 'read' -Description 'read file contents as JSON' -Options $common -Positionals @(New-OpencodePositional -Name 'path' -Description 'file to read' -Kind 'Path') New-OpencodeCommand -Name 'list' -Description 'list files in a directory' -Options $common -Positionals @(New-OpencodePositional -Name 'path' -Description 'directory to list' -Kind 'Directory') New-OpencodeCommand -Name 'search' -Description 'search files by query' -Options $common -Positionals @(New-OpencodePositional -Name 'query' -Description 'file query') ) New-OpencodeCommand -Name 'scrap' -Description 'list all known projects' -Options $common New-OpencodeCommand -Name 'skill' -Description 'list all available skills' -Options $common New-OpencodeCommand -Name 'snapshot' -Description 'snapshot debugging utilities' -Options $common -Subcommands @( New-OpencodeCommand -Name 'track' -Description 'track current snapshot state' -Options $common New-OpencodeCommand -Name 'patch' -Description 'show patch for a snapshot hash' -Options $common -Positionals @(New-OpencodePositional -Name 'hash' -Description 'snapshot hash') New-OpencodeCommand -Name 'diff' -Description 'show diff for a snapshot hash' -Options $common -Positionals @(New-OpencodePositional -Name 'hash' -Description 'snapshot hash') ) New-OpencodeCommand -Name 'startup' -Description 'print startup timing' -Options $common New-OpencodeCommand -Name 'agent' -Description 'show agent configuration details' -Options (@($common) + @( New-OpencodeOption -Tokens @('--tool') -Description 'Tool id to execute' -Kind 'String' -Placeholder '<tool-id>' New-OpencodeOption -Tokens @('--params') -Description 'Tool params as JSON or a JS object literal' -Kind 'String' -Placeholder '<json>' )) -Positionals @(New-OpencodePositional -Name 'name' -Description 'Agent name' -Kind 'Agent') New-OpencodeCommand -Name 'v2' -Description 'debug v2 catalog and built-in plugins' -Options $common New-OpencodeCommand -Name 'info' -Description 'show debug information' -Options $common New-OpencodeCommand -Name 'paths' -Description 'show global paths (data, config, cache, state)' -Options $common New-OpencodeCommand -Name 'wait' -Description 'wait indefinitely (for debugging)' -Options $common ) New-OpencodeCommand -Name 'providers' -Description 'manage AI providers and credentials' -Aliases @('auth') -Options $common -Subcommands @( New-OpencodeCommand -Name 'list' -Description 'list providers and credentials' -Aliases @('ls') -Options $common New-OpencodeCommand -Name 'login' -Description 'log in to a provider' -Options (@($common) + @( New-OpencodeOption -Tokens @('-p', '--provider') -Description 'provider id or name to log in to (skips provider selection)' -Kind 'ProviderAll' -Placeholder '<provider>' New-OpencodeOption -Tokens @('-m', '--method') -Description 'login method label (skips method selection)' -Kind 'String' -Placeholder '<method>' )) -Positionals @(New-OpencodePositional -Name 'url' -Description 'opencode auth provider') New-OpencodeCommand -Name 'logout' -Description 'log out from a configured provider' -Options $common -Positionals @(New-OpencodePositional -Name 'provider' -Description 'provider id or name to log out from' -Kind 'Credential') ) New-OpencodeCommand -Name 'agent' -Description 'manage agents' -Options $common -Subcommands @( New-OpencodeCommand -Name 'create' -Description 'create a new agent' -Options (@($common) + @( New-OpencodeOption -Tokens @('--path') -Description 'directory path to generate the agent file' -Kind 'Directory' New-OpencodeOption -Tokens @('--description') -Description 'what the agent should do' -Kind 'String' -Placeholder '<description>' New-OpencodeOption -Tokens @('--mode') -Description 'agent mode' -Kind 'Enum' -Values @('all', 'primary', 'subagent') New-OpencodeOption -Tokens @('--permissions', '--tools') -Description 'comma-separated list of permissions to allow (default: all)' -Kind 'Enum' -Values @('bash', 'read', 'edit', 'glob', 'grep', 'webfetch', 'task', 'todowrite', 'websearch', 'lsp', 'skill') $model )) New-OpencodeCommand -Name 'list' -Description 'list all available agents' -Options $common ) New-OpencodeCommand -Name 'upgrade' -Description 'upgrade opencode to the latest or a specific version' -Options (@($common) + @( New-OpencodeOption -Tokens @('-m', '--method') -Description 'installation method to use' -Kind 'Enum' -Values @('curl', 'npm', 'pnpm', 'bun', 'brew', 'choco', 'scoop') )) -Positionals @(New-OpencodePositional -Name 'target' -Description "version to upgrade to, for ex '0.1.48' or 'v0.1.48'" -Placeholder '<version>') New-OpencodeCommand -Name 'uninstall' -Description 'uninstall opencode and remove all related files' -Options (@($common) + @( New-OpencodeOption -Tokens @('-c', '--keep-config') -Description 'keep configuration files' New-OpencodeOption -Tokens @('-d', '--keep-data') -Description 'keep session data and snapshots' New-OpencodeOption -Tokens @('--dry-run') -Description 'show what would be removed without removing' New-OpencodeOption -Tokens @('-f', '--force') -Description 'skip confirmation prompts' )) New-OpencodeCommand -Name 'serve' -Description 'starts a headless opencode server' -Options (@($common) + @($server)) New-OpencodeCommand -Name 'web' -Description 'start opencode server and open web interface' -Options (@($common) + @($server)) New-OpencodeCommand -Name 'models' -Description 'list all available models' -Options (@($common) + @( New-OpencodeOption -Tokens @('--verbose') -Description 'use more verbose model output (includes metadata like costs)' New-OpencodeOption -Tokens @('--refresh') -Description 'refresh the models cache from models.dev' )) -Positionals @(New-OpencodePositional -Name 'provider' -Description 'provider ID to filter models by' -Kind 'Provider') New-OpencodeCommand -Name 'stats' -Description 'show token usage and cost statistics' -Options (@($common) + @( New-OpencodeOption -Tokens @('--days') -Description 'show stats for the last N days (default: all time)' -Kind 'Number' -Placeholder '<days>' New-OpencodeOption -Tokens @('--tools') -Description 'number of tools to show (default: all)' -Kind 'Number' -Placeholder '<n>' New-OpencodeOption -Tokens @('--models') -Description 'show model statistics (default: hidden). Pass a number to show top N, otherwise shows all' New-OpencodeOption -Tokens @('--project') -Description 'filter by project (default: all projects, empty string: current project)' -Kind 'String' -Placeholder '<project>' )) New-OpencodeCommand -Name 'export' -Description 'export session data as JSON' -Options (@($common) + @( New-OpencodeOption -Tokens @('--sanitize') -Description 'redact sensitive transcript and file data' )) -Positionals @(New-OpencodePositional -Name 'sessionID' -Description 'session id to export' -Kind 'Session' -Placeholder '<session-id>') New-OpencodeCommand -Name 'import' -Description 'import session data from JSON file or URL' -Options $common -Positionals @(New-OpencodePositional -Name 'file' -Description 'path to JSON file or share URL' -Kind 'Path') New-OpencodeCommand -Name 'github' -Description 'manage GitHub agent' -Options $common -Subcommands @( New-OpencodeCommand -Name 'install' -Description 'install the GitHub agent' -Options $common New-OpencodeCommand -Name 'run' -Description 'run the GitHub agent' -Options (@($common) + @( New-OpencodeOption -Tokens @('--event') -Description 'GitHub mock event to run the agent for' -Kind 'String' -Placeholder '<event>' New-OpencodeOption -Tokens @('--token') -Description 'GitHub personal access token (github_pat_********)' -Kind 'String' -Placeholder '<token>' )) ) New-OpencodeCommand -Name 'pr' -Description 'fetch and checkout a GitHub PR branch, then run opencode' -Options $common -Positionals @(New-OpencodePositional -Name 'number' -Description 'PR number to checkout') New-OpencodeCommand -Name 'session' -Description 'manage sessions' -Options $common -Subcommands @( New-OpencodeCommand -Name 'list' -Description 'list sessions' -Options (@($common) + @( New-OpencodeOption -Tokens @('-n', '--max-count') -Description 'limit to N most recent sessions' -Kind 'Number' -Placeholder '<n>' New-OpencodeOption -Tokens @('--format') -Description 'output format' -Kind 'Enum' -Values @('table', 'json') )) New-OpencodeCommand -Name 'delete' -Description 'delete a session' -Options $common -Positionals @(New-OpencodePositional -Name 'sessionID' -Description 'session ID to delete' -Kind 'Session' -Placeholder '<session-id>') ) New-OpencodeCommand -Name 'plugin' -Description 'install plugin and update config' -Aliases @('plug') -Options (@($common) + @( New-OpencodeOption -Tokens @('-g', '--global') -Description 'install in global config' New-OpencodeOption -Tokens @('-f', '--force') -Description 'replace existing plugin version' )) -Positionals @(New-OpencodePositional -Name 'module' -Description 'npm module name') New-OpencodeCommand -Name 'db' -Description 'database tools' -Options (@($common) + @( New-OpencodeOption -Tokens @('--format') -Description 'Output format [default: "tsv"]' -Kind 'Enum' -Values @('json', 'tsv') )) -Positionals @(New-OpencodePositional -Name 'query' -Description 'SQL query to execute' -Kind 'Text' -Placeholder '<query>') -Subcommands @( New-OpencodeCommand -Name 'path' -Description 'print the database path' -Options $common ) ) $root = New-OpencodeCommand -Name '' -Description 'start opencode tui' -Options $rootOptions -Positionals @( New-OpencodePositional -Name 'project' -Description 'path to start opencode in' -Kind 'Directory' ) -Subcommands $commands $catalog = @{ Root = $root } Set-Variable -Name OpencodeCompletionCatalog -Scope Script -Value $catalog $catalog } # Live identifier sources. Everything except session IDs is read passively from opencode's own files; session IDs # live only in its SQLite database, which is queried through 'opencode db' (no instance bootstrap, so no project # registration or plugin load) with models.dev fetching and auto-update disabled. function Get-OpencodeDynamicCache { $existing = Get-Variable -Name OpencodeDynamicCache -Scope Script -ErrorAction Ignore if ($existing) { return $existing.Value } $cache = @{} Set-Variable -Name OpencodeDynamicCache -Scope Script -Value $cache $cache } function Get-OpencodeBaseDirectory { param([ValidateSet('config', 'data', 'cache')][string]$Kind) # opencode follows the XDG base directories on every platform, defaulting under the user profile. $variable, $default = switch ($Kind) { 'config' { 'XDG_CONFIG_HOME', '.config' } 'data' { 'XDG_DATA_HOME', [System.IO.Path]::Combine('.local', 'share') } 'cache' { 'XDG_CACHE_HOME', '.cache' } } $base = [System.Environment]::GetEnvironmentVariable($variable) if ([string]::IsNullOrEmpty($base)) { $base = [System.IO.Path]::Combine([System.Environment]::GetFolderPath('UserProfile'), $default) } [System.IO.Path]::Combine($base, 'opencode') } function Get-OpencodeFileStamp { param([string[]]$Path) @(foreach ($item in $Path) { $info = [System.IO.FileInfo]::new($item) if ($info.Exists) { '{0}:{1}' -f $info.LastWriteTimeUtc.Ticks, $info.Length } else { '-' } }) -join '|' } function ConvertFrom-OpencodeJson { param([string]$Text, [string]$Path) if (-not $Path -and [string]::IsNullOrWhiteSpace($Text)) { return $null } $options = [System.Text.Json.JsonDocumentOptions]::new() $options.CommentHandling = [System.Text.Json.JsonCommentHandling]::Skip $options.AllowTrailingCommas = $true $errorCountBefore = $Error.Count try { $document = if ($Path) { # UTF-8 bytes parse several times faster than a string; skip a BOM, which the byte parser rejects. $bytes = [System.IO.File]::ReadAllBytes($Path) $offset = if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { 3 } else { 0 } [System.Text.Json.JsonDocument]::Parse([System.ReadOnlyMemory[byte]]::new($bytes, $offset, $bytes.Length - $offset), $options) } else { [System.Text.Json.JsonDocument]::Parse($Text, $options) } try { return $document.RootElement.Clone() } finally { $document.Dispose() } } catch { return $null } finally { # A config file mid-edit or locked is an expected miss, not a fault to leave in $Error. while ($Error.Count -gt $errorCountBefore) { $Error.RemoveAt(0) } } } function Read-OpencodeJsonFile { param([string]$Path) $stamp = Get-OpencodeFileStamp -Path $Path if ($stamp -eq '-') { return $null } $cache = Get-OpencodeDynamicCache $key = 'json|' + $Path $entry = $cache[$key] if ($null -eq $entry -or $entry.Stamp -ne $stamp) { $entry = @{ Stamp = $stamp; Root = (ConvertFrom-OpencodeJson -Path $Path) } $cache[$key] = $entry } $entry.Root } function Get-OpencodeJsonMember { param([object]$Element, [string]$Name) if ($null -eq $Element -or $Element.ValueKind -ne [System.Text.Json.JsonValueKind]::Object) { return $null } $value = [System.Text.Json.JsonElement]::new() if ($Element.TryGetProperty($Name, [ref]$value)) { return $value } $null } function Get-OpencodeJsonName { param([object]$Element) if ($null -eq $Element -or $Element.ValueKind -ne [System.Text.Json.JsonValueKind]::Object) { return } foreach ($property in $Element.EnumerateObject()) { $property.Name } } function Get-OpencodeJsonString { param([object]$Element) if ($null -eq $Element) { return } if ($Element.ValueKind -eq [System.Text.Json.JsonValueKind]::String) { return $Element.GetString() } if ($Element.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) { foreach ($item in $Element.EnumerateArray()) { if ($item.ValueKind -eq [System.Text.Json.JsonValueKind]::String) { $item.GetString() } } } } function Test-OpencodeJsonTrue { param([object]$Element, [string]$Name) $value = Get-OpencodeJsonMember -Element $Element -Name $Name $null -ne $value -and $value.ValueKind -eq [System.Text.Json.JsonValueKind]::True } function Get-OpencodeProjectInfo { # Directories from the current location up to the git worktree root (or the drive root outside git), nearest first. $location = (Get-Location -PSProvider FileSystem).ProviderPath $directories = New-Object System.Collections.Generic.List[string] $root = '' $directory = $location while (-not [string]::IsNullOrEmpty($directory)) { [void]$directories.Add($directory) $gitPath = [System.IO.Path]::Combine($directory, '.git') if ([System.IO.Directory]::Exists($gitPath) -or [System.IO.File]::Exists($gitPath)) { $root = $directory break } $directory = [System.IO.Path]::GetDirectoryName($directory) } [pscustomobject]@{ Root = $root Directories = @($directories.ToArray()) } } function Get-OpencodeConfigSource { # Config directories (agent/mode markdown) and config files, lowest precedence first, as opencode resolves them. $project = Get-OpencodeProjectInfo $globalDirectory = Get-OpencodeBaseDirectory -Kind config $directories = New-Object System.Collections.Generic.List[string] $files = New-Object System.Collections.Generic.List[string] [void]$directories.Add($globalDirectory) foreach ($name in @('config.json', 'opencode.json', 'opencode.jsonc')) { [void]$files.Add([System.IO.Path]::Combine($globalDirectory, $name)) } if ($env:OPENCODE_CONFIG) { [void]$files.Add($env:OPENCODE_CONFIG) } if (-not $env:OPENCODE_DISABLE_PROJECT_CONFIG) { $rootFirst = @($project.Directories) [array]::Reverse($rootFirst) foreach ($directory in $rootFirst) { foreach ($name in @('opencode.json', 'opencode.jsonc')) { [void]$files.Add([System.IO.Path]::Combine($directory, $name)) } } foreach ($directory in $rootFirst) { $dotDirectory = [System.IO.Path]::Combine($directory, '.opencode') if ([System.IO.Directory]::Exists($dotDirectory)) { [void]$directories.Add($dotDirectory) } } } $homeDirectory = [System.IO.Path]::Combine([System.Environment]::GetFolderPath('UserProfile'), '.opencode') if ([System.IO.Directory]::Exists($homeDirectory) -and -not $directories.Contains($homeDirectory)) { [void]$directories.Add($homeDirectory) } if ($env:OPENCODE_CONFIG_DIR -and [System.IO.Directory]::Exists($env:OPENCODE_CONFIG_DIR)) { [void]$directories.Add($env:OPENCODE_CONFIG_DIR) } foreach ($directory in @($directories.ToArray() | Select-Object -Skip 1)) { foreach ($name in @('opencode.json', 'opencode.jsonc')) { [void]$files.Add([System.IO.Path]::Combine($directory, $name)) } } [pscustomobject]@{ Project = $project Directories = @($directories.ToArray()) Stamp = ($files -join '|') + '#' + (Get-OpencodeFileStamp -Path $files.ToArray()) Configs = @(foreach ($file in $files) { $root = Read-OpencodeJsonFile -Path $file if ($null -ne $root -and $root.ValueKind -eq [System.Text.Json.JsonValueKind]::Object) { $root } }) } } function ConvertTo-OpencodeQuotedValue { param([string]$Value, [string]$QuoteCharacter) if ($QuoteCharacter -eq '"') { return '"' + ($Value -replace '([`"$])', '`$1') + '"' } if ($QuoteCharacter -eq "'" -or $Value -match '[\s{}();,|&<>''"`$]|^[@#]') { return "'" + $Value.Replace("'", "''") + "'" } $Value } function Get-OpencodeAgentValue { $source = Get-OpencodeConfigSource $agents = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) foreach ($builtIn in @( @('build', 'built-in primary agent (default)'), @('plan', 'built-in primary agent (read-only planning)'), @('general', 'built-in subagent'), @('explore', 'built-in subagent'), @('compaction', 'built-in hidden agent'), @('summary', 'built-in hidden agent'), @('title', 'built-in hidden agent'))) { $agents[$builtIn[0]] = [pscustomobject]@{ Value = $builtIn[0]; ToolTip = $builtIn[1] } } # Markdown agents: {agent,agents}/**/*.md named by their relative path, {mode,modes}/*.md as primary agents. foreach ($directory in $source.Directories) { foreach ($folder in @('agent', 'agents', 'mode', 'modes')) { $folderPath = [System.IO.Path]::Combine($directory, $folder) if (-not [System.IO.Directory]::Exists($folderPath)) { continue } $recurse = $folder -like 'agent*' foreach ($file in @(Get-ChildItem -LiteralPath $folderPath -Filter '*.md' -File -Recurse:$recurse -ErrorAction Ignore)) { $relative = [System.IO.Path]::GetRelativePath($folderPath, $file.FullName).Replace('\', '/') $name = $relative.Substring(0, $relative.Length - $file.Extension.Length) $head = @(Get-Content -LiteralPath $file.FullName -TotalCount 40 -ErrorAction Ignore) $disabled = $false if ($head.Count -gt 0 -and $head[0].Trim() -eq '---') { foreach ($line in @($head | Select-Object -Skip 1)) { if ($line.Trim() -eq '---') { break } if ($line -match '^\s*disable\s*:\s*true\s*$') { $disabled = $true } } } if ($disabled) { [void]$agents.Remove($name) } else { $agents[$name] = [pscustomobject]@{ Value = $name; ToolTip = "agent from $($file.FullName)" } } } } } foreach ($config in $source.Configs) { foreach ($section in @('mode', 'agent')) { $block = Get-OpencodeJsonMember -Element $config -Name $section foreach ($name in @(Get-OpencodeJsonName -Element $block)) { if (Test-OpencodeJsonTrue -Element (Get-OpencodeJsonMember -Element $block -Name $name) -Name 'disable') { [void]$agents.Remove($name) } elseif (-not $agents.ContainsKey($name)) { $agents[$name] = [pscustomobject]@{ Value = $name; ToolTip = "agent from the '$section' config block" } } } } } @($agents.Values | Sort-Object -Property Value) } function Get-OpencodeCatalogIndex { # models.dev providers from opencode's on-disk cache, re-indexed only when the file changes. $path = [System.IO.Path]::Combine((Get-OpencodeBaseDirectory -Kind cache), 'models.json') $stamp = Get-OpencodeFileStamp -Path $path $cache = Get-OpencodeDynamicCache $entry = $cache['catalog'] if ($null -eq $entry -or $entry.Stamp -ne $stamp) { $catalog = Read-OpencodeJsonFile -Path $path $entry = @{ Stamp = $stamp Providers = @(foreach ($id in @(Get-OpencodeJsonName -Element $catalog)) { $provider = Get-OpencodeJsonMember -Element $catalog -Name $id [pscustomobject]@{ Id = $id Name = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $provider -Name 'name')) | Select-Object -First 1 Env = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $provider -Name 'env')) Models = Get-OpencodeJsonMember -Element $provider -Name 'models' } }) } $cache['catalog'] = $entry } $entry } function Get-OpencodeProviderState { # Mirrors opencode's provider loading: a models.dev provider is usable when one of its env vars is set, it has # stored credentials or a config block; 'opencode' is always usable (only its free models without a key). $source = Get-OpencodeConfigSource $index = Get-OpencodeCatalogIndex $authPath = [System.IO.Path]::Combine((Get-OpencodeBaseDirectory -Kind data), 'auth.json') $setVariables = @(foreach ($provider in $index.Providers) { foreach ($variable in $provider.Env) { if ([System.Environment]::GetEnvironmentVariable($variable)) { $variable } } }) $signature = @($source.Stamp, $index.Stamp, (Get-OpencodeFileStamp -Path $authPath), ($setVariables -join ','), [string]$env:OPENCODE_ENABLE_EXPERIMENTAL_MODELS) -join '#' $cache = Get-OpencodeDynamicCache $entry = $cache['providers'] if ($null -ne $entry -and $entry.Signature -eq $signature) { return $entry } $credentials = [System.Collections.Generic.HashSet[string]]::new([string[]]@(Get-OpencodeCredentialName -Store 'auth.json'), [System.StringComparer]::Ordinal) $variablesSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$setVariables, [System.StringComparer]::Ordinal) $disabled = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) $enabled = $null $configured = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) foreach ($config in $source.Configs) { foreach ($id in @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $config -Name 'disabled_providers'))) { [void]$disabled.Add($id) } $enabledList = Get-OpencodeJsonMember -Element $config -Name 'enabled_providers' if ($null -ne $enabledList) { $enabled = [System.Collections.Generic.HashSet[string]]::new([string[]]@(Get-OpencodeJsonString -Element $enabledList), [System.StringComparer]::Ordinal) } $providerBlock = Get-OpencodeJsonMember -Element $config -Name 'provider' foreach ($id in @(Get-OpencodeJsonName -Element $providerBlock)) { $configured[$id] = Get-OpencodeJsonMember -Element $providerBlock -Name $id } } $providers = [System.Collections.Generic.SortedDictionary[string, object]]::new([System.StringComparer]::Ordinal) foreach ($provider in $index.Providers) { $hasEnv = @($provider.Env | Where-Object { $variablesSet.Contains($_) }).Count -gt 0 $providers[$provider.Id] = [pscustomobject]@{ Id = $provider.Id; Name = $provider.Name; Models = $provider.Models; HasEnv = $hasEnv; Config = $null } } foreach ($id in $configured.Keys) { if (-not $providers.ContainsKey($id)) { $name = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $configured[$id] -Name 'name')) | Select-Object -First 1 $providers[$id] = [pscustomobject]@{ Id = $id; Name = $name; Models = $null; HasEnv = $false; Config = $null } } $providers[$id].Config = $configured[$id] } $experimental = [bool]$env:OPENCODE_ENABLE_EXPERIMENTAL_MODELS $providerValues = New-Object System.Collections.Generic.List[object] $connectedValues = New-Object System.Collections.Generic.List[object] $modelValues = New-Object System.Collections.Generic.List[object] foreach ($provider in $providers.Values) { $label = if ($provider.Name) { "$($provider.Name) ($($provider.Id))" } else { $provider.Id } $value = [pscustomobject]@{ Value = $provider.Id; ToolTip = $label } [void]$providerValues.Add($value) $allowed = -not $disabled.Contains($provider.Id) -and ($null -eq $enabled -or $enabled.Contains($provider.Id)) $apiKey = Get-OpencodeJsonMember -Element (Get-OpencodeJsonMember -Element $provider.Config -Name 'options') -Name 'apiKey' $hasKey = $provider.HasEnv -or $credentials.Contains($provider.Id) -or $null -ne $apiKey if (-not ($allowed -and ($hasKey -or $null -ne $provider.Config -or $provider.Id -eq 'opencode'))) { continue } [void]$connectedValues.Add($value) $freeOnly = $provider.Id -eq 'opencode' -and -not $hasKey $models = [System.Collections.Generic.SortedDictionary[string, string]]::new([System.StringComparer]::Ordinal) foreach ($id in @(Get-OpencodeJsonName -Element $provider.Models)) { $model = Get-OpencodeJsonMember -Element $provider.Models -Name $id $status = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $model -Name 'status')) | Select-Object -First 1 if ($status -eq 'deprecated' -or ($status -eq 'alpha' -and -not $experimental)) { continue } if ($freeOnly) { $inputCost = Get-OpencodeJsonMember -Element (Get-OpencodeJsonMember -Element $model -Name 'cost') -Name 'input' if ($null -ne $inputCost -and $inputCost.ValueKind -eq [System.Text.Json.JsonValueKind]::Number -and $inputCost.GetDouble() -gt 0) { continue } } $models[$id] = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $model -Name 'name')) | Select-Object -First 1 } $configModels = Get-OpencodeJsonMember -Element $provider.Config -Name 'models' foreach ($id in @(Get-OpencodeJsonName -Element $configModels)) { $models[$id] = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element (Get-OpencodeJsonMember -Element $configModels -Name $id) -Name 'name')) | Select-Object -First 1 } $whitelist = Get-OpencodeJsonMember -Element $provider.Config -Name 'whitelist' $allowList = @(Get-OpencodeJsonString -Element $whitelist) $blockList = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $provider.Config -Name 'blacklist')) foreach ($id in $models.Keys) { if ($blockList -ccontains $id -or ($null -ne $whitelist -and $allowList -cnotcontains $id)) { continue } $modelLabel = if ($models[$id]) { $models[$id] } else { $id } [void]$modelValues.Add([pscustomobject]@{ Value = "$($provider.Id)/$id"; ToolTip = "$modelLabel ($($provider.Id))" }) } } $entry = @{ Signature = $signature All = @($providerValues.ToArray()) Connected = @($connectedValues.ToArray()) Models = @($modelValues.ToArray()) } $cache['providers'] = $entry $entry } function Get-OpencodeCredentialName { param([ValidateSet('auth.json', 'mcp-auth.json')][string]$Store) # Only the top-level names are kept; the parsed document (with the secrets) is dropped right away. $path = [System.IO.Path]::Combine((Get-OpencodeBaseDirectory -Kind data), $Store) $stamp = Get-OpencodeFileStamp -Path $path $cache = Get-OpencodeDynamicCache $key = 'credential|' + $path $entry = $cache[$key] if ($null -eq $entry -or $entry.Stamp -ne $stamp) { $root = if ($stamp -ne '-') { ConvertFrom-OpencodeJson -Path $path } $entry = @{ Stamp = $stamp; Names = @(Get-OpencodeJsonName -Element $root | Sort-Object) } $cache[$key] = $entry } $entry.Names } function Get-OpencodeCredentialValue { param([ValidateSet('auth.json', 'mcp-auth.json')][string]$Store) foreach ($name in @(Get-OpencodeCredentialName -Store $Store)) { [pscustomobject]@{ Value = $name; ToolTip = "stored credentials ($Store)" } } } function Get-OpencodeMcpValue { $servers = [System.Collections.Generic.SortedDictionary[string, string]]::new([System.StringComparer]::Ordinal) foreach ($config in (Get-OpencodeConfigSource).Configs) { $block = Get-OpencodeJsonMember -Element $config -Name 'mcp' foreach ($name in @(Get-OpencodeJsonName -Element $block)) { $type = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element (Get-OpencodeJsonMember -Element $block -Name $name) -Name 'type')) | Select-Object -First 1 $servers[$name] = if ($type) { "$type MCP server" } else { 'MCP server' } } } foreach ($name in $servers.Keys) { [pscustomobject]@{ Value = $name; ToolTip = $servers[$name] } } } function Get-OpencodeExecutable { $cache = Get-OpencodeDynamicCache $key = 'exe|' + $env:PATH if (-not $cache.ContainsKey($key)) { # The engine API resolves in ~50 ms; the first Get-Command of a session costs about a second. $command = $ExecutionContext.InvokeCommand.GetCommand('opencode', [System.Management.Automation.CommandTypes]::Application) $cache[$key] = if ($command) { $command.Source } else { '' } } $cache[$key] } function Invoke-OpencodeDatabaseQuery { param([string]$Executable, [string]$Query) # 'opencode db <query>' only opens the database; a missing query would start an interactive sqlite3 shell. $output = '' $process = [System.Diagnostics.Process]::new() $errorCountBefore = $Error.Count try { $info = [System.Diagnostics.ProcessStartInfo]::new($Executable) $info.UseShellExecute = $false $info.CreateNoWindow = $true $info.RedirectStandardInput = $true $info.RedirectStandardOutput = $true $info.RedirectStandardError = $true $info.StandardOutputEncoding = [System.Text.Encoding]::UTF8 $info.Environment['NO_COLOR'] = '1' $info.Environment['OPENCODE_DISABLE_MODELS_FETCH'] = '1' $info.Environment['OPENCODE_DISABLE_AUTOUPDATE'] = '1' foreach ($argument in @('db', $Query, '--format', 'json')) { [void]$info.ArgumentList.Add($argument) } $process.StartInfo = $info [void]$process.Start() $process.StandardInput.Close() $stdout = $process.StandardOutput.ReadToEndAsync() [void]$process.StandardError.ReadToEndAsync() if ($process.WaitForExit(5000)) { $output = $stdout.GetAwaiter().GetResult() } else { $process.Kill($true) } } catch { $output = '' } finally { $process.Dispose() while ($Error.Count -gt $errorCountBefore) { $Error.RemoveAt(0) } } [regex]::Replace($output, '\x1b\[[0-9;?]*[ -/]*[@-~]', '') } function Get-OpencodeSessionValue { $executable = Get-OpencodeExecutable $database = [System.IO.Path]::Combine((Get-OpencodeBaseDirectory -Kind data), 'opencode.db') if ([string]::IsNullOrEmpty($executable) -or -not [System.IO.File]::Exists($database)) { return } # Same set as 'opencode session list': root sessions of the current project, newest first. Inside git the # project is the one whose worktree is the repository root; outside git it is the shared 'global' project. $root = (Get-OpencodeProjectInfo).Root $cache = Get-OpencodeDynamicCache $key = 'session|' + $executable + '|' + $root $stampPaths = @($database, ($database + '-wal')) $entry = $cache[$key] if ($null -ne $entry -and ($entry.Stamp -eq (Get-OpencodeFileStamp -Path $stampPaths) -or ([datetime]::UtcNow - $entry.Time).TotalSeconds -lt 30)) { return $entry.Values } $projectFilter = if ($root) { "project_id in (select id from project where lower(worktree) = lower('{0}'))" -f $root.Replace('\', '/').Replace("'", "''") } else { "project_id = 'global'" } $query = "select id, title, time_updated from session where parent_id is null and $projectFilter order by time_updated desc limit 50" $rows = ConvertFrom-OpencodeJson -Text (Invoke-OpencodeDatabaseQuery -Executable $executable -Query $query) $values = @(if ($null -ne $rows -and $rows.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) { foreach ($row in $rows.EnumerateArray()) { $id = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $row -Name 'id')) | Select-Object -First 1 if ([string]::IsNullOrEmpty($id)) { continue } $title = @(Get-OpencodeJsonString -Element (Get-OpencodeJsonMember -Element $row -Name 'title')) | Select-Object -First 1 $updated = Get-OpencodeJsonMember -Element $row -Name 'time_updated' $when = if ($null -ne $updated -and $updated.ValueKind -eq [System.Text.Json.JsonValueKind]::Number) { [System.DateTimeOffset]::FromUnixTimeMilliseconds($updated.GetInt64()).LocalDateTime.ToString('yyyy-MM-dd HH:mm') } [pscustomobject]@{ Value = $id; ToolTip = (@($title, $when) | Where-Object { $_ }) -join ' - ' } } }) # The stamp is taken after the query: opening the database touches its timestamp. $cache[$key] = @{ Stamp = (Get-OpencodeFileStamp -Path $stampPaths); Time = [datetime]::UtcNow; Values = $values } $values } function Get-OpencodeDynamicCompletion { param([string]$Kind, [string]$CurrentWord, [string]$InlinePrefix = '') $values = switch ($Kind) { 'Session' { Get-OpencodeSessionValue } 'Agent' { Get-OpencodeAgentValue } 'Model' { (Get-OpencodeProviderState).Models } 'Provider' { (Get-OpencodeProviderState).Connected } 'ProviderAll' { (Get-OpencodeProviderState).All } 'Credential' { Get-OpencodeCredentialValue -Store 'auth.json' } 'McpCredential' { Get-OpencodeCredentialValue -Store 'mcp-auth.json' } 'Mcp' { Get-OpencodeMcpValue } default { return } } # Match without the user's opening quote and keep that quote character when emitting. $quote = '' $prefix = $CurrentWord if ($prefix.Length -gt 0 -and ($prefix[0] -eq "'" -or $prefix[0] -eq '"')) { $quote = $prefix.Substring(0, 1) $prefix = $prefix.Substring(1) if ($prefix.EndsWith($quote)) { $prefix = $prefix.Substring(0, $prefix.Length - 1) } } foreach ($value in @($values)) { if ($value.Value.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { New-OpencodeCompletionResult -CompletionText ($InlinePrefix + (ConvertTo-OpencodeQuotedValue -Value $value.Value -QuoteCharacter $quote)) -ListItemText $value.Value -ResultType 'ParameterValue' -ToolTip $value.ToolTip } } } function New-OpencodeCompletionResult { param([string]$CompletionText, [string]$ListItemText, [string]$ResultType, [string]$ToolTip) if ([string]::IsNullOrWhiteSpace($ListItemText)) { $ListItemText = $CompletionText } if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $ToolTip) } function Find-OpencodeSubcommand { param([object]$Command, [string]$Token) foreach ($subcommand in @($Command.Subcommands)) { if ([string]::Equals($subcommand.Name, $Token, [System.StringComparison]::Ordinal)) { return $subcommand } foreach ($alias in @($subcommand.Aliases)) { if ([string]::Equals($alias, $Token, [System.StringComparison]::Ordinal)) { return $subcommand } } } $null } function Find-OpencodeOption { param([object]$Command, [string]$Token) foreach ($option in @($Command.Options)) { foreach ($optionToken in @($option.Tokens)) { if ([string]::Equals($optionToken, $Token, [System.StringComparison]::Ordinal)) { return $option } } } $null } function Get-OpencodeTokenState { param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition, [string]$WordToComplete ) # Elements that end before the cursor are completed tokens; the element under the cursor is the word being # typed, cut at the cursor; anything to the right of the cursor is ignored. $tokens = New-Object System.Collections.Generic.List[string] $currentWord = '' foreach ($element in @($CommandAst.CommandElements | Select-Object -Skip 1)) { $extent = $element.Extent if ($extent.EndOffset -lt $CursorPosition) { $text = if ($element -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $element.Value } else { $extent.Text } [void]$tokens.Add($text) continue } if ($extent.StartOffset -le $CursorPosition) { $currentWord = $extent.Text.Substring(0, $CursorPosition - $extent.StartOffset) } break } if ([string]::IsNullOrEmpty($currentWord) -and -not [string]::IsNullOrEmpty($WordToComplete)) { $currentWord = $WordToComplete } [pscustomobject]@{ CurrentWord = $currentWord Tokens = @($tokens.ToArray()) } } function Get-OpencodeContext { param([string[]]$Tokens) $catalog = Get-OpencodeCompletionCatalog $command = $catalog.Root $pendingOption = $null $positionalCount = 0 foreach ($token in @($Tokens)) { if ($null -ne $pendingOption) { $pendingOption = $null continue } if ($token.StartsWith('-')) { $optionToken = $token $attached = $false if ($token -match '^(--?[A-Za-z0-9][A-Za-z0-9-]*)=') { $optionToken = $matches[1] $attached = $true } $option = Find-OpencodeOption -Command $command -Token $optionToken if ($option -and $option.Kind -ne 'Flag' -and -not $attached) { $pendingOption = $option } continue } if ($positionalCount -eq 0) { $subcommand = Find-OpencodeSubcommand -Command $command -Token $token if ($subcommand) { $command = $subcommand continue } } $positionalCount++ } [pscustomobject]@{ Command = $command PendingOption = $pendingOption PositionalCount = $positionalCount } } function Get-OpencodePathCompletions { param([string]$CurrentWord, [bool]$DirectoryOnly, [string]$InlinePrefix = '') $results = New-Object System.Collections.Generic.List[object] foreach ($item in [System.Management.Automation.CompletionCompleters]::CompleteFilename($CurrentWord)) { if ($DirectoryOnly -and $item.ResultType -ne [System.Management.Automation.CompletionResultType]::ProviderContainer) { continue } [void]$results.Add((New-OpencodeCompletionResult -CompletionText ($InlinePrefix + $item.CompletionText) -ListItemText $item.ListItemText -ResultType $item.ResultType.ToString() -ToolTip $item.ToolTip)) } @($results.ToArray()) } function Get-OpencodeValueCompletions { param([object]$Option, [string]$CurrentWord, [string]$InlinePrefix = '') $dynamic = @(Get-OpencodeDynamicCompletion -Kind $Option.Kind -CurrentWord $CurrentWord -InlinePrefix $InlinePrefix) if ($dynamic.Count -gt 0) { return $dynamic } switch ($Option.Kind) { 'Enum' { return @(foreach ($value in @($Option.Values)) { if ($value.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { New-OpencodeCompletionResult -CompletionText ($InlinePrefix + $value) -ListItemText $value -ResultType 'ParameterValue' -ToolTip $Option.Description } }) } 'Path' { return @(Get-OpencodePathCompletions -CurrentWord $CurrentWord -DirectoryOnly $false -InlinePrefix $InlinePrefix) } 'Directory' { return @(Get-OpencodePathCompletions -CurrentWord $CurrentWord -DirectoryOnly $true -InlinePrefix $InlinePrefix) } default { if (-not [string]::IsNullOrEmpty($CurrentWord)) { return @() } $placeholder = if ($Option.Placeholder) { $Option.Placeholder } else { '<value>' } return @(New-OpencodeCompletionResult -CompletionText ($InlinePrefix + $placeholder) -ListItemText $placeholder -ResultType 'ParameterValue' -ToolTip $Option.Description) } } } function Get-OpencodeOptionCompletions { param([object]$Command, [string]$CurrentWord) $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) foreach ($option in @($Command.Options)) { foreach ($token in @($option.Tokens)) { if (-not $token.StartsWith($CurrentWord, [System.StringComparison]::Ordinal)) { continue } if ($seen.Add($token)) { New-OpencodeCompletionResult -CompletionText $token -ResultType 'ParameterName' -ToolTip $option.Description } } } } function Get-OpencodeSubcommandCompletions { param([object]$Command, [string]$CurrentWord) foreach ($subcommand in @($Command.Subcommands)) { if ($subcommand.Name.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { New-OpencodeCompletionResult -CompletionText $subcommand.Name -ResultType 'ParameterValue' -ToolTip $subcommand.Description } foreach ($alias in @($subcommand.Aliases)) { if ($alias.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { New-OpencodeCompletionResult -CompletionText $alias -ResultType 'ParameterValue' -ToolTip ('alias of {0}: {1}' -f $subcommand.Name, $subcommand.Description) } } } } function Get-OpencodePositionalCompletions { param([object]$Positional, [string]$CurrentWord) $dynamic = @(Get-OpencodeDynamicCompletion -Kind $Positional.Kind -CurrentWord $CurrentWord) if ($dynamic.Count -gt 0) { return $dynamic } switch ($Positional.Kind) { 'Path' { return @(Get-OpencodePathCompletions -CurrentWord $CurrentWord -DirectoryOnly $false) } 'Directory' { return @(Get-OpencodePathCompletions -CurrentWord $CurrentWord -DirectoryOnly $true) } default { if (-not [string]::IsNullOrEmpty($CurrentWord)) { return @() } return @(New-OpencodeCompletionResult -CompletionText $Positional.Placeholder -ResultType 'ParameterValue' -ToolTip $Positional.Description) } } } function Complete-Opencode { param( [string]$WordToComplete, [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $state = Get-OpencodeTokenState -CommandAst $CommandAst -CursorPosition $CursorPosition -WordToComplete $WordToComplete $currentWord = $state.CurrentWord $context = Get-OpencodeContext -Tokens $state.Tokens $command = $context.Command # Attached --option=value form. if ($currentWord -match '^(?<name>--?[A-Za-z0-9][A-Za-z0-9-]*)=(?<value>.*)$') { $option = Find-OpencodeOption -Command $command -Token $matches.name if ($option -and $option.Kind -ne 'Flag') { return @(Get-OpencodeValueCompletions -Option $option -CurrentWord $matches.value -InlinePrefix ($matches.name + '=')) } return @() } if ($null -ne $context.PendingOption) { return @(Get-OpencodeValueCompletions -Option $context.PendingOption -CurrentWord $currentWord) } if ($currentWord.StartsWith('-')) { return @(Get-OpencodeOptionCompletions -Command $command -CurrentWord $currentWord) } $results = New-Object System.Collections.Generic.List[object] if ($context.PositionalCount -eq 0) { foreach ($item in @(Get-OpencodeSubcommandCompletions -Command $command -CurrentWord $currentWord)) { [void]$results.Add($item) } } $positionals = @($command.Positionals) if ($context.PositionalCount -lt $positionals.Count) { $positional = $positionals[$context.PositionalCount] # The root [project] operand is only offered once a partial path is typed, so a bare 'opencode ' shows commands. if (-not ($command.Name -eq '' -and [string]::IsNullOrEmpty($currentWord))) { foreach ($item in @(Get-OpencodePositionalCompletions -Positional $positional -CurrentWord $currentWord)) { [void]$results.Add($item) } } } elseif ($positionals.Count -gt 0 -and $positionals[-1].Kind -eq 'Text' -and [string]::IsNullOrEmpty($currentWord)) { # Variadic free text (run [message..]) keeps accepting words. [void]$results.Add((New-OpencodeCompletionResult -CompletionText $positionals[-1].Placeholder -ResultType 'ParameterValue' -ToolTip $positionals[-1].Description)) } if ([string]::IsNullOrEmpty($currentWord) -and $command.Name -ne '') { foreach ($item in @(Get-OpencodeOptionCompletions -Command $command -CurrentWord '')) { [void]$results.Add($item) } } @($results.ToArray()) } # Register the completer for both opencode and opencode.exe # Using a literal script block to satisfy Import-CompleterScript requirements Register-ArgumentCompleter -Native -CommandName 'opencode', 'opencode.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Opencode -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition } |