use clap::{Parser, Subcommand, ValueEnum};
const HELP_FOOTER: &str = "\
Which creation command:
write Compose the song — style prompt + lyric skeleton you fill (free)
generate Render audio from lyrics you already have (costs credits)
describe Render audio from a one-line description; Suno writes the lyrics
lyrics Lyrics text only, no audio (free)
Tips:
• First run: `suno auth --login`, then `suno doctor` to verify the setup
• Output is a JSON envelope automatically when piped; force with --json
• `suno write` and `suno lyrics` are free; generation costs ~70 credits per call on v5.5
• Exit codes: 0 ok, 1 transient (retry), 2 config/auth, 3 bad input, 4 rate limited
• Config: `suno config path` shows the file; SUNO_* env vars override it
• Full machine-readable manifest: `suno agent-info | jq`
Examples:
suno write --genre \"indie rock\" --theme \"late-night city drives\" --vocal male --out song.txt
# fill the <...> lyric slots in song.txt, then run the generate command `write` printed:
suno generate --title \"Night Drive\" --tags \"Indie rock, ...\" --lyrics-file song.txt --wait --download ./songs/
The full flow: compose, fill, render, download (lyrics embedded in the MP3)
suno describe --prompt \"a chill lo-fi track about rainy mornings\" --wait --download ./
Let Suno write the lyrics from a description
suno list | jq -r '.data.clips[].id'
List your library as JSON and extract clip IDs
suno timed-lyrics <clip_id> --lrc > song.lrc
Word-level synced lyrics in LRC format (raw even when piped)
suno cover <clip_id> --tags \"jazz, smooth piano\" --wait
Re-imagine an existing clip in a new style";
#[derive(Parser)]
#[command(
name = "suno",
version,
about = "Write, generate, and manage Suno music — v5.5 support",
after_long_help = HELP_FOOTER
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub quiet: bool,
}
#[derive(Subcommand)]
pub enum Commands {
Write(WriteArgs),
Generate(GenerateArgs),
Describe(DescribeArgs),
Lyrics(LyricsArgs),
Extend(ExtendArgs),
Concat(ConcatArgs),
Cover(CoverArgs),
Remaster(RemasterArgs),
Stems(StemsArgs),
Info(InfoArgs),
Persona(PersonaArgs),
#[command(visible_alias = "ls")]
List(ListArgs),
Search(SearchArgs),
Status(StatusArgs),
#[command(visible_alias = "dl")]
Download(DownloadArgs),
#[command(visible_alias = "rm")]
Delete(DeleteArgs),
Set(SetArgs),
Publish(PublishArgs),
TimedLyrics(TimedLyricsArgs),
Credits,
Models,
Auth(AuthArgs),
Config(ConfigArgs),
Doctor,
AgentInfo,
#[command(visible_alias = "guides")]
Guide(GuideArgs),
Skill(SkillArgs),
#[command(hide = true)]
InstallSkill(InstallSkillArgs),
Update(UpdateArgs),
#[command(hide = true)]
Contract {
code: i32,
},
}
#[derive(clap::Args)]
pub struct UpdateArgs {
#[arg(long)]
pub check: bool,
#[arg(long)]
pub force: bool,
}
#[derive(clap::Args)]
pub struct GuideArgs {
pub name: Option<String>,
}
const WRITE_HELP: &str = "\
Tips:
• --out FILE is the way to get an editable lyrics file: it holds the lyric block ONLY,
so it feeds `suno generate --lyrics-file` directly. Title/style/tags go to stderr and JSON
• Shell redirection (`suno write > song.txt`) receives the JSON envelope, not lyrics —
output is a JSON envelope whenever stdout is not a terminal. Use --out for the lyrics
• --project-out FILE writes the full human document (title + style prompt + tags + artefact)
• Fill the <...> placeholders before generating; `suno generate` rejects unresolved ones
• Genres are fuzzy/case-insensitive; an unknown genre becomes a raw style tag (never fails)
• --viral adds earworm/hook meta-tags; --instrumental drops all vocals and lyric slots and
emits a generate command with --instrumental
• --mode priming requires --target, --objective and --domain (see `suno guide priming`)
Examples:
suno write --theme \"late-night coding\" --genre indie-rock --vocal male --out song.txt
Scaffold to song.txt; fill the <...> slots, then run the printed generate command
suno write --theme \"summer love\" --genre pop --viral --title \"Golden Hour\" --out song.txt
A pop song with earworm hook tags baked into the style prompt
suno write --genre lo-fi --instrumental --out beat.txt
Instrumental lo-fi scaffold — no lyric slots, generatable as written
suno write --mode priming --domain investment --target \"batch: seed investors\" \\
--objective \"increase recall of fund X\" --subtlety medium --out song.txt
Priming research scaffold (chill lounge, 72 BPM) with a Prime-Stack Map template";
#[derive(clap::Args)]
#[command(after_long_help = WRITE_HELP)]
pub struct WriteArgs {
#[arg(long)]
pub theme: Option<String>,
#[arg(long)]
pub genre: Option<String>,
#[arg(long)]
pub mood: Option<String>,
#[arg(long)]
pub vocal: Option<VocalGender>,
#[arg(long)]
pub bpm: Option<u32>,
#[arg(long)]
pub viral: bool,
#[arg(long)]
pub instrumental: bool,
#[arg(long)]
pub title: Option<String>,
#[arg(long, value_enum, default_value_t = WriteMode::Songwriting)]
pub mode: WriteMode,
#[arg(long)]
pub target: Option<String>,
#[arg(long)]
pub objective: Option<String>,
#[arg(long)]
pub domain: Option<String>,
#[arg(long)]
pub subtlety: Option<String>,
#[arg(long)]
pub out: Option<String>,
#[arg(long)]
pub project_out: Option<String>,
#[arg(long, default_value = "./")]
pub download: String,
}
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum WriteMode {
#[default]
Songwriting,
Priming,
}
impl WriteMode {
pub fn as_str(&self) -> &'static str {
match self {
Self::Songwriting => "songwriting",
Self::Priming => "priming",
}
}
}
#[derive(clap::Args)]
pub struct SkillArgs {
#[command(subcommand)]
pub action: SkillAction,
}
#[derive(Subcommand)]
pub enum SkillAction {
Install,
Status,
}
#[derive(clap::Args)]
pub struct GenerateArgs {
#[arg(short, long)]
pub title: Option<String>,
#[arg(long)]
pub tags: Option<String>,
#[arg(long)]
pub exclude: Option<String>,
#[arg(short, long, conflicts_with = "lyrics_file")]
pub lyrics: Option<String>,
#[arg(long)]
pub lyrics_file: Option<String>,
#[arg(short, long)]
pub model: Option<ModelVersion>,
#[arg(long)]
pub vocal: Option<VocalGender>,
#[arg(long)]
pub weirdness: Option<f64>,
#[arg(long)]
pub style_influence: Option<f64>,
#[arg(long)]
pub audio_influence: Option<f64>,
#[arg(long)]
pub instrumental: bool,
#[arg(long)]
pub force: bool,
#[arg(short, long)]
pub wait: bool,
#[arg(long)]
pub download: Option<String>,
#[arg(long)]
pub token: Option<String>,
#[arg(long)]
pub no_captcha: bool,
#[arg(long)]
pub persona: Option<String>,
}
#[derive(clap::Args)]
pub struct DescribeArgs {
#[arg(short, long)]
pub prompt: String,
#[arg(long)]
pub tags: Option<String>,
#[arg(short, long)]
pub model: Option<ModelVersion>,
#[arg(long)]
pub vocal: Option<VocalGender>,
#[arg(long)]
pub weirdness: Option<f64>,
#[arg(long)]
pub style_influence: Option<f64>,
#[arg(long)]
pub instrumental: bool,
#[arg(long)]
pub force: bool,
#[arg(short, long)]
pub wait: bool,
#[arg(long)]
pub download: Option<String>,
#[arg(long)]
pub token: Option<String>,
#[arg(long)]
pub no_captcha: bool,
#[arg(long)]
pub persona: Option<String>,
}
#[derive(clap::Args)]
pub struct LyricsArgs {
#[arg(short, long)]
pub prompt: String,
}
#[derive(clap::Args)]
pub struct ExtendArgs {
pub clip_id: String,
#[arg(long)]
pub at: f64,
#[arg(long)]
pub lyrics: Option<String>,
#[arg(long)]
pub tags: Option<String>,
#[arg(short, long)]
pub model: Option<ModelVersion>,
#[arg(long)]
pub token: Option<String>,
#[arg(long)]
pub no_captcha: bool,
#[arg(long)]
pub force: bool,
#[arg(short, long)]
pub wait: bool,
}
#[derive(clap::Args)]
pub struct ConcatArgs {
pub clip_id: String,
}
#[derive(clap::Args)]
pub struct CoverArgs {
pub clip_id: String,
#[arg(long)]
pub tags: Option<String>,
#[arg(short, long)]
pub model: Option<ModelVersion>,
#[arg(long)]
pub audio_influence: Option<f64>,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub token: Option<String>,
#[arg(long)]
pub no_captcha: bool,
#[arg(short, long)]
pub wait: bool,
#[arg(long)]
pub download: Option<String>,
}
#[derive(clap::Args)]
pub struct RemasterArgs {
pub clip_id: String,
#[arg(long, default_value = "v5.5")]
pub model: RemasterModel,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub token: Option<String>,
#[arg(long)]
pub no_captcha: bool,
#[arg(short, long)]
pub wait: bool,
#[arg(long)]
pub download: Option<String>,
}
#[derive(clap::Args)]
pub struct InfoArgs {
pub id: String,
}
#[derive(clap::Args)]
pub struct PersonaArgs {
pub id: String,
}
#[derive(clap::Args)]
pub struct StemsArgs {
pub clip_id: String,
}
#[derive(clap::Args)]
pub struct ListArgs {
#[arg(long)]
pub cursor: Option<String>,
}
#[derive(clap::Args)]
pub struct SearchArgs {
pub query: String,
}
#[derive(clap::Args)]
pub struct DeleteArgs {
pub ids: Vec<String>,
#[arg(short = 'y', long)]
pub yes: bool,
}
#[derive(clap::Args)]
pub struct StatusArgs {
#[arg(required = true, num_args = 1..)]
pub ids: Vec<String>,
}
#[derive(clap::Args)]
pub struct DownloadArgs {
#[arg(required = true, num_args = 1..)]
pub ids: Vec<String>,
#[arg(short, long)]
pub output: Option<String>,
#[arg(long)]
pub video: bool,
}
#[derive(clap::Args)]
pub struct SetArgs {
pub id: String,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub lyrics: Option<String>,
#[arg(long)]
pub lyrics_file: Option<String>,
#[arg(long)]
pub caption: Option<String>,
#[arg(long)]
pub remove_cover: bool,
}
#[derive(clap::Args)]
pub struct PublishArgs {
#[arg(required = true, num_args = 1..)]
pub ids: Vec<String>,
#[arg(long)]
pub private: bool,
}
#[derive(clap::Args)]
pub struct TimedLyricsArgs {
pub id: String,
#[arg(long)]
pub lrc: bool,
}
#[derive(clap::Args)]
pub struct AuthArgs {
#[arg(long)]
pub login: bool,
#[arg(long)]
pub refresh: bool,
#[arg(long)]
pub jwt: Option<String>,
#[arg(long)]
pub cookie: Option<String>,
#[arg(long)]
pub device: Option<String>,
#[arg(long)]
pub logout: bool,
}
#[derive(clap::Args)]
pub struct ConfigArgs {
#[command(subcommand)]
pub action: ConfigAction,
}
#[derive(clap::Args)]
pub struct InstallSkillArgs {
#[arg(long)]
pub path: Option<String>,
#[arg(short, long)]
pub force: bool,
#[arg(long)]
pub print: bool,
}
#[derive(Subcommand)]
pub enum ConfigAction {
Show,
Set { key: String, value: String },
Path,
Check,
}
#[derive(ValueEnum, Clone, Debug, Default)]
pub enum ModelVersion {
#[value(name = "v5.5")]
#[default]
V55,
#[value(name = "v5")]
V5,
#[value(name = "v4.5+")]
V45Plus,
#[value(name = "v4.5-all")]
V45All,
#[value(name = "v4.5")]
V45,
#[value(name = "v4")]
V4,
#[value(name = "v3.5")]
V35,
#[value(name = "v3")]
V3,
#[value(name = "v2")]
V2,
}
impl ModelVersion {
pub fn to_api_key(&self) -> &'static str {
match self {
Self::V55 => "chirp-fenix",
Self::V5 => "chirp-crow",
Self::V45Plus => "chirp-bluejay",
Self::V45All => "chirp-auk-turbo",
Self::V45 => "chirp-auk",
Self::V4 => "chirp-v4",
Self::V35 => "chirp-v3-5",
Self::V3 => "chirp-v3-0",
Self::V2 => "chirp-v2-xxl-alpha",
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::V55 => "v5.5",
Self::V5 => "v5",
Self::V45Plus => "v4.5+",
Self::V45All => "v4.5-all",
Self::V45 => "v4.5",
Self::V4 => "v4",
Self::V35 => "v3.5",
Self::V3 => "v3",
Self::V2 => "v2",
}
}
}
#[derive(ValueEnum, Clone, Debug)]
pub enum VocalGender {
Male,
Female,
}
#[derive(ValueEnum, Clone, Debug, Default)]
pub enum RemasterModel {
#[value(name = "v5.5")]
#[default]
V55,
#[value(name = "v5")]
V5,
#[value(name = "v4.5+")]
V45Plus,
}
impl RemasterModel {
pub fn to_api_key(&self) -> &'static str {
match self {
Self::V55 => "chirp-flounder",
Self::V5 => "chirp-carp",
Self::V45Plus => "chirp-bass",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::ValueEnum;
#[test]
fn model_versions_map_to_api_keys() {
assert_eq!(ModelVersion::V45All.to_api_key(), "chirp-auk-turbo");
assert_eq!(ModelVersion::V55.to_api_key(), "chirp-fenix");
for m in ModelVersion::value_variants() {
assert!(m.to_api_key().starts_with("chirp"));
let clap_name = m.to_possible_value().unwrap().get_name().to_string();
assert_eq!(m.display_name(), clap_name);
}
}
#[test]
fn remaster_models_map_to_api_keys() {
for m in RemasterModel::value_variants() {
assert!(m.to_api_key().starts_with("chirp"));
}
}
}