CLI

@nodaro/cli is a terminal client for Nodaro. It covers the core workflow/studio surface of the SDK — list and run workflows, run apps, run a single node directly, watch executions until they finish — wrapped in a small commander binary with multi-profile auth, JSON output for scripts, and --watch mode for interactive runs. (The CLI intentionally omits credits, developer-apps, OAuth, pipelines, reduce, and upload helpers — use the SDK directly for those.)

If you’re integrating from code, prefer the SDK directly. The CLI is the convenience wrapper for terminal work, cron jobs, CI pipelines, and ad-hoc inspection.

Install

Option A — npm (cross-platform, requires Node ≥ 20)

npm install -g @nodaro/cli
nodaro --version

Or run a one-off without installing:

npx @nodaro/cli projects list

Option B — standalone binary (no Node required)

Single-file executables compiled with bun build --compile — ~60 MB, ~10 ms cold start, no dependencies.

# macOS Apple Silicon
curl -L https://github.com/nodaroai/app.nodaro.ai/releases/latest/download/nodaro-darwin-arm64 \
  -o /usr/local/bin/nodaro && chmod +x /usr/local/bin/nodaro

# macOS Intel
curl -L https://github.com/nodaroai/app.nodaro.ai/releases/latest/download/nodaro-darwin-x64 \
  -o /usr/local/bin/nodaro && chmod +x /usr/local/bin/nodaro

# Linux x86_64
curl -L https://github.com/nodaroai/app.nodaro.ai/releases/latest/download/nodaro-linux-x64 \
  -o /usr/local/bin/nodaro && chmod +x /usr/local/bin/nodaro

# Linux ARM64
curl -L https://github.com/nodaroai/app.nodaro.ai/releases/latest/download/nodaro-linux-arm64 \
  -o /usr/local/bin/nodaro && chmod +x /usr/local/bin/nodaro

Windows: download nodaro-windows-x64.exe from the releases page and rename to nodaro.exe.

Authentication

Generate a token at https://app.nodaro.ai/settings/api, then save it locally:

nodaro auth login                                   # interactive (opens a browser by default; use --no-browser to paste a token)
nodaro auth login --token "$NODARO_TOKEN"           # non-interactive
nodaro auth login --no-browser                      # skip browser flow, paste token instead
nodaro auth login --profile staging --base-url https://next.nodaro.ai

The token is stored at ~/.config/nodaro/config.json with chmod 0600. Override the location with NODARO_CONFIG_DIR.

nodaro auth status                                  # show profile (token masked)
nodaro auth logout                                  # delete the saved profile

Profiles

Switch between staging / prod / local instances with --profile:

nodaro auth login --profile prod    --base-url https://app.nodaro.ai
nodaro auth login --profile staging --base-url https://next.nodaro.ai
nodaro auth login --profile local   --base-url http://localhost:8000

nodaro projects list --profile staging

When you omit --profile, the CLI uses the profile named production. The first profile you create becomes the default, so a single nodaro auth login (no --profile) sets you up without any extra flags.

Three ways to run something

The CLI exposes the three execution paths the platform supports — pick the one that matches what you’ve already built.

Goal Command
Run a saved DAG nodaro workflows run <workflowId>
Run a published app (curated inputs/outputs) nodaro apps run <slug> --input k=v
Run a single node directly (no DAG) nodaro nodes run <type> --param k=v

The nodes run path is the SDK / CLI equivalent of the MCP server’s verb tools (generate_image, generate_video, etc.) — the route convention is POST /v1/<type> for every generation node, so any node listed by nodaro nodes list can be invoked with nodaro nodes run.

Commands

# Auth
nodaro auth login [--profile <name>] [--token <token>] [--base-url <url>] [--no-browser]
nodaro auth status [--profile <name>] [--json]
nodaro auth logout [--profile <name>]

# Projects
nodaro projects list [--json]
nodaro projects get <id> [--json]
nodaro projects create --name <name> [--description <desc>] [--json]
nodaro projects update <id> [--name <name>] [--description <desc>] [--json]
nodaro projects delete <id> [--json]

# Workflows
nodaro workflows list --project <projectId> [--json]
nodaro workflows get <id> [--json]
nodaro workflows create --project <projectId> --name <name> [--file bundle.json] [--json]
nodaro workflows update <id> [--name <name>] [--file nodes-edges.json] [--json]
nodaro workflows delete <id> [--json]
nodaro workflows export <id> [--with-assets] [--output bundle.json]
nodaro workflows import <file> --project <projectId> [--json]
nodaro workflows run <id> [--watch] [--node n1 n2 ...] [--json]

# Sharing (workspace features)
nodaro workflows share <id> [--visibility workspace|private] [--json]
nodaro workflows move <id> --project <projectId> [--json]
nodaro workflows shared-with-me [--json]
nodaro workflows collaborators list <id> [--json]
nodaro workflows collaborators add <id> (--user <userId> | --email <email>) --role viewer|editor [--json]
nodaro workflows collaborators update <id> <userId> --role viewer|editor [--json]
nodaro workflows collaborators remove <id> <userId> [--json]

# Apps — workflows wrapped in a curated UI
nodaro apps list [--search <query>] [--limit <n>] [--cursor <token>] [--category <slug>]
nodaro apps get <slug>                                  # show input schema + outputs
nodaro apps run <slug> --input prompt="…" [--watch]
nodaro apps run <slug> --params-file inputs.json [--watch]
nodaro apps runs <slug> [--limit <n>] [--cursor <token>] [--json]  # list past runs
nodaro apps run-get <slug> <runId>

# Nodes — list types + run a single node directly
nodaro nodes list [--category input|parameter|ai-image|ai-video|ai-audio|ai-text|processing|composition|output|control|entity|trigger|utility] [--json]
nodaro nodes get <type>                                 # full input schema
nodaro nodes run <type> --param prompt="…" --param provider=flux [--watch]
nodaro nodes run <type> --params-file body.json [--watch] [--poll-interval 1000]

# Models — browse the model catalog (capability sheets, credit pricing, prompt tips)
nodaro models list [--kind image|video|audio] [--mode t2v|i2v|t2i|tts|…] [--family <vendor>] [--featured] [--json]
#   table shows id, kind, family, modes, credit tiers, featured ★ and the doctrine ✓ flag; --json for the full sheets

# Pickers — valid values for parameter-picker nodes (setting, mood, person, …)
nodaro pickers list [--json]                            # all picker node types + option counts
nodaro pickers get <nodeType> [--full] [--category <c>] [--field <f>] [--json]
#   default (compact) carries id, label, category, term, icon — `term` is the short
#   professional phrase compact hint mode injects ("" for a no-op auto/none option)
#   --full adds each option's description + the prompt fragment it injects
#   --category filters a single-dim picker; --field picks one dimension of a multi-dim picker (person/styling/framing)
nodaro pickers analyze "<text>" [--target <types>] [--instructions <text>] [--model <id>] [--effort <level>] [--json]
#   AI Fill: choose picker values from a free-text description (credit-billed LLM call);
#   --target limits to a comma-separated list of picker node types (default: all analyzable)

# Catalog — maintain a deployment's VENDORED catalog packs (offline, file-based; no auth/client)
nodaro catalog snapshot --in <file>                     # echo a detail=full /v1/catalogs projection + sidecars JSON
nodaro catalog diff-upstream --baseline <f> --upstream <f> --pack <f> [--write <f>]
#   three-way merge: carries upstream edits for entries you left UNMODIFIED (with their 11-locale
#   sidecar strings), reports conflicts (you edited AND upstream changed — pack kept), lists new
#   upstream entries and upstream removals — NOTHING is auto-admitted. Exit code 2 on conflicts.
nodaro catalog validate --pack <file> [--exempt es,fr,…]
#   checks a pack snapshot has sidecar coverage across the 11 locales (or declared exemptions);
#   exit code 1 when a non-exempt locale is missing a translation

# Shots — Cine share → remix records
nodaro shots get <id> [--json]                          # public shots resolve for anyone holding the id
nodaro shots create [--file shot.json] [--visibility private|public] [--json]
nodaro shots update <id> [--file fields.json] [--visibility private|public] [--json]
nodaro shots delete <id>

# Recast — regenerate an analyzed video with your own cast + authored-script import (Cloud edition)
nodaro recast skill                                     # print the authoring guide for writing a script (markdown, free)
nodaro recast validate --file script.json [--json]      # free validation; exit code 1 while invalid
nodaro recast import --file script.json --rights-attested [--json]
#   imports a VALIDATED authored script as a completed analysis (free); --rights-attested asserts
#   the script is your own work — authored recasts render Faithful, exactly as written
nodaro recast estimate --analysis-job <id> [--fidelity faithful] [--resolution <r>] [--segment-sec <n>] [--json]
nodaro recast create --workflow <id> --analysis-job <id> [--rights-attested] [--fidelity faithful] [--resolution <r>] [--segment-sec <n>] [--json]
#   BUYS THE PLAN (credits) — run `estimate` first; returns the run id
nodaro recast start <recastId> [--segment-sec <n>] [--json]   # render a planned run (idempotent)
nodaro recast status <recastId> [--json]                # poll status + any pending interactive step

# Prompt — AI wizard that turns a rough idea into an optimized prompt
nodaro prompt wizard [--node-type <type>] [--prompt "…"] [--provider <name>] [--style <name>] [--aspect-ratio <ratio>] [--duration <seconds>] [--llm-model <id>] [--reasoning-effort <level>]   # interactive Q&A; node picker if --node-type omitted
nodaro prompt analyze --node-type <type> [--prompt "…"] [--provider <name>] [--style <name>] [--aspect-ratio <ratio>] [--duration <seconds>] [--llm-model <id>] [--reasoning-effort <level>] [--json]   # return guided questions
nodaro prompt generate --node-type <type> --selection category=value [--selection ...] [--original-prompt "…"] [--provider <name>] [--style <name>] [--aspect-ratio <ratio>] [--duration <seconds>] [--llm-model <id>] [--reasoning-effort <level>] [--json]   # build a prompt from selections
nodaro prompt enhance --node-type <type> --prompt "…" [--provider <name>] [--style <name>] [--aspect-ratio <ratio>] [--duration <seconds>] [--llm-model <id>] [--reasoning-effort <level>] [--json]   # one-shot rewrite, no questions
#   --reasoning-effort <level>: none|low|medium|high|xhigh|max (model-dependent; unsupported/omitted -> vendor default). xhigh/max bill one tier up. `--advanced` (Gemini models only) runs on the provider's own API so `--temperature` / `--max-tokens` / the full reasoning range apply, and also bills one tier up.

# Executions
nodaro executions get <id> [--watch] [--json]
nodaro executions cancel <id> [--mode cancelled|stopping]

# Jobs
nodaro jobs get <id> [--json]
nodaro jobs cancel <id> [--json]

# Generate Video Pro run control (segmented long-video engine; self-host runs it via the nodaro.ai connection)
nodaro video-pro stop <jobId> [--json]                     # graceful stop: keep + deliver completed segments, refund the rest
nodaro video-pro continue <jobId> [--from-segment N] [--watch] [--poll-interval <ms>] [--json]   # new job regenerating from segment N (default: first missing)

# Characters — full lifecycle + studio operations
nodaro characters list [--project <id>] [--archived] [--limit <n>] [--json]
nodaro characters get <id>
nodaro characters create --name <name> [--description "..."] [--gender <gender>] [--style realistic|anime|3d-pixar|illustration] [--base-outfit "..."] [--seed-prompt "..."] [--node-id <id>] [--project <id>]
nodaro characters update <id> [--name <name>] [--description "..."] [--gender <gender>] [--style realistic|anime|3d-pixar|illustration] [--base-outfit "..."] [--seed-prompt "..."]
nodaro characters delete <id>
nodaro characters restore <id>
nodaro characters duplicate <id> [--node-id <id>] [--project <id>]
nodaro characters usage <id>
nodaro characters generate <id> [--seed-prompt "..."] [--description "..."] [--name <name>] [--count 1|2|4] [--provider <p>] [--watch]
nodaro characters generate-asset <id> --asset-type expressions|poses|lighting|angles|headAngles|bodyAngles|custom --variant <name> [--user-prompt "..."] [--description "..."] [--column <col>] [--attach-name <name>] [--provider <p>] [--watch]
nodaro characters generate-motion <id> --motion-prompt "..." [--attach-name <name>] [--description "..."] [--motion-description "..."] [--provider <p>] [--watch]
nodaro characters approve-portrait <id> --job <jobId>
nodaro characters recaption <id>

# Locations — full lifecycle + studio operations
nodaro locations list [--archived] [--json]
nodaro locations get <id> [--json]
nodaro locations create <name> --node-id <id> [--description "..."] [--category indoor|outdoor|urban|nature|fantasy|sci-fi|historical|futuristic|other] [--style realistic|anime|3d-pixar|illustration] [--project <id>] [--json]
nodaro locations update <id> [--name <name>] [--description "..."] [--category <category>] [--style <style>] [--style-lock true|false] [--canonical-description "..."] [--expected-updated-at <iso>] [--json]
nodaro locations delete <id> [--json]
nodaro locations restore <id> [--json]
nodaro locations generate --name <name> [--description "..."] [--user-prompt "..."] [--category <category>] [--style <style>] [--provider <p>] [--count 1|2|4] [--attach-to-location-id <id>] [--watch] [--json]
nodaro locations generate-asset <id> --asset-type timeOfDay|weather|seasons|angles|lighting|custom --variant <name> [--user-prompt "..."] [--description "..."] [--column <col>] [--attach-name <name>] [--provider <p>] [--watch] [--json]
nodaro locations generate-motion --name <name> --motion-prompt "..." --source-image-url <url> [--provider kling|kling-turbo|kling-3.0|wan-i2v|wan-2.7-i2v|seedance-2] [--style realistic|anime|3d-pixar|illustration] [--canonical-description "..."] [--attach-to-location-id <id>] [--attach-name <name>] [--aspect-ratio 1:1|3:4|16:9|9:16] [--watch] [--json]
nodaro locations approve-main-image <id> --candidate-job-id <jobId> [--json]
nodaro locations recaption <id> [--json]

# Objects — full lifecycle + studio operations
nodaro objects list [--project <id>] [--archived] [--json]
nodaro objects get <id> [--json]
nodaro objects create <name> --node-id <id> [--description "..."] [--category furniture|vehicle|weapon|food|clothing|electronics|nature|tool|animal|other] [--style realistic|anime|3d-pixar|illustration] [--project <id>] [--json]
nodaro objects update <id> [--name <name>] [--description "..."] [--category <category>] [--style <style>] [--style-lock true|false] [--canonical-description "..."] [--expected-updated-at <iso>] [--json]
nodaro objects delete <id> [--permanent] [--json]          # --permanent erases archived rows; default is soft-delete
nodaro objects restore <id> [--json]
nodaro objects generate --name <name> [--description "..."] [--user-prompt "..."] [--category <category>] [--style <style>] [--provider <p>] [--count 1|2|4] [--attach-to-object-id <id>] [--seed-prompt-hint "..."] [--watch] [--json]
nodaro objects generate-asset --asset-type angles|materials|variations|motion|custom --variant <name> --attach-to-object-id <id> [--attach-to-column <col>] [--name <name>] [--description "..."] [--seed-prompt-hint "..."] [--watch] [--json]
nodaro objects generate-motion --name <name> --motion-prompt "..." --source-image-url <url> [--provider kling-turbo|kling|kling-3.0|minimax|hailuo-2.3|wan-i2v|seedance|bytedance-lite] [--style realistic|anime|3d-pixar|illustration] [--canonical-description "..."] [--attach-to-object-id <id>] [--attach-name <name>] [--aspect-ratio 1:1|3:4|16:9|9:16|4:3] [--seed-prompt-hint "..."] [--watch] [--json]
nodaro objects approve-main-image <id> --candidate-job-id <jobId> [--expected-updated-at <iso>] [--json]
nodaro objects recaption <id> [--json]

# Voice — revoice an audio track or a talking video
nodaro voice list [--clones] [--json]                    # the premade voice catalog (or your clones) — names/ids for --voice / --voices
nodaro voice changer --voice <id> --audio <url>|--video <url> [--model <id>] [--stability <0..1>] [--similarity <0..1>] [--style <0..1>] [--use-speaker-boost] [--seed <n>] [--remove-background-noise] [--watch] [--poll-interval <ms>] [--json]
nodaro voice change ...                                  # alias of `voice changer`
nodaro voice recast --audio <url>|--video <url> --voices <v1,v2,...>|--voices-json <json> [--model <id>] [--output video|stems] [--analysis-json <json>|--analysis-file <path>] [--no-preserve-background] [--separation-quality fast|best] [--music-volume-mode match|normalize|manual] [--music-volume <0-200>] [--remove-background-noise] [--voice-fx <preset>] [--voice-fx-mix <0-100>] [--voice-fx-delay <20-2000>] [--voice-fx-decay <0-1>] [--watch] [--poll-interval <ms>] [--json]
                                                         # multi-speaker recast (Voice Changer Pro; self-host runs it via the nodaro.ai connection). --voices maps speakers in detection order;
                                                         # the literal `keep` keeps that speaker's original voice (e.g. --voices Rachel,keep,Aria).
                                                         # --voices-json takes the raw SDK array: voice ids, per-voice settings objects, or null keep-slots.
                                                         # --output stems returns dry per-track stems for an interactive mix (render with `voice export`).
nodaro voice pro ...                                     # alias of `voice recast`
nodaro voice analyze --audio <url>|--video <url> [--separation-quality fast|best] [--suggest-title] [--watch] [--poll-interval <ms>] [--json]
                                                         # detect the speakers WITHOUT recasting (Cloud only) — prints the speaker list on --watch.
                                                         # Save the completed job's output_data and pass it to `voice recast --analysis-file` to skip re-detection.
nodaro voice export --source <videoUrl> --tracks-json <json>|--tracks-file <path> [--voice-fx <preset>] [--voice-fx-mix <0-100>] [--voice-fx-delay <20-2000>] [--voice-fx-decay <0-1>] [--watch] [--poll-interval <ms>] [--json]
                                                         # render the final video from a mixed track set (Cloud only) — tracks are { url, gain 0-200, muted, kind? } lanes
                                                         # from a `voice recast --output stems` job; the video stream is copied, never re-encoded.
nodaro voice design --text <line> --description <desc> [--model <id>] [--loudness <-1..1>] [--guidance-scale <0-100>] [--seed <n>] [--quality <n>] [--enhance] [--user-prompt <text>] [--watch] [--poll-interval <ms>] [--json]
                                                         # design a brand-new synthetic voice from a text description
nodaro voice remix --text <text> --description <desc> [--user-prompt <text>] [--watch] [--poll-interval <ms>] [--json]
                                                         # speak a text in a described voice, without cloning
nodaro voice dub --audio <url> --target-language <code> [--source-language <code>] [--num-speakers <1-20>] [--disable-voice-cloning] [--drop-background-audio] [--watch] [--poll-interval <ms>] [--json]
                                                         # dub into another language while preserving each speaker's voice
nodaro voice clones list [--json]                        # clones made before cloning was retired
nodaro voice clones delete <id> [--json]

# Media — ingestion + compositing: social-video import, trim, caption burn-in, still-to-video, slideshow, image collage, image overlay, save-to-storage, metadata probe
nodaro media download <url> [--max-height <px>] [--section <a-b>] [--watch] [--json]
                                                         # YouTube / TikTok / Instagram / X / Facebook → your storage. --section fetches only
                                                         # that time range (seconds). --watch streams live progress (no job to poll later).
nodaro media metadata <url> [--json]                     # probe duration/dimensions/title WITHOUT downloading
nodaro media trim-video --video <url> --start <sec> --end <sec>|--keep-first <sec>|--keep-last <sec> [--watch] [--poll-interval <ms>] [--json]
nodaro media trim-audio --video <url>|--audio <url> [--start <sec>] [--end <sec>] [--format mp3|wav|aac] [--watch] [--poll-interval <ms>] [--json]
nodaro media add-captions <videoUrl> [--text <text>] [--captions-file <file.json>] [--style subtitle|word-highlight|karaoke|tiktok-words|word-pop|bouncy] [--look outline|clean] [--position bottom|top|center] [--position-y <pct>] [--font-size <px>] [--font-family <name>] [--font-weight <100-900>] [--color <c>] [--background-color <c>] [--stroke-color <c>] [--stroke-width <px>] [--highlight-color <c>] [--uppercase|--no-uppercase] [--animate|--no-animate] [--no-auto-transcribe] [--transcribe-provider <lane>] [--segments-file <file.json>] [--watch] [--poll-interval <ms>] [--json]
                                                         # burn captions in. `subtitle` is static (FFmpeg drawtext); the kinetic styles render
                                                         # via Remotion. The STYLING levers (--look, --font-family, --font-weight, --stroke-*,
                                                         # --uppercase, --position-y) now ALSO style a `subtitle` — a styled subtitle renders via
                                                         # Remotion and bills at the kinetic price; a bare plain-text subtitle stays on the cheap
                                                         # path. Only --highlight-color and --animate are kinetic-only (rejected on subtitle).
                                                         # --animate is on by default; --no-animate freezes the per-word motion (grouping +
                                                         # highlight colour stay). On the kinetic styles an unset --look renders as `outline`.
                                                         # --captions-file is a JSON array of word-timed entries [{ text, startMs, endMs }] — one
                                                         # per WORD for the kinetic styles, and an `audio transcribe` job's output_data.words drops
                                                         # in verbatim (pair it with --no-auto-transcribe). --segments-file gives non-overlapping
                                                         # ranges their own style/look/position.
nodaro media still-to-video --image <url> --audio <url> [--motion none|zoom-in|zoom-out|pan-left|pan-right|ken-burns] [--intensity <1-10>] [--resolution 720p|1080p|4K] [--aspect-ratio <W:H>] [--fps 24|30] [--fit cover|contain] [--pad-color <hex>] [--watch] [--poll-interval <ms>] [--json]
                                                         # one still + one audio → MP4, local FFmpeg, 0 credits. Length = the audio's length
                                                         # (no duration flag by design). --motion animates the still.
nodaro media slideshow --images <url...> [--audio <url>] [--durations "10,auto,auto"] [--per-image <sec>] [--transition <id>] [--transition-duration <sec>] [--motion none|zoom-in|zoom-out|ken-burns|alternate] [--intensity <1-10>] [--resolution 720p|1080p|4K] [--aspect-ratio <W:H>] [--fps 24|30] [--fit cover|contain] [--pad-color <hex>] [--watch] [--json]
                                                         # 2-100 images + optional audio → MP4 slideshow, 0 credits. Audio wired = it sets the
                                                         # length (pins scale proportionally, disclosed); no audio = silent, N × per-image.
nodaro media collage <imageUrls...> [--sizes <0-3,...>] [--numbered] [--label <text>...] [--badge-position top-left|top-right] [--layout smart|grid] [--resolution 2K|4K] [--aspect-ratio <W:H>] [--gap <px>] [--background-color <hex>] [--watch] [--poll-interval <ms>] [--json]
                                                         # composite 2–30 images into one large image. --sizes gives per-image RELATIVE size
                                                         # hints aligned by position (0 auto, 1 big ≈2× linear, 2 medium, 3 small ≈½ — smart layout only).
                                                         # --numbered stamps 1-based sequence numbers at each image's corner (storyboard mode);
                                                         # --badge-position picks the corner for numbers and labels: top-left (default) or top-right.
                                                         # --label sets a per-image caption shown after the number; repeat once per image, in order ("" skips one, aligned with the image args).
nodaro media overlay <imageUrl> [layerUrls...] [--layers-file <path>] [--anchor <anchor>] [--x <%>] [--y <%>] [--width <%>] [--opacity <0-1>] [--platform <id>...] [--qr-text <text>] [--mask-mode none|layers|around|outside] [--mask-spread <px>] [--canvas <WxH>] [--base-fit contain|cover] [--background-color <hex>] [--output-format png|jpg|webp] [--watch] [--poll-interval <ms>] [--json]
                                                         # place up to 12 layers on a base image, pixel-exactly (local sharp, no AI). Positional URLs are
                                                         # image layers and share --anchor/--x/--y/--width/--opacity (the watermark case); --layers-file
                                                         # takes the full JSON array — per-layer options and the text / QR / shape kinds.
                                                         # Placement is in PERCENT of the base image, so one call fits a 1K preview and a 4K render.
                                                         # --platform (repeatable) also renders the composite at that platform's size (2 credits each
                                                         # on top of the 10-credit base); the job's output carries them as variants[].
nodaro media overlay-placement <imageUrl> [--intent <text>] [--aspect <ratio>] [--safe-area <x,y,w,h>] [--json]
                                                         # asks a vision model WHERE one layer should go and answers in the same percent units —
                                                         # anchor, x, y, width + a one-line reason. Synchronous (no job to poll); one image-to-text call.
nodaro media save <url> [--filename <name>] [--type image|video|audio] [--watch] [--poll-interval <ms>] [--json]

# Audio — the primitives Voice Changer Pro composes, standalone, plus speech-to-text
nodaro audio separate --audio <url> [--mode vocal_instrumental|stems] [--quality auto|fast|best] [--watch] [--poll-interval <ms>] [--json]
nodaro audio isolate --audio <url> [--watch] [--poll-interval <ms>] [--json]
nodaro audio fx --audio <url> [--preset <preset>] [--mix <0-100>] [--delay <20-2000>] [--decay <0-1>] [--eq-low <db>] [--eq-high <db>] [--watch] [--poll-interval <ms>] [--json]
nodaro audio mix --audio <url> --audio <url> ... [--volumes <csv>] [--watch] [--poll-interval <ms>] [--json]
nodaro audio adjust-volume --audio <url>|--video <url> [--volume <0-200>] [--normalize] [--fade-in <sec>] [--fade-out <sec>] [--watch] [--poll-interval <ms>] [--json]
nodaro audio combine --segment <url[@a-b]> --segment ... [--watch] [--poll-interval <ms>] [--json]
nodaro audio transcribe --audio <url> [--provider elevenlabs-stt] [--language <code>] [--diarize] [--tag-audio-events] [--word-timestamps] [--watch] [--poll-interval <ms>] [--json]
                                                         # speech → text. --provider elevenlabs-stt is always word-level and is the lane that
                                                         # honours --diarize / --tag-audio-events; OMITTING --provider runs the legacy whisper
                                                         # lane, which has no word timings (--word-timestamps is refused there, before credits).
                                                         # The completed job's output_data carries text, words (one per word, in MILLISECONDS)
                                                         # and json (the normalized transcript, also ms); top-level segments (SECONDS) exist only
                                                         # on the legacy lanes — elevenlabs-stt returns none, so read words.
                                                         # Feed output_data.words to `media add-captions --captions-file` for a kinetic render.

# Edit — editorial primitives for podcast / long-form video
nodaro edit silence-detect <audioUrl> [--threshold-db=-35] [--min-silence-ms <ms>] [--pad-ms <ms>] [--watch] [--poll-interval <ms>] [--json]
nodaro edit apply-edl --edl <file.json> [--transcript <file.json>] [--source <url> ...] [--output video|audio] [--quality proxy|final] [--crossfade-ms <ms>] [--watch] [--poll-interval <ms>] [--json]
nodaro edit plan --mode tighten|clips|chapters --plan-tier economy|standard|premium --transcript <file.json> (--source <url[@audio|@video]> ... | --sources-file <file.json>) [--silence <file.json>] [--instructions <text>] [--style-guide <text>] [--count <n>] [--target-duration-sec <n>] [--target-aspect 16:9|9:16|1:1|4:5] [--platform <name>] [--watch] [--poll-interval <ms>] [--json]

# Organizations — only on instances that have them
nodaro org list [--json]
nodaro org get <id> [--json]
nodaro org create --name <name> --kind school|team [--slug <slug>] [--accept-terms] [--json]
nodaro org members <orgId> [--limit <n>] [--cursor <token>] [--json]
nodaro org invite <orgId> --email a@x.com --email b@x.com [--role admin|member] [--workspace <id>] [--workspace-role admin|member] [--json]
nodaro org invitations <orgId> [--status open|accepted|revoked|expired] [--limit <n>] [--cursor <token>] [--json]
nodaro org revoke <invitationId> [--json]
nodaro org audit <orgId> [--limit <n>] [--cursor <token>] [--json]
nodaro org usage <orgId> [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--tz <iana>] [--group-by workspace|member|model|day|none] [--workspace <id>] [--user <id>] [--limit <n>] [--cursor <token>] [--csv] [--json]

# Workspaces — where work lands
nodaro workspace list [--json]
nodaro workspace current [--json]
nodaro workspace use <id> [--json]
nodaro workspace clear
nodaro workspace get <id> [--json]
nodaro workspace members <id> [--limit <n>] [--cursor <token>] [--json]
nodaro workspace join <code> [--json]
nodaro workspace usage [id] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--tz <iana>] [--group-by member|model|day|none] [--user <id>] [--limit <n>] [--cursor <token>] [--csv] [--json]

Working in a workspace

Instances with organizations put work inside a workspace. A browser shows the current one on every screen; a terminal shows nothing, so the CLI makes it explicit.

Three ways to say which workspace a command acts in, each beating the one below it:

nodaro --workspace <id> workflows list   # 1. this command only
export NODARO_WORKSPACE=<id>             # 2. this shell, or this CI job
nodaro workspace use <id>                # 3. saved on the profile, until changed

nodaro workspace current answers “where am I”, and names which of the three decided — an inherited NODARO_WORKSPACE looks exactly like a saved one otherwise.

nodaro workspace use verifies the workspace before saving it, so a typo fails once, here, rather than on every later command.

The workspace decides which workspace a list reads from and where a create lands. It never decides access: reading, updating, deleting or running something you name by id is governed by that object’s own workspace. So a forgotten --workspace cannot hide your work, and a wrong one cannot reach anyone else’s — it just wastes a command.

With none set, everything happens in your personal space. On an instance without organizations these commands are simply unavailable, and the header is never sent.

Inviting people

nodaro org invite is the reason this group exists: bringing a class or a team onto an instance is a bulk, one-off, scriptable job.

nodaro org invite org_abc --email ada@school.edu --email grace@school.edu   --workspace ws_xyz --workspace-role member

It prints one line per address. An install with no mail provider — every fresh self-host — cannot email anybody, and those rows come back with a link instead:

✓ ada@school.edu — emailed
⚠ grace@school.edu — not emailed (link_only); send this link yourself:
  https://app.example.com/invite/9f3c…

Send those links yourself, or the invitation exists and nobody can reach it.

Usage reports

nodaro org usage and nodaro workspace usage answer who spent how much on what, over a date range. The default prints a table with a totals line; --json emits the full report, and --csv writes the CSV to stdout so it pipes to a file. Dates are inclusive YYYY-MM-DD; --tz defaults to your terminal’s zone.

nodaro org usage org_abc --from 2026-09-01 --to 2026-09-30 --group-by member --csv > september.csv

The interactive recast flow (analyze → recast stems → export)

The one-shot voice recast renders a finished video in a single call. The three-step flow instead lets you inspect the speakers first and mix the result before rendering:

# 1. Detect the speakers (prints the list; save output_data for the fast-path)
nodaro voice analyze --video https://.../panel.mp4 --watch --json > analyze.json
jq .output_data analyze.json > analysis.json

# 2. Recast to dry stems, reusing the analysis (no re-detection)
nodaro voice recast --video https://.../panel.mp4 --voices Rachel,keep,Aria \
  --analysis-file analysis.json --output stems --watch

# 3. Set levels/mutes per lane and render (video stream-copied, bit-identical)
nodaro voice export --source https://.../panel.mp4 --tracks-file mix.json \
  --voice-fx hall --voice-fx-mix 25 --watch

Param syntax

Both --input (apps) and --param (nodes) accept repeated key=value pairs with primitive coercion:

nodaro nodes run generate-image \
  --param prompt="a futuristic city skyline at dusk" \
  --param provider=flux \
  --param resolution=2K \
  --param generateAudio=false

For arrays, nested objects, or any value that doesn’t fit the flag form, use --params-file body.json. Flag values override file values for the same key.

Input/source nodes run the same way. Meta Ads (meta-ads-scrape) pulls public Facebook + Instagram ads from Meta’s Ad Library and answers synchronously (the result carries the ad array directly):

# By keyword
nodaro nodes run meta-ads-scrape \
  --param mode=search --param query="running shoes" --param count=20 --param period=30d

# By advertiser name(s) — resolved to Facebook Pages server-side; use a file for the array
echo '{"mode":"pages","advertiserNames":["Nike","Adidas"],"count":30}' > body.json
nodaro nodes run meta-ads-scrape --params-file body.json

Needs APIFY_API_TOKEN on the server, or a connected nodaro.ai account (the scrape is relayed and billed there).

Output formatting

Every read command supports --json for machine-readable output:

nodaro projects list --json | jq '.[].id'
nodaro workflows run wf_abc --json

Without --json the output is a small ASCII table for list commands and a pretty-printed JSON block for get commands.

Exit codes

Code Meaning
0 success
1 unauthorized / not found / argument error / network error
2 --watch finished and the execution ended in failed
3 --watch stopped because the job entered pending_review — a human decision is pending, not a failure
130 --watch finished and the execution ended in cancelled

--json prints the payload and returns instead of setting 2 / 3 / 130 — branch on .status in that mode.

Exit 3 — a job held for review. On a deployment that registers a job policy, a job can enter pending_review: the work is done, the credits stay reserved, and a human is deciding whether the result is released. That status is in-flight but does not move on its own, so --watch stops on it, prints awaiting review (a human decision is pending; not a failure), and exits 3 rather than polling until you kill the process. Do not re-run the request — a duplicate would be held too. Check back with nodaro jobs get <id>: it resolves to completed (approved), failed (rejected — error_hint.kind is policy-block and error_hint.reason is the text to show) or cancelled.

nodaro nodes run generate-image --param prompt="…" --watch
case $? in
  0) echo "released" ;;
  3) echo "awaiting review — check back later" ;;
  *) echo "failed" ;;
esac

Examples

Run a workflow nightly with cron

0 3 * * * /usr/local/bin/nodaro workflows run wf_abc123 --json >> /var/log/nodaro-nightly.log 2>&1

Wait for completion and gate downstream work

nodaro workflows run wf_abc --watch && \
  echo "shipped" | mail -s "nodaro run done" me@example.com

Generate an image from a single command

nodaro nodes run generate-image \
  --param prompt="a snow leopard on a mountain ridge, cinematic" \
  --param provider=flux \
  --param resolution=2K \
  --watch --json | jq -r '.output_data.imageUrl'

Transcribe a track, then burn its words in as kinetic captions

# 1. word-level transcription (elevenlabs-stt is always word-level)
nodaro audio transcribe --audio https://example.com/talk.mp3 \
  --provider elevenlabs-stt --watch

# 2. take the words (MILLISECONDS; the top-level segments are in seconds)
nodaro jobs get <jobId> --json | jq '.output_data.words' > words.json

# 3. burn them in — supplying captions means add-captions runs no STT of its own
nodaro media add-captions https://example.com/talk.mp4 \
  --captions-file words.json \
  --style word-highlight --no-auto-transcribe --watch

Correct a word’s text in words.json between steps 2 and 3 and the correction is what burns in.

Wrap an app’s prompt with hidden text for one run

nodaro apps run hair-styler-dd3erw --input prompt="curly red hair" \
  --override n1.promptPrefix="Studio portrait of" --override n1.promptSuffix=", 85mm, soft light"

See Prompt pre & post text.

Turn a rough idea into an optimized prompt

# one-shot enhance, then feed straight into a node run
PROMPT=$(nodaro prompt enhance \
  --node-type generate-image \
  --prompt "snow leopard" \
  --json | jq -r '.prompt')

nodaro nodes run generate-image --param prompt="$PROMPT" --watch

Need the wizard’s guided questions in a script? Use the two-step path — prompt analyze --json to fetch the questions, then prompt generate --selection category=value … to build the final prompt (the interactive prompt wizard requires a terminal).

Programmatic alternative

If you’re building integrations, prefer the typed SDK directly:

npm install @nodaro/sdk
import { createClient, StaticTokenAuth } from "@nodaro/sdk"

const nodaro = createClient({
  baseUrl: "https://app.nodaro.ai",
  auth: new StaticTokenAuth(process.env.NODARO_TOKEN!),
})

const exec = await nodaro.workflows.run("wf_abc")

The CLI is a thin convenience wrapper around @nodaro/sdk. Anything the CLI does, the SDK does too.

See also