@sleeperhit/cli
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 docs/agents/ (README, 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
fetchandnode:crypto.randomUUID). - A Sleeper Hit Studio customer API key. Create one at https://sleeperhit.studio/dashboard/api.
- Recommended scopes for the complete season-to-RSS flow:
story:read,story:write,source:read,source:write,credits:read,artifact:read,artifact:publish,publishing:read,publishing:write, andpublishing:publish. A plainsleeperhit loginis intentionally read-only; request the additional scopes with--scopesbefore running write or publishing commands.
Install
From the monorepo root (the package lives at packages/cli and is picked up by the existing packages/* workspace glob):
pnpm install pnpm --filter @sleeperhit/cli build # compiles src -> dist
Global link (run sleeperhit anywhere)
cd packages/cli pnpm link --global # now `sleeperhit` is on your PATH sleeperhit --help
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 var | Required | Default | Purpose |
|---|---|---|---|
SLEEPERHIT_API_KEY | yes | — | Bearer key, sh_<prefix>_<secret>. Sent as Authorization: Bearer <key>. |
SLEEPERHIT_BASE_URL | no | https://sleeperhit.studio | Origin 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>]
logout
whoami [--api-key <key>]
capabilities
guidance
credits
projects create --name <name> [--description <text>] [--metadata <json>]
projects list [--limit N] [--cursor C]
projects get <projectId>
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>]
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]
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"}}']
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]
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.
To keep a known cast instead of running automatic voice assignment, first use
sleeperhit voices list, then provide one complete 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.
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
docs/agents/quickstart.md— the same flow as raw HTTP / Node.docs/agents/api-reference.md— every endpoint, field, and error code.docs/agents/concepts.md— the plan/job/artifact model and live post-creation operations.GET /api/v1/openapi.json— the live OpenAPI 3.1 spec.