CLI

dromad is the command-line client. Every command prints readable output by default and one JSON document with --json — the same object the API returns — so a person and an agent can drive it the same way.

pip install dromad        # or: uv tool install dromad · pipx install dromad
dromad --version

It needs Python 3.12 or newer, and depends on typer, httpx and pydantic only.

Supported versions. The server answers dromad 0.2.0 and newer. An older CLI is refused on every command with cli_outdated (exit code 2) before anything is run or charged, and told how to upgrade: uv tool upgrade dromad, pipx upgrade dromad or pip install -U dromad. Versions are refused when a command has changed meaning since: before 0.2.0 dromad fanout generated questions, which is now dromad questions.

Signing in

dromad login

dromad login

Opens the browser at https://dromad.dev/cli/auth with a short code. Check that the code matches, approve, and the CLI stores a token for this device in ~/.config/dromad/credentials.json (readable by you only). --no-browser prints the URL instead of opening it; --name sets how the device appears under Account › Sessions. To use a different Dromad server, pass --api-url once here; later commands follow the stored credential.

The token belongs to the account the approving browser is signed in to, which the CLI cannot know beforehand: with several browser profiles, that may not be the account you meant. It ends by printing Logged in as <email>. To make sure of it, name the account:

dromad login --email you@example.com

The flow is the same. If another account approves the code, nothing is stored, the token that was issued is revoked (if that fails, the message says so, and it can be removed under Account › Sessions), a credential stored earlier is left alone, and the command fails with account_mismatch (exit code 1) saying which account approved. Run it again and open the URL in the browser profile signed in to the right account. Addresses are compared without regard to case.

dromad whoami

dromad whoami

Which account and which credential the CLI is using, and where it came from. The first line (email under --json) is the account: check it is the one you mean, since its workspaces are what pay.

dromad logout

Revokes this device's token on the server and deletes the local credentials file.

API keys and the environment

For CI, containers and agents without a browser, create an API key at Account › API keys (it is shown once) and set it in the environment:

Variable Meaning
DROMAD_API_KEY A credential (dmk_…). Takes precedence over dromad login.
DROMAD_API_URL The server. --api-url on a command overrides it.
DROMAD_PROJECT The project to file work under (pr_…). Overrides the directory's link.
DROMAD_CONFIG_DIR Where the credentials and links files live (default ~/.config/dromad).

A stored credential is only ever sent to the server that issued it: pointing the CLI at another server makes it unauthenticated rather than leaking your token.

Projects and linking

Work is filed under a project (what you are working on), and a project belongs to a workspace (who you are working with: yourself, or a team that shares its work, its credit and its projects). Commands that make work need to know the project. Link a directory once and they do.

dromad init

dromad init
Select workspace:
  1. John
  2. Exa
Number: 2
Project name [docs]: Docs
Primary domain (optional): docs.exa.ai
Created Exa / Docs
Linked /Users/john/code/docs to Exa / Docs (pr_01M2X…)

Starts a project and links the current directory to it. Without a terminal, or with --json, it asks nothing: dromad init --workspace Exa --name Docs --domain docs.exa.ai. The name defaults to the directory's.

dromad link
dromad link --workspace Exa --project Docs
dromad link --project pr_01M2X…

Links the current directory to a project that already exists. From then on dromad prompt, dromad fanout, dromad questions and dromad audit run here, or in any directory below, are filed under it, and dromad runs list, dromad fanouts list and dromad analyses list show that project's work (--all shows everything you can see).

The link is a note in the CLI's own config on this machine (~/.config/dromad/links.json), keyed by the directory. Nothing is written into the directory, so a repository carries nothing and a fresh clone is linked to nothing: each person links it once. A link holds IDs and grants nothing; the server checks your credential against the workspace on every request.

dromad status

dromad status
Workspace:   Exa
Project:     Docs  (pr_01M2X…)
Domain:      docs.exa.ai
Chosen:      linked from /Users/john/code/docs

Where commands run here are filed. It exits 2 with not_linked when the directory has no project, which is how a script or an agent checks before starting work. --json prints the project with its context.

dromad list

dromad list

The projects you can work in, by workspace, with this directory's marked. --workspace narrows to one.

Forgets this directory's project. Nothing on the server changes.

One command, another project

--project pr_… on dromad prompt, dromad fanout, dromad questions or dromad audit overrides the link for that command, and DROMAD_PROJECT overrides it for a shell or a CI job. With no project from any of the three, the command stops before making a request:

Error: This directory is not linked to a project. Run `dromad init` to start one, or `dromad link` to choose one. (Or pass --project pr_…, or set DROMAD_PROJECT.)

Code: not_linked
Retryable: no

An API key acts in one workspace. If the linked project is in another, the command says the project was not found or the credential has no access to it, and points at dromad list.

Output, JSON and exit codes

  • Readable output goes to stdout; progress messages go to stderr.
  • --json prints exactly one JSON document on stdout, and nothing on stderr: no progress, no cost or balance lines. dromad … --json > out.json 2>&1 leaves a file that parses. The one exception is dromad login, whose sign-in URL and code are printed on stderr either way. On failure the document is the API's error envelope: {"error": {"code", "message", "engine", "retryable"}}. (A mistake in the arguments themselves — an unknown option, a missing argument — is reported as usage text on stderr, exit code 2, before --json has any say.)
  • Exit codes are stable:
Code Meaning
0 The command did what it was asked. A prompt where some engines failed is still 0; its batch is partial.
1 The operation failed: the server refused it, every engine failed, or there was no connection.
2 The command could not be attempted: bad arguments, not signed in, no project, unusable configuration, a CLI too old for the server, a fanout that needed --yes.

Error codes are the API's — of which cli_outdated exits 2, since nothing was attempted — plus these, for failures that never reach the server:

Code Exit Meaning
not_authenticated 2 No credential: run dromad login or set DROMAD_API_KEY.
invalid_config 2 The configuration cannot be used, such as the server URL or the credentials file.
connection_failed 1 The server did not answer; retryable is true.
not_linked No project: the directory is not linked, and neither --project nor DROMAD_PROJECT names one. Exit code 2.
access_denied 1 Sign-in was denied in the browser (dromad login only).
expired_token 1 The sign-in code expired before it was approved (dromad login only).
account_mismatch 1 dromad login --email was approved by a different account. Nothing was stored.
confirmation_required 2 A fanout of more than one prompt or run, with no terminal to ask at and no --yes. Nothing was started.

A credential the server refuses (revoked or unknown) is unauthenticated, exit 1.

Every ID says what it names: dr_ run, rq_ batch, fo_ fanout, fa_ questions analysis, ca_ citation analysis, pr_ project, ws_ workspace, sh_ share. A command handed the wrong kind of ID refuses it before making a request.

Fanout

Given a prompt, what does each engine actually search for? A fanout runs your prompts on the engines you pick, as many times as you ask, with web search on, and keeps every search query each run issued.

dromad fanout "free web search MCP server for Claude Code" --runs 3
1 prompt × 3 engines × 3 runs = 9 prompt runs · estimated cost $0.96
Start it? [y/N]: y

free web search MCP server for Claude Code
1 prompts · 3 engines · 17 observed queries · 9 runs (3 per prompt and engine) · completed

1. “free web search MCP server for Claude Code”
   CLAUDE · 8 queries
     run 1 · dr_01M2…
        1  free MCP web search server Claude Code
        2  Brave Search MCP Claude Code
        3  Exa MCP free tier
     run 2 · dr_01M2…
        1  free MCP web search server Claude Code
        2  Exa MCP free tier
        3  Cursor web search MCP
   CHATGPT · 6 queries
     run 1 · dr_01M2…
        1  free MCP web search server Claude Code
           Brave MCP vs Exa MCP
           Tavily MCP free plan
   …

Give one prompt or several (dromad fanout "first" "second"), or a file with one prompt to a line (--prompts-file prompts.txt, or - for stdin). Prompts are kept exactly as written and in order; a prompt given twice is run twice.

Options: --engines (default all three), --runs (1–5, default 1: independent runs of each prompt on each engine), --name, --estimate (print what it would run and cost, and start nothing), --yes, --no-wait, --timeout (default 1800).

An estimate is shown first (on stderr; under --json nothing is, so ask for it with --estimate --json). Each execution is an ordinary prompt run at its engine's prompt price, so a fanout costs prompts × engines × runs of those. Anything more than one prompt run once asks before starting; when not at a terminal (an agent, a script) it stops with confirmation_required, exit code 2, unless you pass --yes. The estimate is there so you can decide, and is not a quote: nothing is held to it. What decides whether a fanout starts is the workspace's credit at that moment. The whole fanout is priced and its credit set aside at once, all or nothing, so a fanout the credit does not cover starts none of its runs (insufficient_credit). Each run is then charged when it completes; one that fails is not charged.

One fanout queues at most 30 prompt runs at once. That is a limit of the job queue, not of spending: split a larger set of prompts across several fanouts.

How to read it. The result is a fanout (fo_…) holding prompt → engine → run → search → query:

  • Searches are numbered in the order the provider's envelope gives them. Claude searches one query at a time. ChatGPT and Gemini often issue several queries in one search: those share a number, because no order among them was observed.
  • Nothing is merged. A query that appears in two runs was searched twice and is listed under each; query_count counts observations, not distinct texts. Comparing runs is left to you.
  • A run with no queries says which of three things happened: it searched but the provider did not show the text, it did not search, or that was not recorded. Unknown is never reported as "did not search".
  • Each run's answer, sources and raw provider envelope are on the run: dromad runs show dr_… --json.
dromad fanout -f prompts.txt --runs 3 --yes --json \
  | jq -r '.prompts[] | .text as $p | .engines[] | .engine as $e | .runs[] | "\($e) run \(.run_index): \([.queries[].text] | join(" | "))"'

dromad fanouts list, dromad fanouts show

dromad fanouts list
dromad fanouts show fo_01M2… --prompt 1 --engine claude

list shows this directory's project's fanouts, newest first (--all for everything you can see). show prints one fanout; --prompt N and --engine narrow it to one prompt or one engine, in --json too.

Questions

Generate the questions people might ask AI assistants about a topic, grouped by intent. These are written by a model from the topic: a starting list of prompts, not something any engine was observed to search. (This command was called dromad fanout before version 0.2.)

dromad questions "AI search API"
AI search API

Generated 30 questions across 6 clusters.

DISCOVERY OF OPTIONS  (discovery)

   1. What are the best AI search APIs available right now?  [general]
   2. Which companies offer AI-powered search APIs?  [general]
   3. List the top AI search APIs for building a chatbot  [mid]
   …

Options: --count (5–50, default 30), --no-wait (print the ID at once), --timeout (seconds to wait, default 300).

The result is a durable analysis (fa_…). Questions keep stable IDs (q01…) and each has a cluster, an intent and a specificity (general, mid, niche); the full generation prompt is kept as provenance. In JSON, questions and clusters are top-level fields:

dromad questions "AI search API" --json | jq -r '.questions[] | "\(.id) \(.text)"'

It makes one engine call (Claude, web search off) and costs $0.002 for each question it generates: the price is held for --count questions and charged for those that come back. To see what engines search for any of them, pass them to dromad fanout.

Prompt

Ask one question of one or more engines, each with its own web search forced on.

dromad prompt "What are the best AI search APIs available right now?"
CHATGPT  ✓  49 sources · 4 cited

As of **September 12, 2026**, these are the strongest AI-search API options. …

Sources
  1  tavilyai.mintlify.app/documentation/api-reference/introduction?utm_source=ope…
     cited 1× · position 1
  …
Run dr_01M29PRXBX5GMD85SSW2VNY40X · openai/gpt-5.6-luna · 24.8s · $0.05

CLAUDE  ✓  13 sources · 10 cited
…
Batch rq_01M29PRXBRFHWZKXTR0WBV2E6B · completed · 3 of 3 engines completed · $0.32

Options: --engines / -e (comma-separated from chatgpt, claude, gemini; default all three), --no-wait, --timeout (default 600; the runs keep going if it passes).

Each engine's execution is a run (dr_…); together they are a batch (rq_…). One engine failing does not fail the others: the batch is partial and the command still exits 0. --json prints {"batch": …, "runs": [...]}. Each run's result.sources lists every source the engine showed, with cited as true, false or null — null means the provider does not expose it, which is not the same as "not cited":

dromad prompt "best search APIs for AI agents" --json \
  | jq -r '.runs[] | .engine as $e | .result.sources[] | select(.cited == true) | "\($e) \(.url)"'

With --no-wait the command prints the batch and run IDs immediately; follow up with dromad runs list --batch rq_….

The text output ends with the batch's page, View: …/batches/rq_…: the prompt, then each engine's answer and the links it cited. --json has no such line.

Citations

Count what a set of prompt runs cited: domains, pages, engines, overlap.

dromad citations rq_01M29PRXBRFHWZKXTR0WBV2E6B rq_01M29PRXJBE6JT4GHDHM52MFDA --domain exa.ai --brand Exa
24 prompt runs · 8 prompts · 3 engines
465 citations · 131 unique URLs · 84 unique domains
427 sources seen but not cited

DOMAIN            CITATIONS   PAGES   RUNS   PROMPTS   ENGINES
firecrawl.dev            45       5     13         7   chatgpt, claude, gemini
parallel.ai              30       3      8         6   claude, gemini
…

DOMAIN COVERAGE  exa.ai
  cited in 9 of 24 runs (38%) · 5 of 8 prompts · 10 citations
  by engine: chatgpt 5/8 · claude 1/8 · gemini 3/8
  pages cited: docs.exa.ai/reference/search, exa.ai, exa.ai/docs/reference/search, exa.ai/pricing
  seen but not cited on 1 prompt
  absent from 2 prompts, e.g. “Should I use an AI search API or just call a search engine API directly?”

Arguments are run IDs and/or batch IDs, mixed freely. Options: --domain / -d (report coverage for a domain; repeatable), --brand / -b (count a term in answer text; a naive whole-word match; repeatable), --top (rows per table, default 10).

Only citations an engine showed are counted; sources whose citation status is unknown are reported as unknown, never as uncited. Runs that are not completed prompt runs are excluded, and listed with the reason. The result is a durable analysis (ca_…); the JSON has every row, not just the top ones (domains, pages, engines, overlap, domain_coverage, textual_mentions, excluded_runs).

With no --brand and no --domain, the analysis uses what the runs' project records: its domains, and its brands and competitors as terms. The command says so on stderr, the analysis records what was counted, and --no-context counts without it. Set a project's context on its page, or with dromad init --domain.

An analysis counts answers already stored, and finished runs never change, so the same runs with the same domains and brands always give the same numbers. To look at an analysis again, use dromad analyses show; run dromad citations again only to ask a different question (other domains, brands or runs).

The domain table is ordered by citation count, and engines differ a lot in how many citations they attach to one answer — compare the RUNS and PROMPTS columns before reading much into the order. A citation analysis calls no model and is included.

Audit

Fetch one page as a crawler would and report its facts and findings.

dromad audit https://exa.ai/docs/reference/search
https://exa.ai/docs/reference/search

Fetched      success 200 · text/html · 146 ms
Title        Search - Exa
Content      2,444 words · 21 code blocks · 0 tables · 35 links (33 internal) · via generic_selector
Outline      4 headings in the main content · 1 level skip
Structured   Organization, WebSite, WebPage, BreadcrumbList, TechArticle, APIReference
llms.txt     exa.ai/llms.txt found (52 links) · exa.ai/llms-full.txt found (263 links)

FINDINGS
  ✗ heading_level_skip        fail           low    deterministic
      jumps from h2 to h4 at “Authorizations”
  ✓ missing_h1                pass           medium deterministic
  …

Options: --no-wait, --timeout (default 180).

The page is fetched once: robots.txt is honoured, redirects are followed hop by hop and recorded, and private network addresses are refused. Dromad does not run JavaScript; a page that looks like a JavaScript shell is reported as one. Facts come first — fetch, content, structure, metadata, llms.txt — and findings are derived from them. Each finding states its evidence level:

Level Meaning
deterministic An observed fact about the page.
supported An established consequence of an observed fact.
suggestive A signal with some evidence behind it, not proof.
experimental A hypothesis Dromad is still testing.

dromad audit --json prints the audit result itself, with run_id at the top level (.findings, .content.word_count). dromad runs show dr_… --json prints the run, with the same result under .result. An audit calls no model and costs $0.01 when it completes.

Runs

dromad runs list

dromad runs list
dromad runs list --batch rq_01M29PRXBRFHWZKXTR0WBV2E6B

Your runs, newest first: prompt runs by engine (a fanout's runs among them), audits, and the generation runs behind questions (shown as questions/claude). Options: --batch, --limit (1–200, default 20). Times are UTC.

dromad runs show

dromad runs show dr_01M29PRXBX5GMD85SSW2VNY40X

One run in full: a prompt run's answer and sources, or an audit's facts and findings. --wait waits up to 30 seconds for an unfinished run. Finished runs never change, so there is no reason to run the same prompt twice to look at it again.

Analyses

dromad analyses list

dromad analyses list --kind citations

Your question sets and citation analyses, newest first. Options: --kind (questions or citations), --limit.

dromad analyses show

dromad analyses show ca_01M29PXV69QRXDQDR30J720S7X

One analysis in full, rendered as the command that created it would.

Sharing

dromad share

dromad share ca_01M29PXV69QRXDQDR30J720S7X

Prints a link anyone can open without an account. The page is read-only and asks search engines not to index it. Only finished runs and analyses can be shared. Sharing again prints the same link; --rotate replaces it, and the old link stops working.

dromad unshare

Stops sharing: the link stops working at once.

Balance

dromad balance
Balance              $18.42
  purchased          $15.00
  promotional         $3.42
Spent this month      $6.58

Add credit: https://dromad.dev/account/billing

The account's prepaid credit. Promotional credit is spent before purchased credit. While work is queued or running, its price is held: two more lines show what is held and what is left for new work. After a command that waited for charged work, the CLI prints the balance on stderr, so stdout and --json are unchanged. --json prints the summary with amounts as decimal strings.

Pricing

dromad pricing
PRODUCT     ENGINE    PRICE
prompt      chatgpt   $0.05 / prompt
prompt      claude    $0.20 / prompt
prompt      gemini    $0.07 / prompt
questions             $0.002 / question
audit                 $0.01 / url
citations             included

A prompt on all 3 engines (the default): $0.32

A fanout is charged as its prompt runs: prompts × engines × runs.

Rate card 2026-09-v2

The rate card in effect, which needs no credential. Prices are per unit: a prompt on several engines costs the sum of its engines' prices, question generation is priced by the questions it generates, and a fanout has no price of its own: it is its prompt runs. --json prints the card with prices as decimal strings.

Opening in the browser

dromad open

dromad open ca_01M29PXV69QRXDQDR30J720S7X
dromad open rq_01M29PRXBRFHWZKXTR0WBV2E6B --print

Opens a run, batch or analysis in the browser (a batch opens on its own page, /batches/rq_…); --print prints the URL instead, for agents and CI.