Zhang Lu.
POSTS · · 6 min read

I built a CLI for agents: replace HTML scraping with one line of npx zhanglu-net

No MCP, no SDK. Just 9 static JSON endpoints and a 270-line CLI. Shipped in 1.5 hours.

UPDATE · 2026-07-24

What follows is an account of one day — 2026-06-09, zero to shipped — and the numbers are from that day (9 endpoints, a 270-line CLI). The interface kept growing since: /api/presentations.json was added, the site went bilingual (a whole parallel set of English endpoints — prefix any path with /en), and the CLI gained --lang. For current state, always trust /en/agents and /en/llms.txt.

The trigger

zhanglu.net went live yesterday. The very first version shipped with an index of 30 Claude Skills, 3 projects, and 1 WeChat article entry. Then the question came:

An AI agent wants to reference content on my site — how?

The options on the table are bad:

  1. Scrape HTML — what the agent gets is a layout meant for humans, which it has to parse. Wasted tokens, unstable fields.
  2. MCP server — Claude Code supports it, but Codex / Hermes / OpenClaw / the little agents I write myself may not.
  3. Don't bother — then why did I build this site.

I picked a fourth option: add a JSON API layer + a CLI to the site.

The two layers of the design

           zhanglu.net (Cloudflare Pages, fully static)
                  │
   ┌──────────────┼──────────────┐
   │              │              │
HTML pages    /api/*.json     /llms.txt
(for humans)  (for agents)    (self-discovery)
                  │
       ┌──────────┴──────────┐
       │                     │
   curl / fetch         npx zhanglu-net (CLI)
   (any agent)          (Node 18+, zero runtime deps)

Layer one: 9 static JSON endpoints

/api/index.json       manifest (counts + all endpoints)
/api/projects.json    project list
/api/projects/{slug}.json  single project (with body_md)
/api/articles.json    WeChat article entries
/api/skills.json      Claude Skill index
/api/skills/{slug}.json    single skill (with body_md)
/api/about.json       bio
/api/social.json      public socials (email redacted)
/api/search.json      flat corpus, searched by the CLI client

Generated once as static files at build time, edge-cache friendly on CF Pages, Access-Control-Allow-Origin: *, so browser-side agents can use them directly too.

Layer two: the CLI

npx zhanglu-net list skills --featured
npx zhanglu-net get skill boss --md
npx zhanglu-net search "brand judgment" --type skill
npx zhanglu-net about --json

The CLI is a thin wrapper over the JSON endpoints. --json for agents to pipe; human-readable by default, with ANSI colors (auto-disabled when the agent side isn't a TTY).

A few deliberate decisions not to do things

No MCP server. Agents beyond Claude Code may not support MCP yet. HTTP + JSON is the greatest common denominator. If MCP users grow in the future, add a CF Worker to wrap the existing endpoints into an MCP server — a manageable cost.

No server-side search. The corpus is just 30 skills + 3 projects + a few articles, under 100KB of full text. The CLI pulls one /api/search.json and does text.toLowerCase().includes(needle) locally — that's enough. Simple scoring: title hit +5, occurrences accumulate. Switch to MiniSearch when the corpus grows to a few hundred.

Zero runtime deps in the CLI. Node 18+'s built-in fetch replaces node-fetch, node:util's parseArgs replaces commander, hand-rolled ANSI escape codes replace chalk. npx zhanglu-net starts fast and doesn't stall when an agent calls it.

No endpoint for WeChat article bodies. WeChat URLs block scrapers; WebFetch can't reach them. The site only stores the entry (title / date / summary / url), and the body links out.

Self-discovery: /llms.txt

llmstxt.org is pushing a convention — put an llms.txt at the site root that tells AI agents "here's what I have and how to read it." I followed it:

# zhanglu.net

> Zhang Lu's personal site.

## For AI agents
- /api/index.json — manifest
- /api/skills.json — Claude Skills index
- ...

## CLI
npx zhanglu-net --help

Any agent handed https://zhanglu.net just does GET /llms.txt first, and it knows where to go next.

How to use it

In Claude Code: I wrote a /zhanglu skill, with trigger phrases like "look up Zhang Lu's skill" and "X on zhanglu." The skill automatically calls npx zhanglu-net.

Other agents:

# List all my featured skills
curl -s https://zhanglu.net/api/skills.json \
  | jq '.items[] | select(.featured)'

# Get the full text of the boss skill
curl -s https://zhanglu.net/api/skills/boss.json \
  | jq -r .body_md

# Full-text search
curl -s https://zhanglu.net/api/search.json \
  | jq '.items[] | select(.text | test("brand"; "i"))'

Or just:

npx zhanglu-net list skills --featured --json
npx zhanglu-net get skill boss --md
npx zhanglu-net search "brand"

Time to build

Endpoints (9 files):          25 min
CLI (single file, 270 lines): 30 min
llms.txt / robots.txt:        5 min
Skill (SKILL.md + sync):      10 min
Design doc + this post + draft: 20 min
AGENTS.md / README updates:   10 min
build + test + commit:        15 min
─────────────────────────────────
Total:                        ~1.5 hours

Including drafting this very article. Leaving yourself enough time to write the docs matters more than the code itself.

Take it now

If you also maintain a content site, I strongly recommend adding a JSON API layer + /llms.txt. No framework upgrade, no new service required. Astro / Next / Hugo can all generate static JSON at build time. Cloudflare Pages caches it automatically. Zero cost.

The next agent that comes to scrape your site won't get stuck on HTML parsing anymore.

← Integration guide /posts/agent-cli