Sleeper Hit Docs

CLI

Install, configure, and automate Sleeper Hit Studio from a terminal.

View as Markdown

Command-line client for the Sleeper Hit Studio B2B Story API (/api/v1). It maps 1:1 onto the documented endpoints — create projects, build a Series Bible, map and run a podcast season, generate gated episodes, publish an RSS feed, and open the live table-read theater.

The endpoints, auth, error envelope, and flow mirror the Story API docs (quickstart, concepts, API reference). Nothing here contradicts those docs — the CLI is a thin, scriptable wrapper around the same HTTP surface.

Requirements

  • Node.js >= 20 (uses the built-in global fetch and node:crypto.randomUUID).
  • A Sleeper Hit Studio customer API key. Create one at https://sleeperhit.studio/dashboard/api.
  • Or sign in from the browser with sleeperhit login. A plain login asks for every scope (the approval page lists each one); --scopes story:read,story:write asks for exactly that list instead. The key it saves lasts 90 days; logging in again renews it and revokes the key the previous login saved, and sleeperhit logout revokes it on the server. sleeperhit whoami prints the key's scopes and expiry. The saved file is shared with the stdio MCP server's login tool.

Install

npm install -g @sleeperhit/cli sleeperhit --help

Or run it once without installing: npx @sleeperhit/cli --help. The package is one bundled file with no runtime dependencies.

From the monorepo

The package lives at packages/cli and is picked up by the packages/* workspace glob:

pnpm install pnpm --filter @sleeperhit/cli build # bundles src (and @sleeperhit/shared) -> dist/index.js cd packages/cli pnpm link --global # now `sleeperhit` is on your PATH

Run without building (dev)

# from packages/cli pnpm dev -- projects list # or directly with a TS-aware runner: pnpm exec tsx src/index.ts projects list

Configuration

Env varRequiredDefaultPurpose
SLEEPERHIT_API_KEYyes—Bearer key, sh_<prefix>_<secret>. Sent as Authorization: Bearer <key>.
SLEEPERHIT_BASE_URLnohttps://sleeperhit.studioOrigin to target. /api/v1 is appended automatically. Point at https://stage.sleeperhit.studio or a local dev origin when testing.
export SLEEPERHIT_API_KEY="sh_xxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # optional export SLEEPERHIT_BASE_URL="https://sleeperhit.studio"

The CLI auto-generates an Idempotency-Key (UUID) for every POST. Override it with --idempotency-key <key> when you want a stable key tied to your own job record for safe retries.

Errors are printed in the standard { error: { code, message, requestId } } form. On rate_limited the exact Retry-After is surfaced; jobs watch honors it automatically. The CLI exits non-zero on any error (1 API/runtime, 64 usage, 2 when a requested operation is unavailable in the current environment).

Commands

login [--scopes <list>] [--no-browser] [--base-url <url>]   # every scope unless --scopes narrows it; 90-day key; revokes the previous login's key
logout                                                      # revokes the saved key on the server, then removes it
whoami [--api-key <key>]                                    # the key's scopes and expiry
capabilities
guidance
credits

projects create --name <name> [--description <text>] [--metadata <json>]
projects from-script <scriptId> | --script <scriptUploadId>
projects list [--limit N] [--cursor C]
projects get <projectId>        # prints the project JSON, then its workspace gate and table-read cast readiness on stderr
projects episodes <projectId> [--season N]          # seasons, episodes (made in the app included), versions, videos
projects seasons add <projectId> [--name <text>]     # name a new season (free)
projects episodes start <projectId> <episodeId>      # a Series Bible episode as an empty draft
projects update <projectId> [--name <text>] [--description <text>]
        [--metadata <json>] [--archived true|false]

bible get <projectId>
bible save <projectId> --json '<object>' | --file <json> [--title <text>]
bible generate <projectId> [--instructions <text>]
bible coverage <projectId> [--generate] [--focus <text>]
outline coverage <projectId> [--generate] [--focus <text>]

seasons create <projectId> [--title <text>] [--season-number <n>]
        [--publishing-series-id <id>] [--coverage-policy <json>] [--run-key <key>]
seasons list <projectId>
seasons get <runId>
seasons map <runId> <episodeId> --expected-map-version <n>
        [--patch <json>] [--title <text>] [--summary <text>] [--objective <text>]
        [--audience <text|null>] [--tone <text|null>] [--notes <text|null>]
        [--duration-minutes <n>] [--episode-number <n>]
        [--confirmed true|false] [--confirm]
seasons approve <runId> --expected-map-version <n> --confirm
seasons advance <runId> [--episode-id <id>]
seasons replan <runId> [--episode-id <id>] [--instruction <text>] --confirm
seasons retry-coverage <runId> [--episode-id <id>] --confirm
seasons revise <runId> [--episode-id <id>] --instruction <text> --confirm
seasons publish <runId> [--episode-id <id>] --confirm

sources add <projectId> --type text|markdown|url|pdf
        [--content <text> | --file <path>]   (text / markdown)
        [--uri <http(s) url>]                (url / pdf)
        [--label <text>] [--retention EXTRACTED_ONLY|STORE_ORIGINAL|METADATA_ONLY]
        [--metadata <json>]                  use {"sourceContextMode":"full"} for complete planner + writer context
sources list <projectId> [--limit N] [--cursor C]
sources get <projectId> <sourceId>

plans create <projectId>
        --target audience=...,objective=...,outcome=...   (repeatable)
        --artifact type=table_read,mode=podcast|documentary|drama[,narrationPolicy=auto|include|suppress][,durationSeconds=N]
        [--brief genre="..." --brief audience="..." --brief pageTarget=4]
        [--source <sourceId> ...] [--title <text>] [--auto-approve]  (non-table artifacts only)
        [--json '<full payload>']    power-user escape hatch
plans list <projectId> [--limit N] [--cursor C]
plans get <planId>
plans approve <planId> --confirm
plans reject <planId> [--reason <text>]

jobs create <planId> [--project <projectId>] [--artifact type=...,mode=...]
        [--voice-map '{"GARY":{"voiceId":"...","provider":"hume"}}'] [--script <scriptUploadId>]
jobs list [--project <id>] [--plan <id>] [--limit N] [--cursor C]
jobs get <jobId>
jobs watch <jobId> [--interval <ms>]   Poll until terminal; print manifest URLs on READY
jobs cancel <jobId>
jobs artifacts <jobId>

artifacts get <artifactId> [--revision N]
artifacts render-video <artifactId>

script get <artifactId> [--scope all|page|scene|character|range] [--page N] [--scene N]
        [--character <name>] [--start N --end N] [--limit N]
script page <artifactId> <page>
script scene <artifactId> <sceneIndex>
script character <artifactId> <character...>
script replace <artifactId> --file <path> [--instruction <text>] [--narration-policy include|suppress]
script upload --title <title> --file <path> [--format text|fountain|fdx]   # a new script in no project
script show <scriptId>                    # the script's id, its Writers Lab URL id, or any version's id
script update <scriptId> [--title <t>] [--slug <s>] [--logline <text>] [--media-type film|tv]
        [--genre <text>] [--season N] [--clear logline|genre|media-type]...
script versions add <scriptId> --file <path> [--format text|fountain|fdx]
script write <scriptUploadId> --file <path> [--expected-file <path>]   # a draft's or version's whole text
script finish <scriptUploadId> [--coverage adopt|fresh]                # the recommendation, then finish
script versions delete <scriptId> <scriptUploadId> --confirm
script export <scriptUploadId> --format fountain|fdx|pdf [--out <path>] [--no-title-page]
        [--from-page N] [--to-page N] [--font screenplay-sim|courier-prime|courier-new|final-draft]
script overrides get <scriptUploadId>
script overrides set <scriptUploadId> [--avatar <text>] [--theater <text>] [--soundscape <text>]
        [--pitch-deck <text>] [--locations <text>] [--clear <override>]...
script table-read prepare <scriptUploadId> [--confirm]
        Prepare a cast episode's table read (the Writers Lab's Continue to Table Read): prints the price;
        --confirm charges it once for this screenplay version and adds its sound effects and music.

cast <artifactId>
cast update <artifactId> --json '<entries-or-{entries,avatarStyle}>'
cast restyle <artifactId> --style <s>
avatar set <artifactId> --character <c> [--style <s>] [--refine <r>] [--reference-image-url <u>]
voice set <artifactId> --character <c> --voice-id <id> --voice-name <name> [--gender <g>] [--provider <p>]
modify-voice <artifactId> --start <n> --end <n>
        [--effect autotune] [--key C..B] [--scale major|minor|majpent|minpent|chromatic]
        [--strength <0..1>] [--smooth <int 1+>] [--reverb none|light|chapel]

refine <artifactId> <instruction...> [--scope auto|screenplay|plan]
finalize <artifactId> [--mode audio|video]

coverage generate <artifactId> [--focus <text>]
coverage get <artifactId> [--report <id>]
coverage watch <artifactId> [--interval <ms>]

music generate <artifactId> [--coverage <0..1|0..100>]
music update <artifactId> [--scope screenplay|scene|entry] [--scene <n>] [--entry <n>]
        [--mode merge|replace] [--prompt <text>] [--summary <text>] [--weight <0..2>]
        [--mute | --enable]
music get <artifactId>
music watch <artifactId> [--interval <ms>]

sfx add <artifactId> --entry <n> --label <text> --prompt <text>
        [--volume <0..1>] [--trigger before|with|after] [--offset-ms <n>] [--duration <s>] [--disable]
sfx update <artifactId> --id <cueId>
        [--entry <n>] [--label <text>] [--prompt <text>] [--volume <0..1>]
        [--trigger before|with|after] [--offset-ms <n>] [--duration <s>] [--regenerate]
        [--enable | --disable]
sfx list <artifactId>
sfx remove <artifactId> --id <cueId>

Run sleeperhit --help for the same reference at the terminal.

Post-creation artifact operations

refine, finalize, cast, avatar, voice, modify-voice, coverage, music, and sfx are wired against the live /api/v1/artifacts/{artifactId} operation families. They mutate or analyze the same table read artifact while keeping the artifact id and share URLs stable. Commands that queue work return JSON immediately; use the matching watch command or poll artifacts get until the status is terminal.

End-to-end example

Copy-pasteable. Replace the captured IDs as you go (or pipe through jq).

export SLEEPERHIT_API_KEY="sh_..." # 1. Confirm the table read is available and you have credits sleeperhit capabilities | jq '.artifacts.table_read.availability' sleeperhit credits | jq '.credits.balance' # 2. Create a project PROJECT_ID=$(sleeperhit projects create --name "Q3 Launch Story" \ --description "Product launch table read" | jq -r '.project.id') # 3. Attach an inline text source (instant READY) SOURCE_ID=$(sleeperhit sources add "$PROJECT_ID" \ --type text --label press-release \ --content "Acme Corp launches its next-gen platform in Q3 2026, cutting onboarding time 60%." \ | jq -r '.source.id') # 4. Generate the plan PLAN_ID=$(sleeperhit plans create "$PROJECT_ID" \ --target audience="enterprise buyers" \ --target objective="introduce the Q3 platform launch" \ --target outcome="book a demo within 7 days" \ --target tone="confident, specific" \ --artifact type=table_read,mode=documentary \ --source "$SOURCE_ID" | jq -r '.plan.id') # 5. Poll until REQUIRES_APPROVAL, review the exact plan, then cross the human hold sleeperhit plans get "$PLAN_ID" | jq '{status:.plan.status, plan:.plan.plan, total:.plan.quote.total}' sleeperhit plans approve "$PLAN_ID" --confirm # 6. Create the job (reserves credits) and watch it to completion JOB_ID=$(sleeperhit jobs create "$PLAN_ID" | jq -r '.job.id') sleeperhit jobs watch "$JOB_ID" # On READY, watch prints: # Theater: https://sleeperhit.studio/share/table-read/<token> # Theater (full): https://sleeperhit.studio/share/table-read/<token>?view=fullscreen # Audio (live): https://sleeperhit.studio/share/table-read-audio/<token> # Video render URL: /api/v1/artifacts/<artifactId>/render-video # 7. (Optional) opt into a durable MP4 ARTIFACT_ID=$(sleeperhit jobs get "$JOB_ID" \ | jq -r '.job.artifacts[] | select(.type=="table_read") | .id') sleeperhit artifacts render-video "$ARTIFACT_ID"

The shareToken baked into the theater/audio URLs authorizes them — not your API key — so you can hand those URLs directly to end users.

A table read performs the project's cast; it never makes one. From a plan, it starts only when the project's cast canon covers every character — a voice for everyone and, unless the project is audio-only, a headshot, body shot and character sheet. sleeperhit projects get <projectId> prints who still needs what (project.tableReadReadiness); a job that would refuse is a 409 cast_precondition_failed before anything is created or charged.

For voices the cast canon does not carry, first use sleeperhit voices list, then provide a character-to-voice map on an explicit table-read artifact request:

sleeperhit jobs create "$PLAN_ID" \ --artifact type=table_read,mode=documentary \ --voice-map '{"GARY":{"voiceId":"voice_gary","provider":"hume"},"MAEVE":{"voiceId":"voice_maeve","provider":"hume"}}'

--voice-map requires exactly one --artifact type=table_read request. Each entry requires voiceId; voiceName, gender, and provider are optional.

To perform a finalized, cast-ready screenplay as written (no new pages, no coverage rewrite), adopt its version id:

sleeperhit jobs create "$PLAN_ID" --script "$SCRIPT_UPLOAD_ID"

--script uses the single table-read request (adding one when none is given) and cannot be combined with --voice-map.

Use it in your own agent workflow

The CLI emits JSON on stdout and human progress on stderr, so it composes cleanly inside scripts or an agent's tool layer:

#!/usr/bin/env bash set -euo pipefail export SLEEPERHIT_API_KEY="${SLEEPERHIT_API_KEY:?set your key}" ARTICLE_URL="$1" PROJECT_ID=$(sleeperhit projects create --name "Agent run $(date +%s)" | jq -r '.project.id') # Async URL source — poll to READY before planning SOURCE_ID=$(sleeperhit sources add "$PROJECT_ID" --type url --uri "$ARTICLE_URL" | jq -r '.source.id') until [ "$(sleeperhit sources get "$PROJECT_ID" "$SOURCE_ID" | jq -r '.source.status')" = "READY" ]; do sleep 3 done PLAN_ID=$(sleeperhit plans create "$PROJECT_ID" \ --target audience="general" --target objective="summarize the article" --target outcome="share a 90s table read" \ --artifact type=table_read,mode=podcast,narrationPolicy=include,durationSeconds=90 \ --source "$SOURCE_ID" | jq -r '.plan.id') # Wait for the async planner, then stop for exact human review. until [ "$(sleeperhit plans get "$PLAN_ID" | jq -r '.plan.status')" = "REQUIRES_APPROVAL" ]; do sleep 4; done sleeperhit plans get "$PLAN_ID" | jq '.plan | {title, plan, quote}' read -r -p "Approve this exact plan? [y/N] " APPROVE case "$APPROVE" in y|Y|yes|YES) ;; *) exit 1 ;; esac sleeperhit plans approve "$PLAN_ID" --confirm JOB_ID=$(sleeperhit jobs create "$PLAN_ID" | jq -r '.job.id') sleeperhit jobs watch "$JOB_ID" # blocks, streams progress, prints share URLs on READY

For a power-user payload that exactly matches the API schema, bypass the flag helpers with --json:

PLAN_ID=$(sleeperhit plans create "$PROJECT_ID" --json '{ "title": "Launch narrative", "target": { "audience": "enterprise buyers", "objective": "introduce the launch", "outcome": "book a demo" }, "artifactRequests": [{ "type": "table_read", "mode": "drama", "narrationPolicy": "suppress", "durationSeconds": 120 }], "styleConstraints": { "voicePreference": "authoritative, not salesy" }, "sourceIds": ["'"$SOURCE_ID"'"] }' | jq -r '.plan.id') # After it reaches REQUIRES_APPROVAL, review the exact plan with the user. sleeperhit plans get "$PLAN_ID" | jq '.plan | {title, plan, quote}' sleeperhit plans approve "$PLAN_ID" --confirm

See also

  • Quickstart — the same flow as raw HTTP / Node.
  • API reference — every endpoint, field, and error code.
  • Concepts — the plan/job/artifact model and live post-creation operations.
  • GET /api/v1/openapi.json — the live OpenAPI 3.1 spec.

License

MIT. See LICENSE.