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).
██████████████████░░ 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.
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.
Homebrew (macOS) — recommended on Mac:
brew install sapihav/tap/perplexityThe 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 | bashDownloads 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@latestThis drops a perplexity binary in $(go env GOPATH)/bin.
Set your Perplexity API key (create one at https://www.perplexity.ai/settings/api):
export PERPLEXITY_API_KEY=pplx-xxxxxxxxxxxxxxxxThere is no config-file fallback. Missing env var exits with code 2.
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 "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 "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[]}.
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 | 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"
}
]
}
}| 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 |
| 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.
- stdout (success): one JSON envelope per invocation —
{schema_version, provider, command, elapsed_ms, result}. Per-command payload underresult: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:
0success,1API error (HTTP ≥ 400 after retries),2user/config error (e.g., missing env var),3network error.
go test ./...
go test -cover ./internal/client/...
go build -o perplexity .MIT — see LICENSE.