Skip to content

Repository files navigation

perplexity-cli

Thin Go CLI wrapping the Perplexity Sonar API. Returns AI answers with citations as structured JSON — agent-friendly, pipeable, no hidden state.

Binary name: perplexity (not perplexity-cli).

Parity

██████████████████░░ 88%search, ask, reason, research submit, research get shipped (4 of 4 MCP tools, M5 async core landed). Blocking research run + jobs list (M6) remain. See PARITY.md.

Status

Milestone 5 shipped — added the async deep-research core (research submit + research get) wrapping POST /async/chat/completions and GET /async/chat/completions/{id}. M4 brought reason (chain-of-thought via sonar-reasoning-pro); M3 brought the standalone POST /search endpoint; ask keeps chat-completions synthesis. All M2 contract flags (--dry-run, --timeout, --json-errors, --rate-limit, --user-agent, stdin -) apply to every subcommand. See docs/backlog/ for what's next and CHANGELOG.md for release notes.

Install

Homebrew (macOS) — recommended on Mac:

brew install sapihav/tap/perplexity

The tap auto-installs on first use; subsequent brew upgrade picks up new releases.

One-line install (Linux / macOS) — no Go toolchain required:

curl -sSL https://raw.githubusercontent.com/sapihav/perplexity-cli/main/install.sh | bash

Downloads the latest release for your OS/arch, verifies SHA-256, installs perplexity to /usr/local/bin. Override with INSTALL_DIR=$HOME/.local/bin. Requires curl + jq.

From source (requires Go 1.25+):

go install github.com/sapihav/perplexity-cli@latest

This drops a perplexity binary in $(go env GOPATH)/bin.

Auth

Set your Perplexity API key (create one at https://www.perplexity.ai/settings/api):

export PERPLEXITY_API_KEY=pplx-xxxxxxxxxxxxxxxx

There is no config-file fallback. Missing env var exits with code 2.

Usage

perplexity search — ranked web results (no AI synthesis)

Wraps POST /search and returns a list of ranked web results. No answer, no citations — those live on ask.

perplexity search "what is the capital of France?"
perplexity search "llm evals 2026" --max-results 20 --recency 1w
perplexity search "raft consensus" --domain wikipedia.org --language en --country US
perplexity search "ipo filings" --date-from 2024-01-01 --date-to 2024-12-31
echo "nvidia earnings q4" | perplexity search -

Flags (on search): --max-results N (1–20, default 10), --country ISO, --language ISO (ISO 639-1, repeatable, ≤10), --domain DOMAIN (repeatable, ≤20), --exclude-domain DOMAIN (repeatable, ≤20; mutually exclusive with --domain), --recency 1h|1d|1w|1m|1y (mutually exclusive with --date-from/--date-to), --date-from YYYY-MM-DD, --date-to YYYY-MM-DD.

perplexity ask — conversational answers (sonar-pro by default)

perplexity ask "summarise the Raft consensus algorithm"
perplexity ask "continue the analysis" --messages @history.json
echo "what changed between Go 1.24 and 1.25?" | perplexity ask -
perplexity ask "rewrite this prompt" --system "You are a terse editor" --max-tokens 400

--messages @file.json accepts a prior conversation as an array of {role, content} objects; the positional query is appended as the final user turn.

perplexity reason — step-by-step reasoning (sonar-reasoning-pro)

perplexity reason "if a train leaves Paris at 9am at 200km/h, when does it reach Lyon?"
perplexity reason "explain RCU vs spinlocks" --strip-thinking=false   # keep raw <think> in answer
perplexity reason "tricky math problem" --model sonar-reasoning       # smaller reasoning model
echo "why is the sky blue?" | perplexity reason -

The reasoning models emit a <think>...</think> chain-of-thought block followed by the final answer in the same content stream. By default the CLI splits them: the cleaned final answer goes to result.answer, the chain-of-thought to result.thinking. Pass --strip-thinking=false to receive the raw content unchanged (and no thinking field).

reason output payload: {answer, thinking?, model, citations[]}.

perplexity research — async deep research (sonar-deep-research)

sonar-deep-research jobs routinely exceed sync HTTP timeouts, so the API is async-only: submit returns a job id, then poll until status flips to COMPLETED. The CLI mirrors that lifecycle as two subcommands.

# 1. Submit a job — returns immediately with {job_id, status, model, created_at}.
perplexity research submit "compare leading vector databases for RAG in 2026" \
  --reasoning-effort high

# 2. Poll the job by id. Exits 0 on COMPLETED or in-flight, 1 on FAILED.
perplexity research get <job_id>

# Pipe a longer prompt from stdin or a file:
cat prompt.txt | perplexity research submit -
perplexity research submit - <<< "deep dive on RISC-V toolchain maturity"

Submit flags: --reasoning-effort low|medium|high (default medium), --system <prompt>, --messages @file.json, --max-tokens N, --model (default sonar-deep-research).

Get flags: --strip-thinking (default true) — splits the upstream <think>...</think> block out of result.answer and surfaces it under result.reasoning. Pass --strip-thinking=false to keep the raw content inline.

research submit payload: {job_id, status, model, created_at}. research get payload: {job_id, status, model, created_at, started_at?, completed_at?, failed_at?, answer?, reasoning?, error_message?, citations[]}. Status values are CREATED, IN_PROGRESS, COMPLETED, FAILED.

perplexity schema — self-describing command tree

perplexity schema | jq .

Returns the full command tree (flags, defaults, subcommands) as JSON. Agents should prefer this over scraping --help.

Example search output (compact by default, one JSON envelope per invocation):

{"schema_version":"1","provider":"perplexity","command":"search","elapsed_ms":1234,"result":{"results":[{"title":"Paris","url":"https://en.wikipedia.org/wiki/Paris","snippet":"Paris is the capital of France.","published_date":"2024-01-15","domain":"en.wikipedia.org"}]}}

Pretty-printed:

perplexity search "what is the capital of France?" --pretty
{
  "schema_version": "1",
  "provider": "perplexity",
  "command": "search",
  "elapsed_ms": 1234,
  "result": {
    "results": [
      {
        "title": "Paris",
        "url": "https://en.wikipedia.org/wiki/Paris",
        "snippet": "Paris is the capital of France.",
        "published_date": "2024-01-15",
        "domain": "en.wikipedia.org"
      }
    ]
  }
}

Flags

Per subcommand

Flag Scope Default Purpose
--max-results N search 10 Number of ranked results (1–20)
--country ISO search unset Regional bias (e.g. US, GB)
--language ISO search unset ISO 639-1 language filter (e.g. en); repeatable, up to 10 entries
--domain DOMAIN search unset Restrict to domain (repeatable)
--exclude-domain DOMAIN search unset Exclude domain (repeatable)
--recency DURATION search unset 1h, 1d, 1w, 1m, 1y
--date-from YYYY-MM-DD search unset Earliest publish date
--date-to YYYY-MM-DD search unset Latest publish date
--model ask sonar-pro Model name for ask
--max-tokens N ask unset Cap response tokens
--system <prompt> ask unset System prompt
--messages @file.json ask unset Prior conversation (array of {role,content})
--model reason sonar-reasoning-pro Reasoning model (sonar-reasoning-pro | sonar-reasoning)
--max-tokens N reason unset Cap response tokens
--system <prompt> reason unset System prompt
--messages @file.json reason unset Prior conversation (array of {role,content})
--strip-thinking reason true Strip <think>...</think> from result.answer and surface it under result.thinking
--model research submit sonar-deep-research Async-eligible model
--reasoning-effort research submit medium Depth/cost knob: low | medium | high
--max-tokens N research submit unset Cap response tokens
--system <prompt> research submit unset System prompt
--messages @file.json research submit unset Prior conversation (array of {role,content})
--strip-thinking research get true Strip <think>...</think> from result.answer and surface under result.reasoning

Global (persistent on every subcommand)

Flag Default Purpose
--dry-run off Print the planned request (Authorization redacted) and exit without calling the API
--timeout SEC 60 Per-request timeout in seconds
--rate-limit N/s 1 Client-side rate limit (0 disables)
--max-retries N 3 Retries on 429 / 5xx with exponential backoff
--json-errors off Emit errors as {error:{message,code}} on stderr
--user-agent STR perplexity-cli/… Override User-Agent header (also via PERPLEXITY_USER_AGENT)
--pretty off Indent JSON output
--out <file> Write JSON to a file instead of stdout
--verbose, -v off Progress logs to stderr
--quiet, -q off Suppress non-error stderr output

Any subcommand that takes a query also accepts - to read from stdin.

Output contract

  • stdout (success): one JSON envelope per invocation — {schema_version, provider, command, elapsed_ms, result}. Per-command payload under result: search{results[]{title,url,snippet,published_date,domain}}; ask{answer, model, citations[]}; reason{answer, thinking?, model, citations[]}; research submit{job_id, status, model, created_at}; research get{job_id, status, model, created_at, started_at?, completed_at?, failed_at?, answer?, reasoning?, error_message?, citations[]}.
  • stderr: human-readable progress / errors only. No envelope on the error path.
  • Exit codes: 0 success, 1 API error (HTTP ≥ 400 after retries), 2 user/config error (e.g., missing env var), 3 network error.

Development

go test ./...
go test -cover ./internal/client/...
go build -o perplexity .

License

MIT — see LICENSE.

About

Thin CLI wrapper for the perplexity API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages