use clap::{Parser, Subcommand, ValueEnum};
const HELP_FOOTER: &str = "\
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 lyrics` is 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 generate --title \"Weekend Code\" --tags \"indie rock, upbeat\" --lyrics-file lyrics.txt --wait --download ./songs/
Generate a song and download the finished MP3 (lyrics embedded)
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 = "Suno AI music generation CLI — 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 {
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>,
}
#[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"));
}
}
}