Research first. Then write.
A terminal agent that reads what ranks, researches with sources, finds what competitors miss, and writes a draft that cites its facts.
leoagent.dev · Install · How it works · FAQ · Architecture
An AI can write a thousand words about anything. The hard part is writing the right thousand words: the ones that match what searchers want, say something the top results don't, and hold up when a reader checks the facts.
- Know before you write. Leo reads the current results for your keyword and the pages that rank, then researches the topic with sources, all before the first sentence.
- Beat what ranks, don't copy it. The brief lists what every competitor covers and the gaps none of them fill. The draft is written against that.
- Check its work. Every fact links to where it came from, and every intermediate step is saved as a file you can read.
- Resumable. Each stage checkpoints to
.leo/runs/<slug>/. If a run fails or you press Ctrl+C, run the same command again and finished stages are reused, not paid for twice. - Only Claude is required. Every other provider makes a stage better and has a fallback, so a Claude key alone produces a researched, cited article.
- Hard spend limits. Each run has a budget (default $3). Every Claude call is capped at what's left, and the run stops cleanly before going over.
- Scriptable.
--jsonstreams one event per line, output is plain text when piped, and exit codes are meaningful. A keyword queue handles batches. - Safe by design. No shell and no file-writing tools. Pipeline steps get web search at most, and chat mode gets only Leo's own typed tools.
- Two ways to use it. One command per article, or
leoon its own for a chat that brainstorms topics and starts runs you can watch live.
Leo picks the best provider you've configured for each stage and falls back automatically:
| Provider | Used for | Without it |
|---|---|---|
| Claude (required) | Brief, draft, image direction | Uses a Claude Code sign-in, Bedrock, or Vertex |
| DataForSEO | Live Google results | Firecrawl search, then Claude web search |
| Firecrawl | Reading competitor pages | Built-in HTML fetch |
| Perplexity | Cited research | Claude web search |
| OpenRouter | Hero and section images | Images are skipped |
| Sanity | Publishing | Markdown in content/posts/ |
leo doctor shows which path each stage will take on your machine.
-
Clone the repo, install, and link the
leocommand. Installing also builds it:git clone https://github.com/BlockchainHB/leo.git cd leo npm install npm link -
In your blog's folder, describe the blog and add whichever API keys you have:
leo init
-
Write something:
leo write how to price a saas product
Requires Node 22.12 or later. The finished article is at .leo/runs/<slug>/article.md. Publish it with leo publish <slug>.
| Command | What it does |
|---|---|
leo |
Chat: brainstorm topics, check what ranks, then ask for an article and watch it run |
leo init |
Create leo.config.json, add keys to .env, and update .gitignore |
leo write <keyword> |
Research and write one article |
leo write --queue [n] |
Work through queued keywords (--next does one) |
leo queue add "a" "b" |
Queue keywords. Also queue ls, queue rm <id>, queue clear [--done] |
leo publish <slug> |
Send to Sanity as a draft, or write content/posts/<slug>/index.md |
leo runs |
List articles with their status and cost |
leo doctor |
Check Node, config, and providers |
write also takes --publish, --no-images, --fresh (ignore cached stages) and --budget <usd>. Every command takes --json:
leo write --queue 10 --json | jq -c 'select(.type == "run:done") | {slug, words, costUsd}'Exit codes: 0 success, 1 failure, 130 cancelled.
| Stage | Runs as | Saved to |
|---|---|---|
| Search results | Code: DataForSEO or Firecrawl search. Claude web search as a fallback | serp.json |
| Research | Code: Perplexity Agent API. Claude with web search as a fallback | research.json |
| Competitor pages | Code: fetch the top pages, then count words, headings, lists and FAQs | competitors.json |
| Content brief | Claude Sonnet with a schema: intent, angle, outline, sourced facts, gaps, FAQs | brief.json |
| Draft | Claude Opus, streamed live, following the brief and your house rules | draft.md |
| Images | Claude Haiku writes art direction, OpenRouter renders it | images/ |
Research and competitor reading run in parallel. Everything is assembled into article.md with frontmatter (title, description, excerpt, sources, hero image) and checked for length, avoided phrases and missing citations.
The pipeline lives in src/pipeline/run.ts. docs/ARCHITECTURE.md explains why it's a pipeline rather than one big agent.
Configuration
leo init writes leo.config.json. Only blog is required, and the file is validated on load with every problem reported by path.
Keys go in .env next to it. See .env.example.
- Talks only to Anthropic and the providers you configure. No analytics, no telemetry, no servers of its own.
- Keys live in your project's
.env, whichleo initadds to.gitignore. - Leo never loads your personal Claude Code setup (connectors, skills, plugins, hooks or memory) into its calls.
- The article in
examples/cost $0.29 of Claude usage with no other providers configured.
Do I need every API key?
No. Only Claude. The other providers improve individual stages, and each one has a fallback. Run
leo doctor to see what's active.
A run failed halfway. Do I start over?
No. Run the same command again. Finished stages are reused from
.leo/runs/<slug>/. Use --fresh if you do want to start over.
Does it publish on its own?
Only when you run
leo publish or pass --publish. Sanity posts are saved as drafts, so a person still presses Publish.
Can I use it with a CMS other than Sanity?
Yes. Leo writes standard markdown with YAML frontmatter to
content/posts/<slug>/index.md, which Astro, Next.js, Hugo and most static site generators read directly.
How is this different from asking a chatbot for a blog post?
A chatbot writes from memory. Leo reads the current results, researches with sources it cites inline, measures how competitors structure the topic, and writes against the gaps it found. The brief and every intermediate file are saved, so you can see why the article says what it says.
Which models does it use?
Claude Opus for the draft, Sonnet for the brief and research, and Haiku for quick structured tasks. Change any of them under
models in leo.config.json.
From your clone, run commands from source without rebuilding:
npm run dev -- write "test keyword"Run the 24 tests with npm test, and rebuild the linked leo with npm run build. Read docs/ARCHITECTURE.md for how it's built and CONTRIBUTING.md to get involved.
MIT © Hasaam Bhatti. The example article was written by Leo for a fictional blog.

{ "blog": { "name": "Shipyard", "url": "https://shipyard.dev", // your own pages are skipped as competitors "niche": "developer tooling and CLI design", "audience": "senior engineers who build internal tools", "voice": "direct, opinionated, practical" }, "author": { "name": "Hasaam" }, "writing": { "pointOfView": "second-person", "includeFaq": true, "avoid": ["em dashes", "delve"], "rules": [] }, "seo": { "location": "United States", "language": "en", "competitors": 5 }, "images": { "enabled": true, "model": "google/gemini-3.1-flash-image", "sections": 2 }, "internalLinks": [{ "title": "Our CLI style guide", "url": "/guides/cli-style", "topics": ["cli", "ux"] }], "publish": { "provider": "sanity", "projectId": "abc123", "dataset": "production" }, "models": { "writer": "claude-opus-5-5", "analyst": "claude-sonnet-5-5", "fast": "claude-haiku-4-5" }, "budgetUsd": 3 }