文件预览

anysearch_cli.ps1

查看 AnySearch 技能包中的文件内容。

文件内容

scripts/anysearch_cli.ps1

#!/usr/bin/env pwsh
#Requires -Version 5.1

Set-StrictMode -Version Latest

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
chcp 65001 | Out-Null

$ENDPOINT = "https://api.anysearch.com/mcp"
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Definition

function Load-Env {
    $envPaths = @(Join-Path $SCRIPT_DIR ".env", Join-Path $SCRIPT_DIR ".." ".env")
    foreach ($envPath in $envPaths) {
        if (Test-Path $envPath) {
            Get-Content $envPath -Encoding UTF8 | ForEach-Object {
                $line = $_.Split('#')[0].Trim()
                if ($line -and $line -match '=') {
                    $idx = $line.IndexOf('=')
                    $key = $line.Substring(0, $idx).Trim()
                    $val = $line.Substring($idx + 1).Trim().Trim('"').Trim("'")
                    if (-not (Test-Path "env:$key")) {
                        Set-Item -Path "env:$key" -Value $val
                    }
                }
            }
        }
    }
}

Load-Env

$AVAILABLE_DOMAINS = @(
    "code","tech","fashion","travel","home","ecommerce",
    "gaming","film","music","finance","academic","legal",
    "business","ip","security","education","health","religion",
    "geo","environment","energy","ugc"
)

$CONTENT_TYPES = @("web","news","code","doc","academic","data","image","video","audio")
$FRESHNESS_VALUES = @("day","week","month","year")
$ZONES = @("cn","intl")

function Call-Api {
    param(
        [string]$ToolName,
        [hashtable]$Arguments,
        [string]$ApiKey
    )

    $payload = @{
        jsonrpc = "2.0"
        id      = 1
        method  = "tools/call"
        params  = @{
            name      = $ToolName
            arguments = $Arguments
        }
    } | ConvertTo-Json -Depth 10 -Compress

    $headers = @{ "Content-Type" = "application/json; charset=utf-8" }
    if ($ApiKey) {
        $headers["Authorization"] = "Bearer $ApiKey"
    }

    try {
        $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
        $webReq = [System.Net.HttpWebRequest]::Create($ENDPOINT)
        $webReq.Method = "POST"
        $webReq.ContentType = "application/json; charset=utf-8"
        $webReq.Timeout = 30000
        if ($ApiKey) {
            $webReq.Headers.Add("Authorization", "Bearer $ApiKey")
        }
        $reqStream = $webReq.GetRequestStream()
        $reqStream.Write($bodyBytes, 0, $bodyBytes.Length)
        $reqStream.Close()
        $webResp = $webResp = $webReq.GetResponse()
        $respStream = $webResp.GetResponseStream()
        $respReader = New-Object System.IO.StreamReader($respStream, [System.Text.Encoding]::UTF8)
        $rawJson = $respReader.ReadToEnd()
        $respReader.Close()
        $webResp.Close()
        $resp = $rawJson | ConvertFrom-Json
    } catch {
        $err = $_.Exception.Message
        Write-Error "Connection Error: Unable to reach the API endpoint. ($err)"
        exit 1
    }

    $hasError = $false
    try { $hasError = ($null -ne $resp.error) } catch { }

    if ($hasError) {
        $errMsg = ""
        try { $errMsg = $resp.error.message } catch { $errMsg = $resp.error | ConvertTo-Json -Depth 5 }
        Write-Error "API Error: $errMsg"
        exit 1
    }

    $result = $null
    try { $result = $resp.result } catch { $result = $resp }

    if ($result -and $result.content) {
        foreach ($item in $result.content) {
            if ($item.type -eq "text") {
                return $item.text
            }
        }
    }
    return ($result | ConvertTo-Json -Depth 10)
}

function Parse-JsonList {
    param([string]$Value)
    try {
        $parsed = $Value | ConvertFrom-Json
        if ($parsed -is [array]) { return @($parsed) }
        return @($parsed)
    } catch {
        return @($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    }
}

function Invoke-Search {
    param([hashtable]$Opts)

    $arguments = @{ query = $Opts.Query }

    if ($Opts.Domain) {
        $arguments["domain"] = $Opts.Domain
        if ($Opts.SubDomain) { $arguments["sub_domain"] = $Opts.SubDomain }
        if ($Opts.SubDomainParams) {
            try {
                $arguments["sub_domain_params"] = $Opts.SubDomainParams | ConvertFrom-Json -AsHashtable
            } catch {
                Write-Error "Error: --sub_domain_params must be valid JSON"
                exit 1
            }
        }
    }

    if ($Opts.ContentTypes) {
        $arguments["content_types"] = @(Parse-JsonList $Opts.ContentTypes)
    }
    if ($Opts.Zone) { $arguments["zone"] = $Opts.Zone }
    if ($Opts.MaxResults -ne $null) { $arguments["max_results"] = $Opts.MaxResults }
    if ($Opts.Freshness) { $arguments["freshness"] = $Opts.Freshness }

    $result = Call-Api -ToolName "search" -Arguments $arguments -ApiKey $Opts.ApiKey
    Write-Output $result
}

function Invoke-ListDomains {
    param([hashtable]$Opts)

    $arguments = @{}

    if ($Opts.Domains) {
        $arguments["domains"] = @(Parse-JsonList $Opts.Domains)
    } elseif ($Opts.Domain) {
        $arguments["domain"] = $Opts.Domain
    } else {
        Write-Error "Error: provide --domain or --domains"
        exit 1
    }

    $result = Call-Api -ToolName "list_domains" -Arguments $arguments -ApiKey $Opts.ApiKey
    Write-Output $result
}

function Invoke-Extract {
    param([hashtable]$Opts)

    if (-not $Opts.Url) {
        Write-Error "Error: url is required"
        exit 1
    }

    $arguments = @{ url = $Opts.Url }
    $result = Call-Api -ToolName "extract" -Arguments $arguments -ApiKey $Opts.ApiKey
    Write-Output $result
}

function Repair-Json {
    param([string]$Raw)

    $Raw = $Raw.Trim()
    if ($Raw.StartsWith('{') -and -not $Raw.StartsWith('[')) {
        $Raw = "[$Raw]"
    }
    if ($Raw.StartsWith('[')) {
        $inner = $Raw.Substring(1, $Raw.Length - 2).Trim()
        if (-not $inner) { return @() }
        $items = Split-JsonItems $inner
        $queries = @()
        foreach ($item in $items) {
            $item = $item.Trim().Trim(',')
            if (-not $item) { continue }
            if ($item.StartsWith('{')) {
                $queries += Repair-JsonObject $item
            } else {
                $queries += @{ query = $item.Trim().Trim("'").Trim('"') }
            }
        }
        return $queries
    }
    return @(@{ query = $Raw.Trim().Trim("'").Trim('"') })
}

function Split-JsonItems {
    param([string]$S)

    $depth = 0
    $current = ""
    $items = @()

    foreach ($ch in $S.ToCharArray()) {
        if ($ch -eq '{') { $depth++ }
        elseif ($ch -eq '}') { $depth-- }

        if ($ch -eq ',' -and $depth -eq 0) {
            $items += $current
            $current = ""
        } else {
            $current += $ch
        }
    }
    if ($current) {
        $tail = $current.Trim()
        if ($tail) { $items += $tail }
    }
    return ,$items
}

function Repair-JsonObject {
    param([string]$S)

    $inner = $S.Trim()
    if ($inner.StartsWith('{')) { $inner = $inner.Substring(1) }
    if ($inner.EndsWith('}')) { $inner = $inner.Substring(0, $inner.Length - 1) }
    $inner = $inner.Trim()
    if (-not $inner) { return @{} }

    $pairs = Split-JsonItems $inner
    $result = @{}

    foreach ($pair in $pairs) {
        $p = $pair.Trim().Trim(',')
        if (-not $p -or $p -notmatch ':') { continue }
        $colon = $p.IndexOf(':')
        $key = $p.Substring(0, $colon).Trim().Trim('"').Trim("'")
        $val = $p.Substring($colon + 1).Trim()

        if ($val.StartsWith('{')) {
            try { $result[$key] = $val | ConvertFrom-Json -AsHashtable }
            catch { $result[$key] = Repair-JsonObject $val }
        } elseif ($val.StartsWith('[')) {
            try { $result[$key] = @($val | ConvertFrom-Json) }
            catch { $result[$key] = @($val.Trim('[]') -split ',') }
        } elseif ($val -eq 'true') {
            $result[$key] = $true
        } elseif ($val -eq 'false') {
            $result[$key] = $false
        } elseif ($val -eq 'null') {
            $result[$key] = $null
        } else {
            try { $result[$key] = $val | ConvertFrom-Json }
            catch { $result[$key] = $val.Trim('"').Trim("'") }
        }
    }
    return $result
}

function Invoke-BatchSearch {
    param([hashtable]$Opts)

    $queries = $null

    if ($Opts.QueryItems -and $Opts.QueryItems.Count -gt 0) {
        if ($Opts.QueryItems.Count -gt 5) {
            Write-Error "Error: batch_search supports a maximum of 5 queries"
            exit 1
        }
        $queries = @($Opts.QueryItems | ForEach-Object { @{ query = $_ } })
    } elseif ($Opts.Queries) {
        $raw = $Opts.Queries
        if ($raw.StartsWith('@')) {
            $fpath = $raw.Substring(1)
            if (-not (Test-Path $fpath)) {
                Write-Error "Error: file not found: $fpath"
                exit 1
            }
            $raw = Get-Content $fpath -Raw -Encoding UTF8
        }
        try {
            $parsed = $raw | ConvertFrom-Json
            if ($parsed -is [array]) {
                $queries = @($parsed)
            } else {
                $queries = @($parsed)
            }
        } catch {
            $queries = Repair-Json $raw
        }
    } else {
        Write-Error "Error: provide --queries or --query"
        exit 1
    }

    $qcount = 0
    if ($queries) { $qcount = @($queries).Count }

    if ($qcount -lt 1) {
        Write-Error "Error: queries must contain at least 1 item"
        exit 1
    }
    if ($qcount -gt 5) {
        Write-Error "Error: batch_search supports a maximum of 5 queries"
        exit 1
    }

    $arguments = @{ queries = @($queries) }
    $result = Call-Api -ToolName "batch_search" -Arguments $arguments -ApiKey $Opts.ApiKey
    Write-Output $result
}

function Show-Doc {
    @'
# AnySearch Interface Specification (for AI Agent)

## Protocol
- Endpoint: POST https://api.anysearch.com/mcp
- Format: JSON-RPC 2.0, method = "tools/call"
- Auth: Header "Authorization: Bearer <API_KEY>" (optional, anonymous has lower rate limits)

## CLI Invocation (PowerShell)

```powershell
powershell -ExecutionPolicy Bypass -File <skill_dir>/scripts/anysearch_cli.ps1 <command> [options]
```

## Available Commands

### 1. search — Single query search
Two modes: general (omit --domain) and vertical (requires --domain + --sub_domain).

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| query | string | YES | Search query (positional). Vertical search MUST follow query_format from list_domains |
| --domain, -d | string | no | Vertical domain: code tech fashion travel home ecommerce gaming film music finance academic legal business ip security education health religion geo environment energy ugc |
| --sub_domain, -s | string | no | Sub-domain routing key (e.g. finance.us_stock). REQUIRED for vertical search |
| --sub_domain_params | JSON | no | Extra params per sub_domain schema from list_domains |
| --content_types, -t | string | no | Comma-separated or JSON array: web news code doc academic data image video audio |
| --zone, -z | string | no | cn / intl. Required when list_domains marks zone=CN |
| --max_results, -m | int | no | 1-100, default 10 |
| --freshness, -f | string | no | day / week / month / year |

### 2. list_domains — Query vertical domain directory
MUST be called before vertical search to discover available sub_domains and query formats.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| --domain | string | choose one | Single domain to query |
| --domains | string | choose one | Batch up to 5 domains (comma-separated). Takes precedence over --domain |

Returns a Markdown table with columns: domain, sub_domain, description, query_format, params_schema, zone.

IMPORTANT: Cache list_domains results per domain within a session. Do NOT call repeatedly.

### 3. batch_search — Execute 2-5 search queries in parallel
Single failure does not block others; results are merged.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| --query | string | YES (x1-5) | Repeatable single-query shorthand. Up to 5 |
| --queries, -q | JSON | YES | JSON array of query objects, or @file.json to read from file |

Each query object supports: query (required), domain, sub_domain, content_types, zone, max_results, freshness.

### 4. extract — Fetch full page content as Markdown
Truncated at 50,000 chars. HTML pages only.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| url | string | YES | Target URL (positional or via --url / -u) |

---

## Decision Flow

```
User query
  |
  +-- Has structured identifiers? (Stock:/CVE:/DOI:/IATA:/patent etc.)
  |     YES -> 1) powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 list_domains --domain X
  |             2) read query_format from result -> construct query accordingly
  |             3) powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "<query>" --domain X --sub_domain Y --zone cn
  |
  +-- Multiple independent intents?
  |     YES -> powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 batch_search --query "..." --query "..."
  |
  +-- Need deeper content than snippets?
        YES -> powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 extract "https://example.com/article"

  Otherwise -> powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "<general query>"
```

---

## Vertical Search Semantic Constraints

Before performing vertical search, you MUST call list_domains for the target domain
and strictly obey the returned semantic constraints:

1. **query_format**: Describes exactly how to structure the query string for that sub_domain.
   Example: "直接输入股票代码(如 AAPL)、公司名称、货币对(如 EUR_USD)、商品(如 WTICO_USD)"
   -> This means you pass the raw ticker/name/pair directly, NOT a natural language sentence.

2. **params_schema**: JSON schema for optional extra parameters.
   Example: {"type":"object","properties":{"period":{"type":"string","enum":["1d","1w","1m","3m","1y"]}}}
   -> You can pass --sub_domain_params '{"period":"1w"}' to narrow results.

3. **zone**: If "CN", you MUST set --zone cn in the search call.

4. **sub_domain selection**: Match the user's intent to the best sub_domain description.

---

## Scenario Examples (all runnable CLI commands)

### Scenario 1: General web search — look up a factual question

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "What is the capital of France"
```

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "quantum computing breakthroughs 2025" --max_results 5 --freshness month
```

### Scenario 2: Search with content type filter — find video or image results

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "how to bake sourdough bread" --content_types video --max_results 3
```

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "Mount Everest" --content_types image --max_results 5
```

### Scenario 3: Vertical search — stock market data (structured identifier)

Step 1: Discover available sub_domains for finance:

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 list_domains --domain finance
```

Step 2: Search with the correct sub_domain and query format:

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "AAPL" --domain finance --sub_domain finance.us_stock --zone cn --max_results 5
```

### Scenario 4: Vertical search — academic paper lookup

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 list_domains --domain academic
```

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "10.1038/s41586-020-2649-2" --domain academic --sub_domain academic.doi --max_results 3
```

### Scenario 5: Vertical search — security vulnerability (CVE)

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 list_domains --domain security
```

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "CVE-2024-3094" --domain security --sub_domain security.cve --max_results 3
```

### Scenario 6: Vertical search — legal document or case

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 list_domains --domain legal
```

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "contract dispute damages" --domain legal --sub_domain legal.case_law --max_results 5
```

### Scenario 7: Batch search — multiple independent queries in one call

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 batch_search --query "AAPL stock price" --query "TSLA earnings 2025" --query "GOOG market cap"
```

With full query objects (recommended for PowerShell to avoid quote-stripping issues):

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 batch_search --query AAPL --query TSLA --query GOOG
```

From a JSON file:

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 batch_search --queries @queries.json
```

### Scenario 8: Extract full page content

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 extract "https://en.wikipedia.org/wiki/Quantum_computing"
```

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 extract --url "https://example.com/news/article-12345"
```

### Scenario 9: News search with time filter

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "AI regulation" --content_types news --freshness day --max_results 5
```

### Scenario 10: Search with API key

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "climate change policy 2025" --api_key <your_api_key> --max_results 3
```

### Scenario 11: China-specific vertical search (requires zone=cn)

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 list_domains --domain finance
```

```powershell
powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1 search "600519" --domain finance --sub_domain finance.cn_stock --zone cn --max_results 5
```

---

## Rate Limit Handling
- On rate limit error with auto_registered api_key in response: present key to user for approval, then save to .env and retry
- On anonymous quota exhausted: inform user that a key provides higher limits; suggest configuring one via .env or environment variable
'@
}

function Show-Usage {
    @'
AnySearch CLI - Unified real-time search client.

Usage: anysearch.ps1 <command> [options]

Commands:
  search <query>         Search the web (general or vertical domain search)
  list_domains           Query domain directory for available sub_domains
  extract <url>          Fetch full page content from a URL
  batch_search           Execute 2-5 search queries in parallel
  doc                    Print AI-facing interface specification

Global Options:
  --api_key <key>        API key for authentication

Search Options:
  --domain, -d           Vertical domain
  --sub_domain, -s       Sub-domain routing key
  --sub_domain_params    Additional params as JSON
  --content_types, -t    Content filter (web,news,code,...)
  --zone, -z             Region: cn / intl
  --max_results, -m      Max results (default 10, max 100)
  --freshness, -f        Time filter: day/week/month/year

List-Domains Options:
  --domain               Single domain to query
  --domains              Batch domains (comma-separated or JSON array)

Batch-Search Options:
  --queries, -q          JSON array of query objects (or @file.json)
  --query                Repeatable single-query shorthand

Examples:
  .\anysearch.ps1 search "quantum computing"
  .\anysearch.ps1 search "AAPL" --domain finance --sub_domain finance.us_stock
  .\anysearch.ps1 list_domains --domain finance
  .\anysearch.ps1 extract https://example.com
  .\anysearch.ps1 batch_search --query AAPL --query GOOG
  .\anysearch.ps1 batch_search --queries '[{"query":"AAPL"},{"query":"GOOG"}]'
'@
}

$apiKey = if ($env:ANYSEARCH_API_KEY) { $env:ANYSEARCH_API_KEY } else { "" }

if ($args.Count -eq 0) {
    Show-Usage
    exit 0
}

$command = $args[0]
if ($args.Count -gt 1) {
    $rest = [array]$args[1..($args.Count - 1)]
} else {
    $rest = [array]@()
}

switch ($command) {
    "-h" { Show-Usage; exit 0 }
    "--help" { Show-Usage; exit 0 }
    "help" { Show-Usage; exit 0 }
}

switch ($command) {
    "search" {
        $query = ""
        $domain = ""
        $subDomain = ""
        $subDomainParams = ""
        $contentTypes = ""
        $zone = ""
        $maxResults = $null
        $freshness = ""

        $i = 0
        $positional = @()
        while ($i -lt $rest.Count) {
            if ($rest[$i] -match '^-') { break }
            $positional += $rest[$i]
            $i++
        }
        $query = $positional -join ' '

        while ($i -lt $rest.Count) {
            switch ($rest[$i]) {
                "--domain" { $domain = $rest[$i+1]; $i += 2 }
                "-d"       { $domain = $rest[$i+1]; $i += 2 }
                "--sub_domain" { $subDomain = $rest[$i+1]; $i += 2 }
                "-s"       { $subDomain = $rest[$i+1]; $i += 2 }
                "--sub_domain_params" { $subDomainParams = $rest[$i+1]; $i += 2 }
                "--content_types" { $contentTypes = $rest[$i+1]; $i += 2 }
                "-t"       { $contentTypes = $rest[$i+1]; $i += 2 }
                "--zone"   { $zone = $rest[$i+1]; $i += 2 }
                "-z"       { $zone = $rest[$i+1]; $i += 2 }
                "--max_results" { $maxResults = [int]$rest[$i+1]; $i += 2 }
                "-m"       { $maxResults = [int]$rest[$i+1]; $i += 2 }
                "--freshness" { $freshness = $rest[$i+1]; $i += 2 }
                "-f"       { $freshness = $rest[$i+1]; $i += 2 }
                "--api_key" { $apiKey = $rest[$i+1]; $i += 2 }
                default    { Write-Error "Unknown flag: $($rest[$i])"; exit 1 }
            }
        }

        if (-not $query) {
            Write-Error "Error: query is required"
            exit 1
        }

        Invoke-Search @{
            Query             = $query
            Domain            = $domain
            SubDomain         = $subDomain
            SubDomainParams   = $subDomainParams
            ContentTypes      = $contentTypes
            Zone              = $zone
            MaxResults        = $maxResults
            Freshness         = $freshness
            ApiKey            = $apiKey
        }
    }

    "list_domains" {
        $domain = ""
        $domains = ""

        $i = 0
        while ($i -lt $rest.Count) {
            switch ($rest[$i]) {
                "--domain"  { $domain = $rest[$i+1]; $i += 2 }
                "--domains" { $domains = $rest[$i+1]; $i += 2 }
                "--api_key" { $apiKey = $rest[$i+1]; $i += 2 }
                default     { Write-Error "Unknown flag: $($rest[$i])"; exit 1 }
            }
        }

        Invoke-ListDomains @{
            Domain = $domain
            Domains = $domains
            ApiKey  = $apiKey
        }
    }

    "extract" {
        $url = ""
        $positional = @()
        $i = 0

        while ($i -lt $rest.Count) {
            if ($rest[$i] -match '^-') { break }
            $positional += $rest[$i]
            $i++
        }
        $url = $positional -join ' '

        while ($i -lt $rest.Count) {
            switch ($rest[$i]) {
                "--url" { $url = $rest[$i+1]; $i += 2 }
                "-u"    { $url = $rest[$i+1]; $i += 2 }
                "--api_key" { $apiKey = $rest[$i+1]; $i += 2 }
                default { Write-Error "Unknown flag: $($rest[$i])"; exit 1 }
            }
        }

        Invoke-Extract @{ Url = $url; ApiKey = $apiKey }
    }

    "batch_search" {
        $queryItems = [System.Collections.Generic.List[string]]::new()
        $queries = $null
        $positional = $null
        $i = 0

        while ($i -lt $rest.Count) {
            switch ($rest[$i]) {
                "--queries" { $queries = $rest[$i+1]; $i += 2 }
                "-q"        { $queries = $rest[$i+1]; $i += 2 }
                "--query"   { $queryItems.Add($rest[$i+1]); $i += 2 }
                "--api_key" { $apiKey = $rest[$i+1]; $i += 2 }
                default     {
                    if (-not $positional) { $positional = $rest[$i] }
                    else { Write-Error "Unknown argument: $($rest[$i])"; exit 1 }
                    $i++
                }
            }
        }

        if ($positional -and -not $queries) { $queries = $positional }

        Invoke-BatchSearch @{
            Queries    = $queries
            QueryItems = $queryItems
            ApiKey     = $apiKey
        }
    }

    "doc" {
        Show-Doc
    }

    default {
        Write-Error "Unknown command: $command"
        Show-Usage
        exit 1
    }
}