CLI-Anything-Web Methodology (Phase 2)
Analyze captured traffic, design the CLI command structure, and implement the
complete Python CLI package. This skill owns the core transformation from raw
HTTP traffic to a production-ready CLI.
Prerequisites (Hard Gate)
Do NOT start unless:
If raw-traffic.json is missing or has no WRITE operations, invoke the
capture skill first.
Exception for read-only sites: If the site is genuinely read-only (search engine,
dashboard, analytics viewer with no create/update/delete), the trace may contain only
GET requests. In this case, note "read-only site — no write operations" in <APP>.md
and proceed. The generated CLI will have read-only commands (list, get, search) but
no create/update/delete commands. This is valid.
No-auth sites: If the target site requires no authentication (public API,
no login needed), the "Auth state captured" prerequisite does not apply. Note
"no-auth site" in <APP>.md and proceed.
Step A: Analyze (API Discovery)
Goal: Map raw traffic to a structured API model.
Process:
Read traffic-analysis.json first (if it exists alongside raw-traffic.json).
This file is auto-generated by parse-trace.py or mitmproxy-capture.py → analyze-traffic.py and contains
pre-detected protocol type, auth pattern, endpoint grouping, GraphQL operations,
batchexecute RPC IDs, and suggested CLI commands. Use it as a starting point —
verify its findings and fill in anything marked "unknown" by reading raw-traffic.json
manually.
Enhanced analysis (v1.3.0, when captured via mitmproxy-capture.py):
request_sequence: Timeline-ordered requests with auth flow detection (login → token → API calls)
session_lifecycle: Cookie inventory, auth cookie identification, session pattern (cookie_auth/token_refresh/no_session)
endpoint_sizes: Response body size classification per endpoint (small/medium/large) and total data transferred
These fields are only present when mitmproxy-capture.py was used. If missing (has_timestamps: false), rely on manual analysis.
If traffic-analysis.json doesn't exist, run the analyzer:
python ${CLAUDE_PLUGIN_ROOT}/scripts/analyze-traffic.py \
<app>/traffic-capture/raw-traffic.json --summary
Parse raw-traffic.json (for details the analyzer couldn't extract)
Group requests by base path (e.g., /api/v1/boards/, /api/v1/items/)
For each endpoint group, identify:
- HTTP method (GET/POST/PUT/DELETE/PATCH)
- URL pattern (extract path parameters like
:id)
- Query parameters and their types
- Request body schema (JSON fields, types, required/optional)
- Response body schema
- Authentication method (Bearer token, cookie, API key)
- Rate limiting signals (429 responses, retry-after headers)
Identify RPC protocol type -- classify the API transport:
| Protocol |
Detection Signal |
Client Pattern |
| REST |
Resource URLs (/api/v1/boards/:id), standard HTTP methods |
client.py with method-per-endpoint |
| GraphQL |
Single /graphql endpoint, query/mutation in body |
client.py with query templates |
| gRPC-Web |
application/grpc-web content type, binary payloads |
Proto-based client |
| Google batchexecute |
batchexecute in URL, f.req= body, )]}'\n prefix |
rpc/ subpackage (see references/google-batchexecute.md) |
| Custom RPC |
Single endpoint, method name in body, proprietary encoding |
Custom codec module |
| Public REST API |
Documented /api/ endpoints, OpenAPI spec, JSON responses |
Standard client.py with httpx |
| Plain HTML (no framework) |
No SPA root, no framework globals, data in <table>/<div> |
client.py with httpx + BeautifulSoup4 |
This determines client architecture in Step B -- REST uses simple client.py,
non-REST protocols need a dedicated rpc/ subpackage with encoder/decoder/types.
Detect data model:
- Entity types (boards, items, users, projects...)
- Relationships (board has many items, item belongs to board)
- ID formats (UUID, numeric, slug)
Detect auth pattern:
- Cookie-based sessions
- Bearer/JWT tokens
- OAuth refresh flow
- API key headers
- Browser-delegated auth: tokens embedded in page JavaScript (e.g.,
WIZ_global_data),
not in HTTP headers. Requires CDP for initial cookies, HTTP for token extraction.
See references/auth-strategies.md "Browser-Delegated Auth" section.
- No auth / public access: fully public API, no login required. CLI may
optionally support API key auth for write operations (e.g., dev.to).
Write <APP>.md -- software-specific SOP document
Output: <APP>.md with API map, data model, auth scheme.
References: traffic-patterns.md, google-batchexecute.md, ssr-patterns.md
Step B: Implement (Code Generation)
Study Existing CLIs First (Critical for Accuracy)
Before implementing, read an existing CLI that uses the same protocol as your
target. These are battle-tested implementations that solved the same problems you'll face.
| Protocol |
Reference CLI |
Key files to read |
| Google batchexecute |
notebooklm/agent-harness/cli_web/notebooklm/ |
core/rpc/encoder.py, core/rpc/decoder.py, core/client.py, core/auth.py |
| GraphQL + WAF |
booking/agent-harness/cli_web/booking/ |
core/client.py (curl_cffi + GraphQL), core/auth.py (WAF tokens) |
| HTML scraping |
futbin/agent-harness/cli_web/futbin/ |
core/client.py (httpx + BS4), commands/players.py |
| HTML + Cloudflare |
producthunt/agent-harness/cli_web/producthunt/ |
core/client.py (curl_cffi impersonate) |
| REST API |
unsplash/agent-harness/cli_web/unsplash/ |
core/client.py, commands/photos.py |
| Simple HTML |
gh-trending/agent-harness/cli_web/gh_trending/ |
Minimal structure example |
How to use reference CLIs:
- Read the reference CLI's
core/client.py — understand the request/response pattern
- Read
core/auth.py — copy the login_browser() pattern exactly for Google apps
- Read
core/rpc/ (for batchexecute) — understand encoder/decoder, DO NOT reinvent
- Read
commands/ — see how Click commands are structured, how --json works
- Read
utils/helpers.py — see handle_errors(), _resolve_cli(), repl patterns
For batchexecute apps specifically, the notebooklm CLI is your bible:
- Copy the encoder/decoder architecture (don't reinvent the batchexecute wire format)
- Copy the auth token extraction pattern (CSRF, session ID, build label)
- Copy the cookie domain priority logic (critical for Israeli/international users)
- Adapt the RPC method IDs and param structures to your target app
The agent implementing the CLI MUST read these files before writing code. Use the
Agent tool to dispatch a research agent that reads
the reference implementation while you design the command structure.
Design Before You Code
Before writing any code, note the command structure in <APP>.md (10 minutes max):
- Map each API endpoint group to a Click command group:
/api/v1/boards/* → boards command group
/api/v1/items/* → items command group
- Map CRUD operations to subcommands (GET list →
list, GET single → get,
POST → create, PUT/PATCH → update, DELETE → delete)
- Note auth design:
auth login, auth status, auth refresh; credentials at
~/.config/cli-web-<app>/auth.json
- Note REPL design: bare command enters REPL, branded banner via
repl_skin.py
Goal: Generate the complete Python CLI package.
Package Structure
See HARNESS.md "Generated CLI Structure" for the complete package template.
Key points: cli_web/ namespace (NO __init__.py), <app>/ sub-package (HAS __init__.py),
core/, commands/, utils/, tests/ directories.
Step B.0: Scaffold Core Modules
Run the scaffold generator script to create all boilerplate files:
python ${CLAUDE_PLUGIN_ROOT}/scripts/scaffold-cli.py <app>/agent-harness \
--app-name <app> \
--protocol <rest|graphql|html-scraping|batchexecute> \
--http-client <httpx|curl_cffi> \
--auth-type <none|cookie|api-key|google-sso> \
--resources <comma-separated-resources> \
[--has-polling] [--has-context] [--has-partial-ids]
This generates exceptions.py, client.py skeleton, helpers.py, config.py, output.py,
the CLI entry point with REPL, setup.py, conftest.py, repl_skin.py, and (for
batchexecute) the rpc/ subpackage.
Fallback: If the script is unavailable, read ${CLAUDE_PLUGIN_ROOT}/skills/boilerplate/SKILL.md
and follow its instructions to scaffold manually.
After scaffolding, review the generated files and customize client.py with actual
endpoint methods from <APP>.md.
Implementation Rules
exceptions.py -- implement first. Required types: AppError (base), AuthError(recoverable), RateLimitError(retry_after), NetworkError, ServerError(status_code), NotFoundError. See references/exception-hierarchy-example.py for the complete template.
client.py -- HTTP client with exception mapping and auth retry:
- HTTP library choice:
- Centralized auth header/cookie injection
- Automatic JSON parsing with response body verification
- Status code → exception mapping: 401/403→
AuthError, 404→NotFoundError, 429→RateLimitError, 5xx→ServerError
- Auth retry (3-attempt auto-refresh): On 401/403: attempt 0 = try current cookies, attempt 1 = reload from
auth.json on disk, attempt 2 = headless browser refresh via refresh_auth() in auth.py. See HARNESS.md "Token Auto-Refresh" for the full pattern. The auth.py.tpl and client_rest_*.py.tpl templates generate this by default.
- Exponential backoff for rate limits (see
references/polling-backoff-example.py)
- For apps with 3+ resource types: split into namespaced sub-clients (
client.notebooks.list(), client.sources.add())
- See
references/client-architecture-example.py for the full pattern
auth.py -- handles token storage, refresh, expiry. Implementation depends on auth type:
For no-auth sites: DO NOT create auth.py, session.py, or auth command groups.
These files are dead code for public APIs and confuse users. The CLI should have
NO auth-related files or commands. The only exception is if the site has optional
auth (e.g., API key for write operations) — in that case, implement a minimal
auth module.
For browser-delegated auth (Google, Microsoft, etc.): Full playwright-cli login flow
with cookie domain priority for international users.
See references/auth-strategies.md for all patterns (browser login, cookie priority, API key, env var, context commands).
Store cookies at ~/.config/cli-web-<app>/auth.json with chmod 600.
Anti-bot resilient client construction (when detected in Phase 2):
- Extract session tokens via CDP first (cookies), then HTTP GET + HTML parsing (CSRF, session IDs)
- Never hardcode build labels (
bl), session IDs (f.sid), or CSRF tokens -- extract dynamically at runtime
- Replicate same-origin headers captured during Phase 1 traffic (e.g.,
x-same-domain: 1 for Google apps)
- Implement auto-retry on 401/403: re-fetch homepage -> re-extract tokens -> retry once
- See
references/google-batchexecute.md for the complete Google pattern
RPC codec subpackage (for non-REST protocols like batchexecute):
When the API uses a non-REST protocol, add core/rpc/ with:
types.py -- method ID enum, URL constants
encoder.py -- request encoding (protocol-specific format)
decoder.py -- response decoding (strip prefix, parse chunks, extract results)
The client.py still exists but delegates encoding/decoding to rpc/.
Progress feedback -- Use rich>=13.0 spinners for operations >2s (suppress in --json mode). See references/rich-output-example.py.
JSON error output -- --json mode errors are JSON too, not plain text. Standard codes: AUTH_EXPIRED, RATE_LIMITED, NOT_FOUND, SERVER_ERROR, NETWORK_ERROR. Implement via utils/output.py json_error().
All commands use handle_errors(json_mode) context manager — centralizes error handling, exit codes (1=user, 2=system, 130=interrupt), and JSON errors. See references/helpers-module-example.py.
Generation commands support --wait, --retry N, --output path — for agent-scriptable end-to-end workflows. See references/polling-backoff-example.py.
Windows UTF-8 fix — Add at the top of <app>_cli.py before any imports that print:
import sys
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
try: sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except AttributeError: pass
HTML table parsers MUST extract ALL visible columns — not just name/price,
because missing fields in --json output make the CLI useless for filtering and analysis.
If the site shows version, club, nation, stats, skills, weak foot — parse all of them.
Empty fields in --json output = incomplete parser.
Entry point: cli-web-<app> via setup.py console_scripts
Namespace: cli_web.*
Copy repl_skin.py from plugin for consistent REPL experience
utils/helpers.py -- shared CLI helpers (generate for every CLI):
resolve_partial_id(partial, items) — prefix-match UUIDs for get/rename/delete
handle_errors(json_mode) — context manager replacing try/except in all commands
require_notebook(notebook_arg) — gets notebook ID from arg or persistent context
sanitize_filename(name) — safe filenames from artifact titles
poll_until_complete(check_fn) — exponential backoff polling
get_context_value(key) / set_context_value(key, value) — persistent context.json
See references/helpers-module-example.py for the complete module.
Not all helpers apply to every CLI. Include only what the CLI uses:
handle_errors and print_json are always needed. resolve_partial_id only
for UUID-based apps. require_notebook/context helpers only for apps with
persistent context. poll_until_complete only for generation/async operations.
REPL Implementation Rules (Critical)
These three bugs appear in almost every generated REPL. Get them right the first time:
1. Use shlex.split(), never line.split()
# ✓ Correct — handles quoted args: players search "messi" -> ['players', 'search', 'messi']
import shlex
args = shlex.split(line)
# ✗ Wrong — produces: ['players', 'search', '"messi"'] — quotes become part of the value
args = line.split()
2. Never pass **ctx.params to cli.main() in REPL dispatch
# ✓ Correct — preserve --json flag by prepending to args
repl_args = ["--json"] + args if ctx.obj.get("json") else args
cli.main(args=repl_args, standalone_mode=False)
# ✗ Wrong — ctx.params = {"json_mode": False} gets passed to Context.__init__()
# which doesn't accept it → TypeError: Context.__init__() got an unexpected
# keyword argument 'json_mode'
cli.main(args=args, standalone_mode=False, **ctx.params)
3. Keep _print_repl_help() in sync with the actual command surface
The _print_repl_help() function in <app>_cli.py is the user's first discovery surface — it's what they see when they type help in the REPL. It must mirror the real commands, including all key options. A REPL that shows outdated or incomplete help is confusing and makes the CLI feel broken.
# ✓ Correct — help lists actual options users can pass
def _print_repl_help():
_skin.info("Available commands:")
print(" players list [OPTIONS]")
print(" --position <GK|ST|CM|...> Filter by position")
print(" --rating-min N --rating-max N Rating range")
print(" --cheapest Sort cheapest first")
# ✗ Wrong — stale help doesn't mention new --position, --rating-min, etc.
def _print_repl_help():
print(" players list [--min-price N] List players with filters")
Rule: every time you add options to a command, update _print_repl_help() in the same commit.
4. Use @click.argument for positional REPL params, not @click.option("--x", required=True)
REPL commands show players search <query> in help. If query is a --query option,
users typing players search messi get "Error: Missing option '--query'".
Use positional arguments for natural command-line style:
# ✓ Correct — users type: players search messi OR players get 21610
@players.command()
@click.argument("query")
def search(query): ...
@players.command()
@click.argument("player_id", type=int)
def get(player_id): ...
# ✗ Wrong — users get an error unless they type: players search --query messi
@players.command()
@click.option("--query", required=True)
def search(query): ...
Rule of thumb: if a command takes a single required value that would be a positional arg
in a shell command (git checkout main, grep pattern), use @click.argument.
Use @click.option only for optional or named parameters (--rating-min, --platform).
Parallel Implementation (dispatch independent modules as subagents)
When the CLI has 3+ command groups (e.g., notebooks, sources, chat, artifacts),
dispatch parallel subagents -- one per command module. Each agent gets:
- The
<APP>.md API spec for its resource
- The
client.py and auth.py interfaces it depends on
- Clear scope: "Implement
commands/notebooks.py with list, get, create, delete"
Parallelization opportunities:
| Independent from each other |
Dispatch in parallel |
commands/notebooks.py, commands/sources.py, commands/chat.py |
Yes -- each command file only depends on client.py |
rpc/encoder.py and rpc/decoder.py |
Yes -- encoder doesn't depend on decoder |
auth.py and models.py |
Yes -- no shared logic |
client.py and commands/* |
No -- commands depend on client |
<app>_cli.py (entry point) |
Last -- imports all commands, write after they're done |
Implementation order (with maximum parallelism):
Phase A (sequential): Write core foundation
exceptions.py → client.py → auth.py (if needed) → models.py
Phase B (parallel): Dispatch ALL independent work simultaneously
┌─ Agent 1: commands/notebooks.py
├─ Agent 2: commands/sources.py
├─ Agent 3: commands/chat.py
├─ Agent 4: commands/artifacts.py
├─ Agent 5: rpc/encoder.py + rpc/decoder.py (if non-REST)
└─ Agent 6 (background): test_core.py (unit tests for core modules)
All run concurrently — each only depends on Phase A modules
Phase C (sequential): Wire everything together
utils/helpers.py → <app>_cli.py → __main__.py → setup.py → copy repl_skin.py
Key parallelism rules:
- Dispatch independent command modules as parallel subagents (one per
commands/*.py file)
- Start unit test writing as a background agent during command implementation
- Entry point (
<app>_cli.py, setup.py) must come last (depends on all commands)
Mandatory Smoke Check (Before Testing Phase)
Before invoking testing, install (pip install -e .) and verify:
cli-web-<app> --help loads
cli-web-<app> auth status --json shows valid (if auth-required)
cli-web-<app> <resource> list --json returns real data
- One WRITE command works (if applicable)
Red flags — fix before testing:
wrb.fr, af.httprm in output → decoder broken
[] or null where data expected → wrong params or client-side operation
- Wrong field values (e.g., "3" instead of prompt text) → parser index mismatch
- Null write response → may be client-side, see
references/google-batchexecute.md "Client-Side Operations"
Update phase state:
python ${CLAUDE_PLUGIN_ROOT}/scripts/phase-state.py complete <app> \
--phase methodology --output <app>/agent-harness/
Next Step
When implementation is complete and the smoke check passes, invoke the testing
skill to plan and write tests.
Do NOT skip testing -- every CLI must have comprehensive tests before publishing.
Companion Skills
| Skill |
When it activates |
capture |
Phase 1 -- traffic recording (prerequisite for this skill) |
testing |
Phase 3 -- test writing, documentation |
standards |
Phase 4 -- publish, verify, smoke test |
Integration
| Relationship |
Skill |
| Preceded by |
capture (Phase 1) |
| Followed by |
testing (Phase 3) |
| References |
traffic-patterns.md, auth-strategies.md, google-batchexecute.md, ssr-patterns.md, exception-hierarchy-example.py, client-architecture-example.py, polling-backoff-example.py, rich-output-example.py |
Reference Files
references/traffic-patterns.md -- Common API patterns (REST, GraphQL, RPC)
references/auth-strategies.md -- Auth implementation strategies
references/google-batchexecute.md -- Google batchexecute RPC protocol spec
references/ssr-patterns.md -- SSR framework patterns and data extraction strategies
references/exception-hierarchy-example.py -- Complete exception hierarchy with HTTP status mapping
references/client-architecture-example.py -- Namespaced sub-client pattern with auth retry
references/polling-backoff-example.py -- Exponential backoff polling and rate-limit retry
references/rich-output-example.py -- Rich progress bars, JSON error responses, table formatting
Source: ItamarZand88/CLI-Anything-WEB — distributed by TomeVault.
1---2name: itamarzand88-cli-anything-web-methodology3description: CLI-Anything-Web Methodology (Phase 2)4---56# CLI-Anything-Web Methodology (Phase 2)78Analyze captured traffic, design the CLI command structure, and implement the9complete Python CLI package. This skill owns the core transformation from raw10HTTP traffic to a production-ready CLI.1112---1314## Prerequisites (Hard Gate)1516Do NOT start unless:17- [ ] `raw-traffic.json` exists (with WRITE operations, or read-only GET-only traffic)18- [ ] Auth state was captured during Phase 1 (if the site requires auth)1920If raw-traffic.json is missing or has no WRITE operations, invoke the21`capture` skill first.2223**Exception for read-only sites:** If the site is genuinely read-only (search engine,24dashboard, analytics viewer with no create/update/delete), the trace may contain only25GET requests. In this case, note "read-only site — no write operations" in `<APP>.md`26and proceed. The generated CLI will have read-only commands (list, get, search) but27no create/update/delete commands. This is valid.2829**No-auth sites:** If the target site requires no authentication (public API,30no login needed), the "Auth state captured" prerequisite does not apply. Note31"no-auth site" in `<APP>.md` and proceed.3233---3435## Step A: Analyze (API Discovery)3637**Goal:** Map raw traffic to a structured API model.3839**Process:**40410. **Read `traffic-analysis.json` first** (if it exists alongside `raw-traffic.json`).42 This file is auto-generated by `parse-trace.py` or `mitmproxy-capture.py` → `analyze-traffic.py` and contains43 pre-detected protocol type, auth pattern, endpoint grouping, GraphQL operations,44 batchexecute RPC IDs, and suggested CLI commands. Use it as a starting point —45 verify its findings and fill in anything marked "unknown" by reading `raw-traffic.json`46 manually.4748 **Enhanced analysis (v1.3.0, when captured via mitmproxy-capture.py):**49 - `request_sequence`: Timeline-ordered requests with auth flow detection (login → token → API calls)50 - `session_lifecycle`: Cookie inventory, auth cookie identification, session pattern (cookie_auth/token_refresh/no_session)51 - `endpoint_sizes`: Response body size classification per endpoint (small/medium/large) and total data transferred52 These fields are only present when `mitmproxy-capture.py` was used. If missing (`has_timestamps: false`), rely on manual analysis.5354 If `traffic-analysis.json` doesn't exist, run the analyzer:55 ```bash56 python ${CLAUDE_PLUGIN_ROOT}/scripts/analyze-traffic.py \57 <app>/traffic-capture/raw-traffic.json --summary58 ```59601. Parse `raw-traffic.json` (for details the analyzer couldn't extract)612. Group requests by base path (e.g., `/api/v1/boards/`, `/api/v1/items/`)623. For each endpoint group, identify:63 - HTTP method (GET/POST/PUT/DELETE/PATCH)64 - URL pattern (extract path parameters like `:id`)65 - Query parameters and their types66 - Request body schema (JSON fields, types, required/optional)67 - Response body schema68 - Authentication method (Bearer token, cookie, API key)69 - Rate limiting signals (429 responses, retry-after headers)70714. **Identify RPC protocol type** -- classify the API transport:7273 | Protocol | Detection Signal | Client Pattern |74 |----------|-----------------|----------------|75 | REST | Resource URLs (`/api/v1/boards/:id`), standard HTTP methods | `client.py` with method-per-endpoint |76 | GraphQL | Single `/graphql` endpoint, `query`/`mutation` in body | `client.py` with query templates |77 | gRPC-Web | `application/grpc-web` content type, binary payloads | Proto-based client |78 | Google batchexecute | `batchexecute` in URL, `f.req=` body, `)]}'\n` prefix | `rpc/` subpackage (see `references/google-batchexecute.md`) |79 | Custom RPC | Single endpoint, method name in body, proprietary encoding | Custom codec module |80 | Public REST API | Documented `/api/` endpoints, OpenAPI spec, JSON responses | Standard `client.py` with httpx |81 | Plain HTML (no framework) | No SPA root, no framework globals, data in `<table>`/`<div>` | `client.py` with httpx + BeautifulSoup4 |8283 This determines client architecture in Step B -- REST uses simple `client.py`,84 non-REST protocols need a dedicated `rpc/` subpackage with encoder/decoder/types.85865. Detect data model:87 - Entity types (boards, items, users, projects...)88 - Relationships (board has many items, item belongs to board)89 - ID formats (UUID, numeric, slug)90916. Detect auth pattern:92 - Cookie-based sessions93 - Bearer/JWT tokens94 - OAuth refresh flow95 - API key headers96 - Browser-delegated auth: tokens embedded in page JavaScript (e.g., `WIZ_global_data`),97 not in HTTP headers. Requires CDP for initial cookies, HTTP for token extraction.98 See `references/auth-strategies.md` "Browser-Delegated Auth" section.99 - No auth / public access: fully public API, no login required. CLI may100 optionally support API key auth for write operations (e.g., dev.to).1011027. Write `<APP>.md` -- software-specific SOP document103104**Output:** `<APP>.md` with API map, data model, auth scheme.105106**References:** `traffic-patterns.md`, `google-batchexecute.md`, `ssr-patterns.md`107108---109110## Step B: Implement (Code Generation)111112### Study Existing CLIs First (Critical for Accuracy)113114Before implementing, **read an existing CLI that uses the same protocol** as your115target. These are battle-tested implementations that solved the same problems you'll face.116117| Protocol | Reference CLI | Key files to read |118|----------|--------------|-------------------|119| **Google batchexecute** | `notebooklm/agent-harness/cli_web/notebooklm/` | `core/rpc/encoder.py`, `core/rpc/decoder.py`, `core/client.py`, `core/auth.py` |120| **GraphQL + WAF** | `booking/agent-harness/cli_web/booking/` | `core/client.py` (curl_cffi + GraphQL), `core/auth.py` (WAF tokens) |121| **HTML scraping** | `futbin/agent-harness/cli_web/futbin/` | `core/client.py` (httpx + BS4), `commands/players.py` |122| **HTML + Cloudflare** | `producthunt/agent-harness/cli_web/producthunt/` | `core/client.py` (curl_cffi impersonate) |123| **REST API** | `unsplash/agent-harness/cli_web/unsplash/` | `core/client.py`, `commands/photos.py` |124| **Simple HTML** | `gh-trending/agent-harness/cli_web/gh_trending/` | Minimal structure example |125126**How to use reference CLIs:**1271281. Read the reference CLI's `core/client.py` — understand the request/response pattern1292. Read `core/auth.py` — copy the login_browser() pattern exactly for Google apps1303. Read `core/rpc/` (for batchexecute) — understand encoder/decoder, DO NOT reinvent1314. Read `commands/` — see how Click commands are structured, how --json works1325. Read `utils/helpers.py` — see handle_errors(), _resolve_cli(), repl patterns133134**For batchexecute apps specifically**, the notebooklm CLI is your bible:135- Copy the encoder/decoder architecture (don't reinvent the batchexecute wire format)136- Copy the auth token extraction pattern (CSRF, session ID, build label)137- Copy the cookie domain priority logic (critical for Israeli/international users)138- Adapt the RPC method IDs and param structures to your target app139140The agent implementing the CLI MUST read these files before writing code. Use the141`Agent` tool to dispatch a research agent that reads142the reference implementation while you design the command structure.143144### Design Before You Code145146Before writing any code, note the command structure in `<APP>.md` (10 minutes max):147148- Map each API endpoint group to a Click command group:149 - `/api/v1/boards/*` → `boards` command group150 - `/api/v1/items/*` → `items` command group151- Map CRUD operations to subcommands (GET list → `list`, GET single → `get`,152 POST → `create`, PUT/PATCH → `update`, DELETE → `delete`)153- Note auth design: `auth login`, `auth status`, `auth refresh`; credentials at154 `~/.config/cli-web-<app>/auth.json`155- Note REPL design: bare command enters REPL, branded banner via `repl_skin.py`156157**Goal:** Generate the complete Python CLI package.158159### Package Structure160161See HARNESS.md "Generated CLI Structure" for the complete package template.162Key points: `cli_web/` namespace (NO `__init__.py`), `<app>/` sub-package (HAS `__init__.py`),163`core/`, `commands/`, `utils/`, `tests/` directories.164165### Step B.0: Scaffold Core Modules166167Run the scaffold generator script to create all boilerplate files:168169```bash170python ${CLAUDE_PLUGIN_ROOT}/scripts/scaffold-cli.py <app>/agent-harness \171 --app-name <app> \172 --protocol <rest|graphql|html-scraping|batchexecute> \173 --http-client <httpx|curl_cffi> \174 --auth-type <none|cookie|api-key|google-sso> \175 --resources <comma-separated-resources> \176 [--has-polling] [--has-context] [--has-partial-ids]177```178179This generates exceptions.py, client.py skeleton, helpers.py, config.py, output.py,180the CLI entry point with REPL, setup.py, conftest.py, repl_skin.py, and (for181batchexecute) the rpc/ subpackage.182183> **Fallback**: If the script is unavailable, read `${CLAUDE_PLUGIN_ROOT}/skills/boilerplate/SKILL.md`184> and follow its instructions to scaffold manually.185186After scaffolding, review the generated files and customize `client.py` with actual187endpoint methods from `<APP>.md`.188189### Implementation Rules190191- **`exceptions.py`** -- implement first. Required types: AppError (base), AuthError(recoverable), RateLimitError(retry_after), NetworkError, ServerError(status_code), NotFoundError. See `references/exception-hierarchy-example.py` for the complete template.192193- **`client.py`** -- HTTP client with exception mapping and auth retry:194 - **HTTP library choice:**195 - `httpx` (default) — for most sites (REST, GraphQL, batchexecute)196 - `curl_cffi` — for Cloudflare-protected sites. Uses Chrome TLS fingerprint197 impersonation to bypass bot detection without cookies or auth:198 ```python199 from curl_cffi import requests as curl_requests200 resp = curl_requests.get(url, impersonate="chrome")201 ```202 Use `curl_cffi` when Phase 1 detects Cloudflare (`cf-ray` header, challenge page).203 Add `curl_cffi, beautifulsoup4` to `setup.py` instead of `httpx`.204 - Centralized auth header/cookie injection205 - Automatic JSON parsing with response body verification206 - **Status code → exception mapping**: 401/403→`AuthError`, 404→`NotFoundError`, 429→`RateLimitError`, 5xx→`ServerError`207 - **Auth retry (3-attempt auto-refresh)**: On 401/403: attempt 0 = try current cookies, attempt 1 = reload from `auth.json` on disk, attempt 2 = headless browser refresh via `refresh_auth()` in `auth.py`. See HARNESS.md "Token Auto-Refresh" for the full pattern. The `auth.py.tpl` and `client_rest_*.py.tpl` templates generate this by default.208 - Exponential backoff for rate limits (see `references/polling-backoff-example.py`)209 - For apps with 3+ resource types: split into namespaced sub-clients (`client.notebooks.list()`, `client.sources.add()`)210 - See `references/client-architecture-example.py` for the full pattern211212- **`auth.py`** -- handles token storage, refresh, expiry. Implementation depends on auth type:213214 **For no-auth sites:** DO NOT create `auth.py`, `session.py`, or auth command groups.215 These files are dead code for public APIs and confuse users. The CLI should have216 NO auth-related files or commands. The only exception is if the site has optional217 auth (e.g., API key for write operations) — in that case, implement a minimal218 auth module.219220 **For browser-delegated auth (Google, Microsoft, etc.):** Full playwright-cli login flow221 with cookie domain priority for international users.222223 See `references/auth-strategies.md` for all patterns (browser login, cookie priority, API key, env var, context commands).224 Store cookies at `~/.config/cli-web-<app>/auth.json` with chmod 600.225226- **Anti-bot resilient client construction** (when detected in Phase 2):227 - Extract session tokens via CDP first (cookies), then HTTP GET + HTML parsing (CSRF, session IDs)228 - **Never hardcode** build labels (`bl`), session IDs (`f.sid`), or CSRF tokens -- extract dynamically at runtime229 - Replicate same-origin headers captured during Phase 1 traffic (e.g., `x-same-domain: 1` for Google apps)230 - Implement auto-retry on 401/403: re-fetch homepage -> re-extract tokens -> retry once231 - See `references/google-batchexecute.md` for the complete Google pattern232233- **RPC codec subpackage** (for non-REST protocols like batchexecute):234 When the API uses a non-REST protocol, add `core/rpc/` with:235 - `types.py` -- method ID enum, URL constants236 - `encoder.py` -- request encoding (protocol-specific format)237 - `decoder.py` -- response decoding (strip prefix, parse chunks, extract results)238 The `client.py` still exists but delegates encoding/decoding to `rpc/`.239240- **Progress feedback** -- Use `rich>=13.0` spinners for operations >2s (suppress in --json mode). See `references/rich-output-example.py`.241242- **JSON error output** -- `--json` mode errors are JSON too, not plain text. Standard codes: AUTH_EXPIRED, RATE_LIMITED, NOT_FOUND, SERVER_ERROR, NETWORK_ERROR. Implement via `utils/output.py` json_error().243244- **All commands use `handle_errors(json_mode)` context manager** — centralizes error handling, exit codes (1=user, 2=system, 130=interrupt), and JSON errors. See `references/helpers-module-example.py`.245246- **Generation commands support `--wait`, `--retry N`, `--output path`** — for agent-scriptable end-to-end workflows. See `references/polling-backoff-example.py`.247248- **Windows UTF-8 fix** — Add at the top of `<app>_cli.py` before any imports that print:249 ```python250 import sys251 if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):252 try: sys.stdout.reconfigure(encoding="utf-8", errors="replace")253 except AttributeError: pass254 ```255- **HTML table parsers MUST extract ALL visible columns** — not just name/price,256 because missing fields in `--json` output make the CLI useless for filtering and analysis.257 If the site shows version, club, nation, stats, skills, weak foot — parse all of them.258 Empty fields in `--json` output = incomplete parser.259- Entry point: `cli-web-<app>` via setup.py console_scripts260- Namespace: `cli_web.*`261- Copy `repl_skin.py` from plugin for consistent REPL experience262- **`utils/helpers.py`** -- shared CLI helpers (generate for every CLI):263 - `resolve_partial_id(partial, items)` — prefix-match UUIDs for get/rename/delete264 - `handle_errors(json_mode)` — context manager replacing try/except in all commands265 - `require_notebook(notebook_arg)` — gets notebook ID from arg or persistent context266 - `sanitize_filename(name)` — safe filenames from artifact titles267 - `poll_until_complete(check_fn)` — exponential backoff polling268 - `get_context_value(key)` / `set_context_value(key, value)` — persistent context.json269 See `references/helpers-module-example.py` for the complete module.270271> **Not all helpers apply to every CLI.** Include only what the CLI uses:272> `handle_errors` and `print_json` are always needed. `resolve_partial_id` only273> for UUID-based apps. `require_notebook`/context helpers only for apps with274> persistent context. `poll_until_complete` only for generation/async operations.275276### REPL Implementation Rules (Critical)277278These three bugs appear in almost every generated REPL. Get them right the first time:279280**1. Use `shlex.split()`, never `line.split()`**281282```python283# ✓ Correct — handles quoted args: players search "messi" -> ['players', 'search', 'messi']284import shlex285args = shlex.split(line)286287# ✗ Wrong — produces: ['players', 'search', '"messi"'] — quotes become part of the value288args = line.split()289```290291**2. Never pass `**ctx.params` to `cli.main()` in REPL dispatch**292293```python294# ✓ Correct — preserve --json flag by prepending to args295repl_args = ["--json"] + args if ctx.obj.get("json") else args296cli.main(args=repl_args, standalone_mode=False)297298# ✗ Wrong — ctx.params = {"json_mode": False} gets passed to Context.__init__()299# which doesn't accept it → TypeError: Context.__init__() got an unexpected300# keyword argument 'json_mode'301cli.main(args=args, standalone_mode=False, **ctx.params)302```303304**3. Keep `_print_repl_help()` in sync with the actual command surface**305306The `_print_repl_help()` function in `<app>_cli.py` is the user's first discovery surface — it's what they see when they type `help` in the REPL. It must mirror the real commands, including all key options. A REPL that shows outdated or incomplete help is confusing and makes the CLI feel broken.307308```python309# ✓ Correct — help lists actual options users can pass310def _print_repl_help():311 _skin.info("Available commands:")312 print(" players list [OPTIONS]")313 print(" --position <GK|ST|CM|...> Filter by position")314 print(" --rating-min N --rating-max N Rating range")315 print(" --cheapest Sort cheapest first")316317# ✗ Wrong — stale help doesn't mention new --position, --rating-min, etc.318def _print_repl_help():319 print(" players list [--min-price N] List players with filters")320```321322Rule: **every time you add options to a command, update `_print_repl_help()` in the same commit**.323324---325326**4. Use `@click.argument` for positional REPL params, not `@click.option("--x", required=True)`**327328REPL commands show `players search <query>` in help. If `query` is a `--query` option,329users typing `players search messi` get "Error: Missing option '--query'".330Use positional arguments for natural command-line style:331332```python333# ✓ Correct — users type: players search messi OR players get 21610334@players.command()335@click.argument("query")336def search(query): ...337338@players.command()339@click.argument("player_id", type=int)340def get(player_id): ...341342# ✗ Wrong — users get an error unless they type: players search --query messi343@players.command()344@click.option("--query", required=True)345def search(query): ...346```347348Rule of thumb: if a command takes a single required value that would be a positional arg349in a shell command (`git checkout main`, `grep pattern`), use `@click.argument`.350Use `@click.option` only for optional or named parameters (`--rating-min`, `--platform`).351352### Parallel Implementation (dispatch independent modules as subagents)353354When the CLI has 3+ command groups (e.g., notebooks, sources, chat, artifacts),355dispatch parallel subagents -- one per command module. Each agent gets:356- The `<APP>.md` API spec for its resource357- The `client.py` and `auth.py` interfaces it depends on358- Clear scope: "Implement `commands/notebooks.py` with list, get, create, delete"359360**Parallelization opportunities:**361362| Independent from each other | Dispatch in parallel |363|----------------------------|---------------------|364| `commands/notebooks.py`, `commands/sources.py`, `commands/chat.py` | Yes -- each command file only depends on `client.py` |365| `rpc/encoder.py` and `rpc/decoder.py` | Yes -- encoder doesn't depend on decoder |366| `auth.py` and `models.py` | Yes -- no shared logic |367| `client.py` and `commands/*` | **No** -- commands depend on client |368| `<app>_cli.py` (entry point) | **Last** -- imports all commands, write after they're done |369370**Implementation order (with maximum parallelism):**371372```373Phase A (sequential): Write core foundation374 exceptions.py → client.py → auth.py (if needed) → models.py375376Phase B (parallel): Dispatch ALL independent work simultaneously377 ┌─ Agent 1: commands/notebooks.py378 ├─ Agent 2: commands/sources.py379 ├─ Agent 3: commands/chat.py380 ├─ Agent 4: commands/artifacts.py381 ├─ Agent 5: rpc/encoder.py + rpc/decoder.py (if non-REST)382 └─ Agent 6 (background): test_core.py (unit tests for core modules)383 All run concurrently — each only depends on Phase A modules384385Phase C (sequential): Wire everything together386 utils/helpers.py → <app>_cli.py → __main__.py → setup.py → copy repl_skin.py387```388389**Key parallelism rules:**390- Dispatch independent command modules as parallel subagents (one per `commands/*.py` file)391- Start unit test writing as a background agent during command implementation392- Entry point (`<app>_cli.py`, `setup.py`) must come last (depends on all commands)393394---395396## Mandatory Smoke Check (Before Testing Phase)397398Before invoking testing, install (`pip install -e .`) and verify:3991. `cli-web-<app> --help` loads4002. `cli-web-<app> auth status --json` shows valid (if auth-required)4013. `cli-web-<app> <resource> list --json` returns real data4024. One WRITE command works (if applicable)403404**Red flags — fix before testing:**405- `wrb.fr`, `af.httprm` in output → decoder broken406- `[]` or `null` where data expected → wrong params or client-side operation407- Wrong field values (e.g., "3" instead of prompt text) → parser index mismatch408- Null write response → may be client-side, see `references/google-batchexecute.md` "Client-Side Operations"409410Update phase state:411```bash412python ${CLAUDE_PLUGIN_ROOT}/scripts/phase-state.py complete <app> \413 --phase methodology --output <app>/agent-harness/414```415416## Next Step417418When implementation is complete and the smoke check passes, invoke the `testing`419skill to plan and write tests.420421Do NOT skip testing -- every CLI must have comprehensive tests before publishing.422423---424425## Companion Skills426427| Skill | When it activates |428|-------|------------------|429| `capture` | Phase 1 -- traffic recording (prerequisite for this skill) |430| `testing` | Phase 3 -- test writing, documentation |431| `standards` | Phase 4 -- publish, verify, smoke test |432433---434435## Integration436437| Relationship | Skill |438|-------------|-------|439| **Preceded by** | `capture` (Phase 1) |440| **Followed by** | `testing` (Phase 3) |441| **References** | `traffic-patterns.md`, `auth-strategies.md`, `google-batchexecute.md`, `ssr-patterns.md`, `exception-hierarchy-example.py`, `client-architecture-example.py`, `polling-backoff-example.py`, `rich-output-example.py` |442443---444445## Reference Files446447- **`references/traffic-patterns.md`** -- Common API patterns (REST, GraphQL, RPC)448- **`references/auth-strategies.md`** -- Auth implementation strategies449- **`references/google-batchexecute.md`** -- Google batchexecute RPC protocol spec450- **`references/ssr-patterns.md`** -- SSR framework patterns and data extraction strategies451- **`references/exception-hierarchy-example.py`** -- Complete exception hierarchy with HTTP status mapping452- **`references/client-architecture-example.py`** -- Namespaced sub-client pattern with auth retry453- **`references/polling-backoff-example.py`** -- Exponential backoff polling and rate-limit retry454- **`references/rich-output-example.py`** -- Rich progress bars, JSON error responses, table formatting455456---457> Source: [ItamarZand88/CLI-Anything-WEB](https://github.com/ItamarZand88/CLI-Anything-WEB) — distributed by [TomeVault](https://tomevault.io).458<!-- tomevault:4.0:skill_md:2026-05-27 -->