# Qiaomu Opencli

> Use when running OpenCLI commands to interact with websites (Bilibili, Twitter, Reddit, Xiaohongshu, etc.), desktop apps (Cursor, Notion, Claude), or public APIs (HackerNews, arXiv, npm, pubmed). Also covers browser automation (navigate/click/type/extract), auto-fixing broken adapters, and creating new adapters for any website. Covers 136 site adapters, 7 app adapters, 13 external CLIs.

- Skill: `joeseesun/qiaomu-opencli` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add joeseesun/qiaomu-opencli`
- Raw SKILL.md: https://api.skillmd.com/api/skills/joeseesun/qiaomu-opencli/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: joeseesun (https://skillmd.com/u/joeseesun)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/joeseesun/qiaomu-opencli

---


# OpenCLI — Complete Guide

> Make any website or Electron App your CLI. Reuse Chrome login, zero risk, AI-powered discovery.

---

## Install & Run

```bash
npm install -g @jackwener/opencli
opencli <command>

npm update -g @jackwener/opencli   # Update
opencli --version                  # Check version
```

## Prerequisites

Browser commands require:
1. Chrome running **(logged into target sites)**
2. **opencli Browser Bridge** Chrome extension installed
3. Daemon auto-starts on first browser command

```bash
opencli doctor    # Diagnose extension + daemon connectivity
```

> Public API commands (`hackernews`, `arxiv`, `npm`, `pubmed`…) need no browser.

---

## Command Quick Reference

Usage: `opencli <site> <command> [args] [--limit N] [-f json|yaml|md|csv|table]`

Type legend: 🌐 = Browser (needs Chrome login) · ✅ = Public API · 🖥️ = Desktop (Electron/CDP) · 🔧 = External CLI

### Website Adapters (136 sites)

| Site | Type | Commands |
|------|------|----------|
| **1688** | 🌐 | `search` `item` `download` `store` `assets` |
| **1point3acres** | 🌐 | `hot` `latest` `digest` `search` `thread` `forum` `forums` `user` `notifications` |
| **36kr** | 🌐 | `hot` `news` `search` `article` |
| **51job** | 🌐 | `search` `hot` `detail` `company` |
| **aibase** | 🌐 | `news` |
| **amazon** | 🌐 | `bestsellers` `search` `product` `offer` `discussion` `movers-shakers` `new-releases` |
| **apple-podcasts** | ✅ | `top` `search` `episodes` |
| **arxiv** | ✅ | `search` `paper` `recent` `author` |
| **baidu-scholar** | 🌐 | `search` |
| **band** | 🌐 | `bands` `posts` `post` `mentions` |
| **barchart** | 🌐 | `quote` `options` `greeks` `flow` |
| **bbc** | ✅ | `news` `topic` |
| **bilibili** | 🌐 | `hot` `search` `me` `favorite` `history` `feed` `feed-detail` `user-videos` `subtitle` `dynamic` `ranking` `following` `comments` `download` `video` |
| **binance** | ✅ | `top` `ticker` `price` `klines` `gainers` `losers` `pairs` `trades` `depth` `asks` `prices` |
| **bloomberg** | ✅🌐 | RSS: `main` `markets` `tech` `politics` `economics` `opinions` · Browser: `news` |
| **bluesky** | 🌐 | `search` `profile` `user` `feeds` `followers` `following` `thread` `trending` `starter-packs` |
| **boss** | 🌐 | `search` `detail` `recommend` `joblist` `greet` `batchgreet` `send` `chatlist` `chatmsg` `invite` `mark` `exchange` `resume` `stats` |
| **brave** | 🌐 | `search` |
| **chaoxing** | 🌐 | `assignments` `exams` |
| **claude** | 🌐 | `status` `new` `send` `read` `ask` `detail` `history` |
| **cnki** | 🌐 | `search` |
| **coingecko** | ✅ | `top` `coin` `trending` `categories` `exchanges` `derivatives` `global` |
| **coupang** | 🌐 | `search` `add-to-cart` |
| **crates** | ✅ | `search` `crate` |
| **ctrip** | 🌐 | `search` `hotel-search` `hotel-suggest` `flight` |
| **dblp** | ✅ | `search` `author` `paper` `venue` |
| **deepseek** | 🌐 | `status` `new` `send` `read` `ask` `detail` `history` |
| **defillama** | ✅ | `protocols` `protocol` |
| **devto** | ✅ | `top` `tag` `user` |
| **dianping** | 🌐 | `search` `shop` |
| **dictionary** | ✅ | `search` `synonyms` `examples` |
| **dockerhub** | ✅ | `search` `image` |
| **doubao** | 🌐 | `status` `new` `send` `read` `ask` `detail` `history` `meeting-summary` `meeting-transcript` |
| **douban** | 🌐 | `search` `top250` `subject` `photos` `download` `marks` `reviews` `movie-hot` `book-hot` |
| **douyin** | 🌐 | `profile` `videos` `user-videos` `activities` `collections` `hashtag` `location` `stats` `publish` `draft` `drafts` `delete` `update` |
| **duckduckgo** | 🌐 | `search` `suggest` |
| **eastmoney** | 🌐 | `quote` `kline` `hot-rank` `rank` `index-board` `sectors` `northbound` `money-flow` `kuaixun` `announcement` `etf` `convertible` `holders` `longhu` |
| **endoflife** | ✅ | `product` |
| **facebook** | 🌐 | `feed` `profile` `search` `friends` `groups` `events` `notifications` `memories` `add-friend` `join-group` `marketplace-inbox` `marketplace-listings` |
| **flathub** | ✅ | `search` `app` |
| **gemini** | 🌐 | `ask` `new` `image` `deep-research` `deep-research-result` |
| **gitee** | 🌐 | `search` `trending` `user` |
| **google** | ✅ | `news` `search` `suggest` `trends` |
| **google-scholar** | 🌐 | `search` `profile` `cite` |
| **goproxy** | ✅ | `module` `versions` |
| **gov-law** | 🌐 | `search` `recent` |
| **gov-policy** | 🌐 | `search` `recent` |
| **grok** | 🌐 | `ask` |
| **hackernews** | ✅ | `top` `new` `best` `ask` `show` `jobs` `search` `user` |
| **hf** | ✅ | `top` |
| **homebrew** | ✅ | `popular` `formula` `cask` |
| **hupu** | 🌐 | `hot` `search` `detail` `like` `unlike` `reply` `mentions` |
| **imdb** | ✅ | `top` `trending` `search` `title` `person` `reviews` |
| **indeed** | 🌐 | `search` `job` |
| **instagram** | 🌐 | `explore` `profile` `search` `user` `followers` `following` `follow` `unfollow` `like` `unlike` `comment` `save` `unsave` `saved` `reel` `story` `post` `note` `download` `collection-create` `collection-delete` |
| **jd** | 🌐 | `item` |
| **jike** | 🌐 | `feed` `search` `create` `like` `comment` `repost` `notifications` `post` `topic` `user` |
| **jimeng** | 🌐 | `generate` `history` |
| **ke** | 🌐 | `ershoufang` `chengjiao` `xiaoqu` `zufang` |
| **lesswrong** | ✅ | `frontpage` `curated` `new` `top` `top-week` `top-month` `top-year` `shortform` `read` `comments` `user` `user-posts` `sequences` `tags` `tag` |
| **lichess** | ✅ | `top` `user` |
| **linkedin** | 🌐 | `search` `timeline` |
| **linux-do** | 🌐 | `hot` `latest` `feed` `search` `categories` `category` `tags` `topic` `topic-content` `user-posts` `user-topics` |
| **lobsters** | ✅ | `hot` `newest` `active` `tag` |
| **maimai** | 🌐 | `search-talents` |
| **maven** | ✅ | `search` `artifact` |
| **mdn** | ✅ | `search` |
| **medium** | 🌐 | `feed` `search` `user` |
| **mubu** | 🌐 | `search` `recent` `doc` `docs` `notes` |
| **notebooklm** | 🌐 | `status` `list` `open` `current` `get` `history` `summary` `note-list` `notes-get` `source-list` `source-get` `source-fulltext` `source-guide` |
| **nowcoder** | 🌐 | `hot` `trending` `search` `suggest` `jobs` `companies` `experience` `salary` `practice` `papers` `topics` `creators` `detail` `notifications` `recommend` `referral` |
| **npm** | ✅ | `search` `package` `downloads` |
| **nuget** | ✅ | `search` `package` |
| **nvd** | ✅ | `cve` |
| **oeis** | ✅ | `search` `sequence` |
| **ones** | 🌐 | `login` `logout` `me` `tasks` `task` `my-tasks` `worklog` `token-info` |
| **openalex** | ✅ | `search` `work` |
| **openfda** | ✅ | `drug-label` `food-recall` |
| **openreview** | ✅ | `search` `paper` `author` `venue` `reviews` |
| **osv** | ✅ | `vulnerability` `query` |
| **packagist** | ✅ | `search` `package` |
| **pixiv** | 🌐 | `ranking` `search` `user` `illusts` `detail` `download` |
| **powerchina** | 🌐 | `search` |
| **producthunt** | ✅ | `today` `hot` `browse` `posts` |
| **pubmed** | ✅ | `search` `article` `author` `citations` `related` |
| **pypi** | ✅ | `package` `downloads` |
| **quark** | 🌐 | `ls` `mkdir` `mv` `rename` `rm` `save` `share-tree` |
| **qwen** | 🌐 | `status` `new` `send` `read` `ask` `image` `detail` `history` |
| **reddit** | 🌐 | `hot` `frontpage` `home` `popular` `search` `subreddit` `subreddit-info` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `reply` `subscribe` `saved` `upvoted` `whoami` |
| **rednote** | 🌐 | `search` `feed` `user` `note` `comments` `download` `notifications` |
| **rest-countries** | ✅ | `country` `region` |
| **reuters** | 🌐 | `search` |
| **rfc** | ✅ | `rfc` |
| **rubygems** | ✅ | `search` `gem` |
| **sinablog** | 🌐 | `hot` `search` `article` `user` |
| **sinafinance** | ✅ | `news` |
| **smzdm** | 🌐 | `search` |
| **spotify** | ✅ | `auth` `status` `play` `pause` `next` `prev` `volume` `search` `queue` `shuffle` `repeat` |
| **stackoverflow** | ✅ | `hot` `search` `bounties` `unanswered` |
| **steam** | ✅ | `top-sellers` |
| **substack** | 🌐 | `feed` `search` `publication` |
| **taobao** | 🌐 | `search` `detail` `reviews` `add-cart` `cart` |
| **tdx** | 🌐 | `hot-rank` |
| **ths** | 🌐 | `hot-rank` |
| **tieba** | 🌐 | `hot` `search` `posts` `read` |
| **tiktok** | 🌐 | `explore` `search` `profile` `user` `following` `follow` `unfollow` `like` `unlike` `comment` `save` `unsave` `live` `notifications` `friends` |
| **toutiao** | 🌐 | `hot` `articles` |
| **tvmaze** | ✅ | `search` `show` |
| **twitter** | 🌐 | `trending` `bookmarks` `search` `profile` `timeline` `thread` `article` `follow` `unfollow` `bookmark` `unbookmark` `bookmark-folders` `post` `like` `likes` `reply` `quote` `retweet` `unretweet` `delete` `block` `unblock` `followers` `following` `notifications` `download` `lists` `list-tweets` `list-add` `list-remove` `tweets` `unlike` `spaces` |
| **uisdc** | 🌐 | `news` |
| **uiverse** | 🌐 | `code` `preview` |
| **v2ex** | ✅🌐 | Public: `hot` `latest` `topic` `node` `nodes` `member` `user` `replies` · Browser: `daily` `me` `notifications` |
| **wanfang** | 🌐 | `search` |
| **web** | 🌐 | `read` — any URL to Markdown |
| **weibo** | 🌐 | `hot` `search` `feed` `user` `me` `post` `comments` |
| **wikidata** | ✅ | `search` `entity` |
| **wikipedia** | ✅ | `search` `summary` `random` `trending` |
| **weixin** | 🌐 | `download` — 公众号 article to Markdown |
| **weread** | 🌐 | `shelf` `search` `book` `highlights` `notes` `notebooks` `ranking` |
| **wttr** | ✅ | `current` `forecast` |
| **xianyu** | 🌐 | `search` `item` `chat` |
| **xiaoe** | 🌐 | `courses` `catalog` `content` `detail` `play-url` |
| **xiaohongshu** | 🌐 | `search` `notifications` `feed` `user` `note` `comments` `download` `publish` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` |
| **xiaoyuzhou** | ✅ | `podcast` `podcast-episodes` `episode` |
| **xueqiu** | 🌐 | `hot-stock` `stock` `watchlist` `feed` `hot` `search` `comments` `earnings-date` `fund-holdings` `fund-snapshot` |
| **yahoo** | 🌐 | `search` |
| **yahoo-finance** | 🌐 | `quote` |
| **yollomi** | 🌐 | `models` `generate` `video` `upload` `remove-bg` `edit` `background` `face-swap` `object-remover` `restore` `try-on` `upscale` |
| **youtube** | 🌐 | `search` `video` `transcript` `feed` `history` `channel` `playlist` `comments` `like` `unlike` `subscribe` `unsubscribe` `subscriptions` `watch-later` |
| **yuanbao** | 🌐 | `new` `ask` |
| **zhihu** | 🌐 | `hot` `search` `question` |
| **zlibrary** | 🌐 | `search` `info` |
| **zsxq** | 🌐 | `groups` `dynamics` `topics` `topic` `search` |

### Desktop Apps (CDP/Electron)

| App | Commands |
|-----|----------|
| **antigravity** | `status` `send` `read` `new` `dump` `extract-code` `model` `watch` |
| **chatgpt-app** | `status` `new` `send` `read` `ask` `model` |
| **chatwise** | `status` `new` `send` `read` `ask` `model` `history` `export` `screenshot` |
| **codex** | `status` `send` `read` `new` `dump` `extract-diff` `model` `ask` `screenshot` `history` `export` |
| **cursor** | `status` `send` `read` `new` `dump` `composer` `model` `extract-code` `ask` `screenshot` `history` `export` |
| **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` |
| **doubao-app** | `status` `new` `send` `read` `ask` `screenshot` `dump` |
| **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` |

### External CLI (passthrough)

| CLI | Installed | Description |
|-----|-----------|-------------|
| **gh** | ✅ | GitHub CLI — repos, PRs, issues, releases |
| **obsidian** | ✅ | Obsidian vault — notes, search, tags |
| **docker** | ✅ | Docker CLI |
| **lark-cli** | ✅ | Lark/Feishu — messages, docs, calendar, tasks (200+ commands) |
| **vercel** | ✅ | Vercel — deploy, domains, env vars, logs |
| **wx** | ✅ | WeChat local data CLI — sessions, messages, search, export |
| **discord** | ❌ | Discord CLI |
| **dws** | ❌ | DingTalk Workspace |
| **longbridge** | ❌ | Longbridge CLI — market data, account, trading |
| **ntn** | ❌ | Notion CLI |
| **tg** | ❌ | Telegram CLI |
| **wecom-cli** | ❌ | WeCom/企业微信 |

```bash
opencli external install discord    # Install external CLI
opencli external list               # Show all external CLIs
opencli gh pr list --limit 5       # Passthrough to gh
opencli wx messages                 # Passthrough to wx-cli
```

### Management Commands

```bash
opencli list [-f json|yaml]         # All available commands
opencli doctor                      # Diagnose extension + daemon
opencli daemon status|restart|stop  # Daemon control
opencli adapter eject [site]        # Eject adapter to ~/.opencli/clis/ for local editing
opencli adapter reset [site]        # Reset ejected adapter to upstream
opencli profile list|use|rename     # Chrome profile management
opencli plugin list|install|update  # Plugin management
```

---

## Browser Automation

Control Chrome step-by-step. Reuses existing login sessions.

### Critical Rules

1. **Always `state` first** — never guess element indices. `state` returns structured DOM with `[N]` indices, is instant and costs zero tokens.
2. **Never use `eval` to click or type** — use `click <N>` and `type <N> "text"` instead.
3. **Verify inputs with `get value`** — after `type`, run `get value <index>` to confirm.
4. **Run `state` after every page change** — after `open`, `click` (on links), `scroll`.
5. **Chain commands aggressively** — `open + state`, `type + type + click` in one `&&` chain. Target 3-5 tool calls total.
6. **`eval` is read-only** — use ONLY for data extraction (`JSON.stringify(...)`), never for clicking/typing/navigating.
7. **Prefer `network` to discover APIs** — JSON APIs are more reliable than DOM scraping.

### Command Cost Guide

| Cost | Commands |
|------|----------|
| Free & instant | `state`, `get *`, `eval`, `network`, `scroll`, `keys` |
| Free but changes page | `open`, `click`, `type`, `select`, `back` |
| Expensive (vision) | `screenshot` — ONLY when user needs a saved image |

### Commands

```bash
# Navigation
opencli browser open <url>
opencli browser back
opencli browser scroll down|up [--amount N]

# Inspect (free)
opencli browser state                    # Primary — structured DOM with [N] indices
opencli browser screenshot [path.png]    # Only for user deliverables

# Get (free)
opencli browser get title|url|html
opencli browser get text|value|attributes <index>

# Interact
opencli browser click <index>
opencli browser type <index> "text"
opencli browser select <index> "option"
opencli browser keys "Enter"             # Enter, Escape, Tab, Control+a

# Wait
opencli browser wait time 3
opencli browser wait selector ".loaded" [--timeout 5000]
opencli browser wait text "Success"

# Extract (read-only)
opencli browser eval "JSON.stringify([...document.querySelectorAll('h2')].map(e => e.textContent))"

# Network (API discovery)
opencli browser network
opencli browser network --detail 3      # Full response body of request #3

# Save as CLI
opencli browser init hn/top             # Scaffold adapter at ~/.opencli/clis/hn/top.ts
opencli browser verify hn/top           # Test the adapter

opencli browser close
```

### Chaining Examples

```bash
# GOOD: open + inspect in one call
opencli browser open https://example.com && opencli browser state

# GOOD: fill form in one call
opencli browser type 3 "hello" && opencli browser type 4 "world" && opencli browser click 7

# GOOD: click + wait + state (for page-changing clicks)
opencli browser click 12 && opencli browser wait time 1 && opencli browser state
```

### Troubleshooting

| Error | Fix |
|-------|-----|
| "Browser not connected" | Run `opencli doctor` |
| "attach failed: chrome-extension://" | Disable 1Password temporarily |
| Element not found | `opencli browser scroll down && opencli browser state` |
| Stale indices after page change | Run `opencli browser state` again |

---

## Auto-Fix Broken Adapters

When an `opencli` command fails because a website changed its DOM/API, **automatically diagnose, fix, and retry** — don't just report the error.

### Safety Boundaries

- **`AUTH_REQUIRED`** (exit 77) — STOP. Tell user to log in. Do not modify code.
- **`BROWSER_CONNECT`** (exit 69) — STOP. Tell user to run `opencli doctor`.
- **CAPTCHA / rate limiting** — STOP. Not an adapter issue.
- **Only modify** the file at `RepairContext.adapter.sourcePath`. Never touch `src/`, `extension/`, `tests/`, `package.json`.
- **Max 3 repair rounds** per failure.

### Before Repairing: "Empty" ≠ "Broken"

`EMPTY_RESULT` is often NOT a bug — platforms degrade results under anti-scrape heuristics. Before patching:
- Retry with an alternative query (e.g., `"AI 攻略"` vs `"AI"`)
- Spot-check in a normal Chrome tab — if visible there but empty via adapter, it's auth/rate-limit
- `0 results` from a search is a valid answer, not a bug

### Repair Workflow

**Step 1: Collect diagnostics**
```bash
OPENCLI_DIAGNOSTIC=1 opencli <site> <command> [args...] 2>diagnostic.json
cat diagnostic.json | sed -n '/___OPENCLI_DIAGNOSTIC___/{n;p;}'
```

Output is a `RepairContext` JSON with: `error.code`, `adapter.sourcePath`, `adapter.source`, `page.snapshot`, `page.networkRequests`.

**Step 2: Classify the failure**

| Error Code | Likely Cause | Strategy |
|-----------|-------------|----------|
| SELECTOR | DOM restructured | Explore current DOM → find new selector |
| EMPTY_RESULT | API response schema changed | Check network → find new response path |
| API_ERROR | Endpoint URL changed | Discover new API via network intercept |
| AUTH_REQUIRED | Cookies expired | **STOP** — tell user to log in |
| TIMEOUT | Page loads differently | Add/update wait conditions |

**Step 3: Explore live site**
```bash
# DOM changed
opencli browser open https://example.com/target-page && opencli browser state

# API changed
opencli browser open https://example.com && opencli browser network
opencli browser network --detail <index>
```

**Step 4: Patch the adapter**

Read `RepairContext.adapter.sourcePath` and make minimal targeted fixes:
- Selector update: replace `.old-class` with `.new-class`
- API endpoint: update URL in `fetch()`
- Response schema: fix data path (`data.results` → `data.data.items`)

**Step 5: Verify**
```bash
opencli <site> <command> [args...]    # Run without diagnostic mode
```

If still failing, go back to Step 1. After 3 rounds, stop and report what was tried.

---

## Create New Adapters

### Quick Method (4 steps — for a single URL + goal)

1. **Open page + capture API**
   ```bash
   # Navigate and wait 3-5s for page to load
   # Check network for JSON APIs
   opencli browser open <url> && opencli browser wait time 3 && opencli browser network
   ```

2. **Lock down the target API** — find the one returning `application/json` with the data you need

3. **Verify the API is reusable**
   ```bash
   # Test in browser context
   fetch('/api/endpoint', { credentials: 'include' }).then(r => r.json())
   ```

4. **Write adapter from template** (see templates below)

**Auth decision tree:**
```
fetch(url) works?                          → Tier 1: PUBLIC (browser: false)
fetch(url, {credentials:'include'}) works? → Tier 2: COOKIE (most common)
Needs Bearer/CSRF header?                  → Tier 3: HEADER
Page requests it but fetch can't?          → Tier 4: INTERCEPT (installInterceptor)
```

### Adapter Templates

**Tier 1 — Public API (fastest)**
```typescript
// ~/.opencli/clis/<site>/<name>.ts
import { cli, Strategy } from '@jackwener/opencli/registry';

cli({
  site: 'mysite', name: 'hot', description: '一句话描述',
  domain: 'www.example.com',
  strategy: Strategy.PUBLIC, browser: false,
  args: [{ name: 'limit', type: 'int', default: 20 }],
  columns: ['rank', 'title', 'score'],
  func: async (_page, kwargs) => {
    const res = await fetch('https://api.example.com/hot');
    const data = await res.json();
    return data.items.slice(0, kwargs.limit).map((item: any, i: number) => ({
      rank: i + 1, title: item.title, score: item.score,
    }));
  },
});
```

**Tier 2 — Cookie auth (most common)**
```typescript
import { cli, Strategy } from '@jackwener/opencli/registry';

cli({
  site: 'mysite', name: 'feed', description: '一句话描述',
  domain: 'www.example.com',
  strategy: Strategy.COOKIE, browser: true,
  args: [{ name: 'limit', type: 'int', default: 20 }],
  columns: ['rank', 'title', 'value'],
  func: async (page, kwargs) => {
    await page.goto('https://www.example.com');
    const data = await page.evaluate(`(async () => {
      const res = await fetch('/api/feed', { credentials: 'include' });
      const d = await res.json();
      return (d.data?.items || []).map(item => ({ title: item.title, value: item.value }));
    })()`);
    return (data as any[]).slice(0, kwargs.limit).map((item, i) => ({
      rank: i + 1, title: item.title || '', value: item.value || '',
    }));
  },
});
```

**Tier 4 — Intercept (for sites with signing/anti-scrape)**
```typescript
import { cli, Strategy } from '@jackwener/opencli/registry';

cli({
  site: 'mysite', name: 'feed', description: '一句话描述',
  domain: 'www.example.com',
  strategy: Strategy.INTERCEPT, browser: true,
  args: [{ name: 'limit', type: 'int', default: 20 }],
  columns: ['rank', 'title', 'value'],
  func: async (page, kwargs) => {
    await page.goto('https://www.example.com/target');
    await page.wait(3);
    await page.installInterceptor('api-keyword');       // URL substring match
    await page.autoScroll({ times: 2, delayMs: 2000 }); // Trigger lazy load
    const requests = await page.getInterceptedRequests();
    if (!requests?.length) return [];
    const results: any[] = [];
    for (const req of requests) {
      results.push(...(req.data?.data?.items || []));
    }
    return results.slice(0, kwargs.limit).map((item, i) => ({
      rank: i + 1, title: item.title || '', value: item.value || '',
    }));
  },
});
```

### Test After Writing

```bash
npm run build                              # Syntax check
opencli list | grep mysite                 # Confirm registered
opencli mysite mycommand --limit 3 -v      # Verify actual output
```

### Full Exploration Guide (for new sites)

When you need to explore a site from scratch rather than a single URL:

**Step 1: API Discovery**
```bash
opencli explore https://www.example.com --site mysite
```
Outputs to `.opencli/explore/mysite/`: `endpoints.json`, `capabilities.json`, `auth.json`

Or auto-generate everything:
```bash
opencli generate https://www.example.com --goal "hot"
```

**Key discovery tactics:**
- **`.json` suffix trick** — Reddit-style sites: just append `.json` to URL for clean REST data
- **`__INITIAL_STATE__`** — SSR sites (Bilibili, Xiaohongshu) embed data in `window.__INITIAL_STATE__`
- **Active interaction** — Lazy-load APIs (comments, subtitles) only appear after clicking a button
- **Pinia/Vuex intercept** — Vue sites: call store actions to trigger signed requests without reverse-engineering signatures

**Step 2: Pick auth tier** — run `opencli cascade <api-url>` to auto-detect

**Step 3: Find existing adapter to copy** — `ls clis/<site>/`, copy the closest one, change 3 fields: `name`, API URL, field mapping

**Step 4: Test**
```bash
opencli mysite hot --limit 3 -v      # verbose: see pipeline data flow
opencli mysite hot -f json | jq '.[0]'  # confirm JSON structure
```

**Common pitfalls:**

| Pitfall | Fix |
|---------|-----|
| Missing `navigate` before `evaluate` | Add `page.goto()` before evaluate |
| Public API starts browser anyway | Add `strategy: Strategy.PUBLIC, browser: false` |
| `evaluate` returns empty on SPA | Add `wait selector` or `wait time` before evaluate |
| Cookie expired | Re-login in Chrome |
| `EMPTY_RESULT` after intercept | Check if `installInterceptor` keyword matches actual URL |

