CLI Reference¶
There are two layers. At the repo root, three shell scripts — ./init,
./demo, ./docsite — are the everyday entry points: they check prerequisites,
install, and launch. Underneath, each agent is an independent
uv-managed project with its own cli.py, and
they only ever talk to each other over HTTP through
the hub.
This page documents the root scripts, all four agent CLIs, and the stage dispatch that lets the hub (and the Dashboard) launch any of them as a background job.
Reading this page
Every command below assumes you've run uv sync once inside the
relevant repo, and that you run commands with uv run so they execute
inside that project's .venv. See Architecture for
how the repos relate, and API Reference for the HTTP contract
these CLIs talk to.
Root scripts — ./init, ./demo, ./docsite¶
Run from the repo root. Each one checks its prerequisites, installs what is
missing, and then launches. Shared helpers (colour output, require,
free_port, start_hub, open_browser) live in scripts/_common.sh.
| Script | Usage | Flags | What it does |
|---|---|---|---|
./init |
./init |
--no-launch, --reset, --port N |
First-run setup from a clean clone: checks uv/Python/Node/npm/curl (and warns about missing ffmpeg), syncs every Python project, builds the Dashboard, prompts for GEMINI_API_KEY and writes it to AnalysisEngine/.env + SimilarContent/.env, then starts the hub and opens a browser. You land on an empty dashboard — no corpus, no proposals, no renders. --reset clears the stored API keys (*/.env, platforms/x/session.txt) so you can re-enter them — your scraped data is kept. To delete the data too, use ./clean, which archives it first. |
./stop |
./stop |
--list |
Stops the hub and any stage jobs this checkout started. Matches processes by working directory, not command line, so several clones of this repo on one machine never stop each other's hub — and it says so if the usual port is held by a different checkout. --list shows what it would stop. Data is untouched. |
./clean |
./clean |
--yes, --keep-keys, --list |
Wipes back to a fresh clone: stops everything, archives every scrap of generated data to backups/cuttingroom-data-<timestamp>.zip, verifies that archive, prints its path, asks, and only then deletes — data and stored API keys. If the archive cannot be written or is corrupt, nothing is deleted. Keys are deliberately not archived (live credentials do not belong in a zip); --keep-keys leaves them in place. Anything git tracks is never removed, and agent-written memory/*/patterns.md is restored to the shipped version. |
./demo |
./demo |
--keep, --port N |
Unpacks demodataset.zip (shipped separately, placed in the repo root) into demo-data/data/, copies it over the working tree and launches, so every view has content. Stops with instructions if no dataset and no zip are present. No API keys, no scraping, no model calls. Overwrites generated data (corpus, studio, renders, evals, logs); never touches source or .env files. --keep launches without overwriting what is already on disk. |
./docsite |
./docsite |
--build, --port N |
Builds this documentation site into documentation/site and serves it with live reload. mkdocs is not a standalone install — it lives in ReelScraper's dev dependency group, so the script runs uv run --project ReelScraper mkdocs. --build builds only. |
./init or ./demo?
./demo is the one to run first if you just want to look around — it is
instant and offline. ./init is the real first-run path for someone who
will scrape their own niche. See
Entry Points & Demo Data for the full picture,
including what demo-data/ contains and its privacy caveat.
The port is not fixed
All three scripts prefer a default port (8787 for the hub, 8000 for the docs) and fall back to a free one when it is busy, printing the port they actually got. Nothing in the system hardcodes 8787 — see Ports below.
ReelScraper/cli.py — the hub¶
Run from inside ReelScraper/. This is the entry point for the product as a
whole: it boots the FastAPI hub, serves the Dashboard, and runs individual
pipeline stages on demand.
| Command | Usage | Args / flags | What it does | Example |
|---|---|---|---|---|
start |
uv run cli.py start [--port N] [--host H] [--strict-port] [--no-browser] |
--port (default 8787, or $HUB_PORT); --host (default 127.0.0.1, or $HUB_HOST); --strict-port fails instead of falling back; --no-browser skips opening one |
Boots the API hub (Uvicorn) and opens the web board. This is "the product" — hub + Dashboard together, since the hub mounts the built Dashboard (frontend/dist) at /. If the preferred port is busy it binds an OS-assigned free one instead of dying. It prints HUB_URL=<url> on stdout and exports BACKEND_API so every stage it spawns inherits the real address. |
uv run cli.py start --port 9123 --no-browser |
scrape |
uv run cli.py scrape <platform> |
<platform> — one of instagram, x, youtube |
Runs the scrape stage manually: shells to platforms/<platform>/scrape.py --file pages.txt, which bootstraps a guest session, paginates each creator's posts, and writes normalized content.json rows locally (no hub POST from inside scrape.py itself). |
uv run cli.py scrape instagram |
analyze |
uv run cli.py analyze <platform> |
<platform> |
Runs the scoring stage: shells to platforms/<platform>/run.py analyze, which runs the 4-signal virality engine (core/virality.py) over scraped content and indexes it into local memory (SQLite FTS5) plus xlsx/CSV reports. |
uv run cli.py analyze instagram |
media |
uv run cli.py media <platform> |
<platform> |
Runs the media-download stage: shells to download_media.py <platform> (cwd = repo root), which persists the top-viral clips' videos and thumbnails to media/<platform>/<content_id>.{mp4,jpg} so the board can play them and AnalysisEngine can watch them. |
uv run cli.py media instagram |
Naming: this stage is called “Media” on the pipeline board, not “Analyze”
cli.py analyze runs the scoring stage (stage 4, virality scoring).
The Blueprint stage (stage 6, Gemini frame-by-frame analysis) is a
separate agent, AnalysisEngine — see below. The pipeline board deliberately
labels stage 6 "Blueprint" rather than "Analyze" to avoid this exact
confusion. See Pipeline for the full stage list.
Per-platform manual commands¶
Independently of cli.py, each platform adapter under platforms/<p>/ also
exposes its own scripts for manual/one-off use:
| Command | What it does | Example |
|---|---|---|
uv run scrape.py --file pages.txt |
Scrapes the handles listed in pages.txt directly (same script cli.py scrape shells to). |
uv run scrape.py --file pages.txt |
uv run run.py analyze |
Runs virality scoring directly (same script cli.py analyze shells to). |
uv run run.py analyze |
uv run run.py search "<query>" |
Searches the platform's local corpus/memory. | uv run run.py search "morning routine" |
uv run run.py insight negative "..." --tags antipattern |
Appends a manual insight of a given kind, tagged, to shared insights. | uv run run.py insight negative "hook too slow" --tags antipattern |
Stage dispatch via the API (not a CLI, but CLI-adjacent)¶
The hub also exposes every stage — including the two below that live in sibling repos — as a subprocess job launched over HTTP, used by the Dashboard's "run stage" buttons:
POST /api/pipeline/{platform}/{stage}
stage ∈ scrape, analyze, media, analysis-engine, auto-search,
auto-search-beat, render. For the first three, the hub shells into the
commands in the table above. For the rest, it shells into the sibling repo's
own CLI:
| Stage | Shells to |
|---|---|
analysis-engine |
uv run cli.py run <platform> inside ../AnalysisEngine |
auto-search |
uv run cli.py run <platform> inside ../AutoSearch |
auto-search-beat |
uv run cli.py beat <platform> inside ../AutoSearch |
render |
the registered producer's own render_cmd, inside its declared dir — the hub hardcodes no producer path. For SimilarContent that resolves to uv run cli.py render inside ../SimilarContent. |
POST /api/pipeline/{platform}/run-all runs only the core four in dependency
order — scrape → analyze → media → analysis-engine — halting on the first
non-zero exit. Discovery is excluded (opt-in, human-gated) and so is render
(paid, per-item, human-triggered). Rendering is normally driven through
POST /api/studio/{p}/{file}/render, which resolves the producer from the item
itself and uses a deterministic job id as a per-item lock.
Each call returns {job_id}; poll GET /api/pipeline/status or subscribe to
the unnamed frames on GET /api/events (SSE) for job status. See
API Reference for the full contract.
curl -X POST http://127.0.0.1:8787/api/pipeline/instagram/scrape
# => {"job_id": "..."}
curl http://127.0.0.1:8787/api/pipeline/status
AnalysisEngine cli.py¶
Run from inside AnalysisEngine/. Watches downloaded clips frame-by-frame
with Gemini and writes schema_version-2 blueprints back to the hub. See
Agents → AnalysisEngine for the full call
sequence.
| Command | Usage | Args / flags | What it does | Example |
|---|---|---|---|---|
run |
uv run cli.py run <platform> [--no-references] |
<platform>; --no-references skips the reference queue |
Bootstraps (health check, self-registers as a producer, fetches its hub-stored config), then drains the analysis work queue: GET /api/analysis/{p}/pending (top-viral clips with media but no blueprint yet, or flagged stale) plus, unless --no-references, GET /api/reference/{p}/pending. Each item is analyzed with Gemini, self-judged, optionally refined, and posted to POST /api/analysis/{p}, with a matching POST /api/evals. Emits the full run.start → item.start → item.stage → item.done → run.end lifecycle to POST /api/logs. |
uv run cli.py run instagram |
once |
uv run cli.py once <content_id> |
<content_id> — a single content or reference id |
Same bootstrap, then searches the pending queues across platforms for the matching id and runs the identical single-item analyze path, wrapped in one run.start/run.end pair — useful for re-running or debugging one clip without draining the whole queue. |
uv run cli.py once ig_abc123 |
status |
uv run cli.py status |
none | Bootstraps, then prints hub connectivity (GET /api/platforms) and this agent's secret presence (GET /api/config/agent/analysis-engine/secrets/status) plus the resolved boot config — a quick health/config check with no side effects. |
uv run cli.py status |
run vs once
Use run <platform> for the normal unattended pipeline pass over the
whole queue. Use once <content_id> when you need to force one specific
clip through analysis — e.g. after fixing a prompt bug, or to unblock a
producer waiting on one blueprint.
AutoSearch cli.py¶
Run from inside AutoSearch/. The "front door" agent: searches Instagram for
new creators, scores niche-fit, and posts candidates to the hub's discovery
queue for human approval. See
Agents → AutoSearch for the full call sequence,
cadence model, and safety rules.
| Command | Usage | Args / flags | What it does | Example |
|---|---|---|---|---|
run |
uv run cli.py run <platform> |
<platform> |
Manual/exhaustive discovery pass. Bootstraps, checks the discovery_enabled kill-switch (fail-closed — skips and logs run.skip if false), loads niche config and corpus factors, expands search terms (via Claude if ANTHROPIC_API_KEY is present, else seed keywords verbatim), bootstraps a guest IG session, and searches each term — hydrating profiles, sampling reels, scoring niche-fit, and posting every passing candidate to POST /api/discovery/{p}. Bypasses the weekly plan/cadence but still respects caps, pacing, and the circuit breaker. |
uv run cli.py run instagram |
beat |
uv run cli.py beat <platform> [--max_units N] |
<platform>; --max_units caps work units per tick |
The cadence heartbeat tick, meant to be invoked frequently (e.g. by the hub's background heartbeat thread, cron, or a scheduled routine). Bootstraps, checks the kill-switch, then runs a pure-local gate (rest_day → out_of_window → over_cap → breaker_cooldown → random probability) — most beats no-op by design and print/log nothing beyond a beat.skip. When the gate opens, it runs the same per-candidate discovery flow as run, capped to beat_max_units (or --max_units) work units, and updates the local cadence ledger. |
uv run cli.py beat instagram |
synthetic |
uv run cli.py synthetic <platform> [N] |
<platform>; optional count N |
Verification path: fabricates N candidates locally with no Instagram or Anthropic calls, and drives the identical item.start → Searching → Scoring → post_candidate → item.done sequence as a real run, ending with one post_insight and run.end. Useful for exercising the hub/Dashboard discovery UI without touching Instagram. |
uv run cli.py synthetic instagram 5 |
smoke |
uv run cli.py smoke |
none | Verification path: asserts the guest cookie jar carries no sessionid and performs one web_profile_info hydration. No hub writes besides logs — a safety self-check that the guest-only posture is intact before a real run. |
uv run cli.py smoke |
status |
uv run cli.py status |
none | Bootstraps, then reports hub connectivity, this agent's secret presence, and the resolved config (including discovery_enabled and cadence knobs) — a quick health/config check with no side effects. |
uv run cli.py status |
Kill-switch and pacing are enforced in the code path, not just documented
Both run and beat read discovery_enabled from
GET /api/config/agent/auto-search and fail closed (default false) if
it's missing or false. AutoSearch is one of only two agents permitted to
touch Instagram at all (alongside ReelScraper itself), and it does so
read-only, guest-first, paced strictly slower than the scraper, with a
circuit breaker on repeated failures.
SimilarContent cli.py¶
Run from inside SimilarContent/. The reference producer: it reads the
scored corpus plus blueprints, writes clone recipes into the human gate, and —
once a human approves one — renders it into an actual reel. See
Agents → Producers & SPI for the manifest contract.
The two halves are deliberately split by cost:
proposeis free. It reads blueprints and writes markdown. No image-provider key required.renderis paid. Every frame is an image-API call, so it is human-triggered only and never part of the one-click pipeline run.
| Command | Usage | What it does |
|---|---|---|
propose |
uv run cli.py propose --platform instagram |
Ranks the corpus, attaches schema-2 blueprints, and publishes the easiest-to-make winners to POST /api/studio/{p} as proposed. |
render |
uv run cli.py render --platform instagram --file <name.md> |
Turns ONE approved recipe into a reel: generates frames, stitches with ffmpeg, writes a caption, and uploads the result to POST /api/renders/{p}. This is what the hub launches when you press Render in the Studio. |
status |
uv run cli.py status [--platform instagram] |
Prints the active image provider and whether its key resolves, whether ffmpeg is present, and a checklist of approved items with their render state. No side effects. |
register |
uv run cli.py register |
(Re)posts this producer's manifest to POST /api/producers/register. Idempotent by name. |
The verb is propose, not run
Unlike AnalysisEngine and AutoSearch, SimilarContent has no run
subcommand — because there is no single unattended pass to make. The
human gate sits between the two halves by design.
propose flags¶
| Flag | Default | What it does |
|---|---|---|
--platform |
instagram |
Platform to read the corpus from. |
--count N |
the top_n hub knob (5) |
How many recipes to publish this run. |
--top N |
max(15, 3 × count) |
Size of the corpus pool to rank before picking. |
--topic "..." |
— | Focus on a topic via /api/corpus/{p}/search instead of /top. |
--content-id ID |
— | Propose these exact exemplars, skipping ranking. Repeatable. Ranking cannot reach a mid-corpus clip (e.g. a freshly scraped creator), so this is the escape hatch. |
--dry-run |
off | Select and build the recipes, but POST nothing. |
render flags¶
render requires either --file or --all-approved — it will not guess.
| Flag | Default | What it does |
|---|---|---|
--platform |
instagram |
Platform whose studio to read. |
--file <name.md> |
— | Render exactly one studio item (what the hub passes). |
--all-approved |
off | Render every approved item that has no render yet. |
--limit N |
— | Cap --all-approved. |
--max-frames N |
the max_frames_per_clone knob (12) |
Override the per-clone frame budget. |
--force |
off | Re-render even if a render already exists. |
--restitch |
off | Re-encode the frames already on disk. Free — no image calls, same pictures, existing caption kept. Use after a stitcher change. |
--dry-run |
off | Parse the recipe, allocate frame holds, and print every composed prompt without making a single API call. |
Start with --dry-run
It is where most mistakes are visible, and where they cost nothing.
Output shape knobs¶
Two register.py knobs control the canvas, both settable from the Dashboard's
agent config:
| Knob | Default | Values | What it does |
|---|---|---|---|
aspect_ratio |
9:16 |
9:16, 4:5, 1:1 |
The output canvas. 9:16 (1080×1920) is the reels/shorts/tiktok format and the only one that fills a phone full-bleed. 4:5 = 1080×1350 (IG feed portrait), 1:1 = 1080×1080. |
video_fit |
auto |
auto, cover, contain |
How a generated frame meets that canvas. auto crops when the frame is within 10% of the canvas aspect and letterboxes when it is further out; cover always crops; contain always letterboxes. None of them ever stretch. |
There is deliberately no width/height knob
The canvas is derived from aspect_ratio, so the output can never be a
size that disagrees with the aspect it claims to be.
Guard rails¶
- A key is only demanded when one is actually needed.
propose,status,--dry-runand--restitchall bootstrap withneed_key=False. renderskips anything notapproved, and skips already-rendered items unless--forceor--restitch.- Circuit breaker. Three consecutive image failures — a bad key or an exhausted quota — abort the whole run rather than burning the queue one paid failure at a time.
- ffmpeg is checked up front on any non-dry run.
Ports: nothing is hardcoded¶
8787 is a preference, not a contract. cli.py start probes the port and
binds an OS-assigned free one when it is busy, then:
- prints
HUB_URL=http://127.0.0.1:<port>on stdout, so a launcher script can discover the real port; - exports
BACKEND_APIinto its own environment, so every stage subprocess it spawns (AnalysisEngine, AutoSearch, a producer's render command) inherits the address the hub actually got instead of defaulting to 8787.
Every agent resolves the hub from BACKEND_API (falling back to
http://127.0.0.1:8787). The Dashboard dev server reads the same variable —
see Agents → Dashboard. So when the hub lands on
another port:
BACKEND_API=http://127.0.0.1:9123 uv run cli.py status # any agent
BACKEND_API=http://127.0.0.1:9123 npm run dev # Dashboard dev server
Production needs none of this: the hub serves the built Dashboard same-origin, so it is port-agnostic by construction.
One port per checkout¶
A fallback port is fine for a one-off, and useless as an address — it changes on
every restart, so nothing can be bookmarked and no .env can point at it. ./init
therefore pins the port a checkout owns into ReelScraper/.env as HUB_PORT
(8787 for the first clone on the machine, 8788 for the next) and writes that address
into every component's BACKEND_API.
That second half is the one that matters. cli.py start exports BACKEND_API only
for the stages it spawns; an agent you run by hand reads its own .env. On a
second clone, a .env still pointing at 8787 aims that agent at the first clone's
hub — where it reads the wrong corpus and writes to the wrong studio, with every call
returning 200. Each agent checks GET /api/hub at startup and refuses to run against
a hub belonging to another checkout. See
Niches → Running two niches at once.
Planned: unified CLI-parity commands¶
Not yet built
The following are planned additions, not present in the current CLIs. They're called out here so the eventual command surface is discoverable ahead of implementation.
The Dashboard's human gate currently exposes discovery candidate
approve/reject only as an HTTP call
(POST /api/discovery/{platform}/{candidate_id}/status, see
API Reference). The plan is to give AutoSearch's
own CLI parity with that gate so it can be operated headlessly (e.g. from a
script or cron job) without going through the Dashboard:
| Planned command | Would do |
|---|---|
discover list |
List pending discovery candidates for a platform (CLI equivalent of GET /api/discovery/{platform}/pending). |
discover approve <candidate_id> |
Approve a candidate — hub appends the handle to pages.txt (CLI equivalent of POST /api/discovery/{platform}/{candidate_id}/status {"status":"approved"}). |
discover reject <candidate_id> |
Reject a candidate (CLI equivalent of the same endpoint with {"status":"rejected"}). |
These would call the same /api/discovery/* routes the Dashboard already
uses — no new hub endpoints, just a CLI-side client for the existing human
gate contract.
See also¶
- Architecture — how the hub, agents, and Dashboard fit together.
- Pipeline — the 7-stage Discover → Studio flow these commands drive.
- API Reference — the full
/api/*contract every CLI talks to. - Agents — per-agent call sequences (AnalysisEngine, SimilarContent, AutoSearch).