feat(suno): add suno.com music-generation adapter#1638
Open
ele-yufo wants to merge 4 commits into
Open
Conversation
A new browser-based adapter for Suno (https://suno.com), the AI music generation service. Targets the /api/generate/v2-web/ schema introduced in 2026-05 and the cookie-isolation-aware Bearer auth path. ## Commands - `opencli suno status` — login state, plan, credit breakdown, captcha - `opencli suno list` — list recent clips in the user's library - `opencli suno generate` — Simple or Custom mode generation + download - `opencli suno download` — fetch an existing clip by id in MP3/M4A/WAV/cover Each generate request returns 2 candidate clips (Suno's native A/B). All formats land alongside a metadata JSON dump. ## Custom mode controls Matches the web UI exactly: - `--lyrics` + `--tags` + `--negative-tags` + `--title` - `--weirdness` (0..1) → `metadata.control_sliders.weirdness_constraint` - `--style-weight` (0..1) → `metadata.control_sliders.style_weight` - `--instrumental`, `--model` (chirp-fenix / chirp-bluejay / v4 / v3-5) ## Paid-format guard WAV downloads charge against the user's account (the Suno UI's "Download → WAV" flow makes the same `billing/clips/{id}/download/` call). Both `generate` and `download` skip WAV by default and require `--confirm-paid true` to charge — surfaced in the result row as `skipped(needs --confirm-paid):wav` rather than silently dropping it. ## Auth notes Studio API rejects the OpenCLI bridge's `credentials: 'include'` cross-origin fetch (third-party cookie isolation in the eval context). The adapter explicitly pulls the live Clerk JWT via `await window.Clerk.session.getToken()` and sends it as `Authorization: Bearer`, alongside the `browser-token` (anti-replay timestamp) and `device-id` (from the `suno_device_id` cookie) headers that the studio API also requires. This matches the wire format observed in a real `/api/generate/v2-web/` request capture. ## Validation - `npx tsc --noEmit` — clean - `opencli validate suno` — PASS, 0 errors, 0 warnings, 4 commands - `npx vitest run --project adapter clis/suno/` — 47 tests pass (4 files) - `npm test` — 3811 passing across 380 files - Live (Pro account, 2 credits/song): two-clip generation completes, MP3 (796 KB, 64 kbps 48 kHz), M4A (603 KB opus), and WAV (6.7 MB PCM 16-bit 48 kHz) all download intact; cover JPEG + metadata JSON also written. ## Known gaps - **Stems extraction (12-track separation)** — schema known from the capture (`task: 'gen_stem'`, `stem_type_id: 91`, `stem_task: 'twelve'`, `continue_clip_id: <source>`), but not yet wired. Happy to follow up in a separate PR if reviewers want it included; stems are paid and the design space (one-shot vs poll for 12 separate child clips) is worth its own review pass. - **Captcha solving** — `/api/c/check` is checked pre-flight and the command fails fast with a clear instruction if Suno requires a challenge. Solving captchas headlessly is out of scope.
CI on the previous commit failed on two checks: - `build (ubuntu-latest)` → cli-manifest.json was out of sync with the source; I had run `git checkout -- cli-manifest.json` before pushing, which dropped the manifest entries for the four new suno commands. This regenerates the manifest via `npm run build`. - `doc-coverage --strict` → suno was the only adapter without a matching `docs/adapters/<mode>/suno.md`. Add a browser-mode page covering all four commands, options, behaviour notes, and the Clerk-Bearer + browser-token / device-id auth quirk. Doc coverage now reports `146/146 adapters documented`. No source changes — adapter behaviour is unchanged.
The convention-audit CI gate scans object literals returned anywhere
in an adapter — including inner page.evaluate IIFE returns — and
treats any literal whose keys overlap row-shape names (e.g. `status`)
as a row candidate. My inner `return { status: res.status, body }`
shape made the audit flag `body` as a silently dropped column.
Refactor the feed lookup to return the same `{ ok, error/clips }`
envelope shape already used in list.js: it both passes the audit and
matches the rest of the adapter's error-handling discipline.
- `npx vitest run --project adapter clis/suno/` — 47 tests still pass
- `node scripts/check-silent-column-drop.mjs` — no new violations
`Plan: session.planKey || 'unknown'` tripped the typed-error-lint `silent-sentinel` rule, which forbids `|| '<literal>'` fallbacks in adapter rows. `ensureSunoSession` already populates planKey from the same billing/info response as planId (and throws on missing planId), so the fallback was dead defensive code anyway. While here, harden the captcha catch block: a probe failure now defaults to `required: true` (conservative), keeping the displayed status row truthful instead of claiming "Not required" for an unverified state. - `node scripts/check-typed-error-lint.mjs` — 173/173 baseline, no new - `node scripts/check-silent-column-drop.mjs` — 102/102 baseline, no new - `npx vitest run --project adapter clis/suno/` — 47 tests pass
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A new browser-based adapter for Suno, the AI music generation service. Targets the
/api/generate/v2-web/schema introduced in 2026-05 and the cookie-isolation-aware Bearer auth path.Commands
opencli suno statusopencli suno listopencli suno generateopencli suno downloadEach
generaterequest returns 2 candidate clips (Suno's native A/B). All formats land alongside a metadata JSON dump for traceability.Custom-mode controls (matches the web UI)
--lyrics+--tags+--negative-tags+--title--weirdness(0..1) →metadata.control_sliders.weirdness_constraint--style-weight(0..1) →metadata.control_sliders.style_weight--instrumental,--model(chirp-fenix/chirp-bluejay/chirp-v4/chirp-v3-5)Paid-format guard
WAV downloads charge against the user's account (the Suno UI's Download → WAV flow makes the same
billing/clips/{id}/download/call). Bothgenerateanddownloadskip WAV by default and require--confirm-paid trueto actually charge — surfaced in the result row asskipped(needs --confirm-paid):wavrather than silently dropping it.Auth notes (worth a reviewer's attention)
The studio API rejects the OpenCLI bridge's
credentials: 'include'cross-origin fetch (third-party cookie isolation in the eval context). The adapter explicitly pulls the live Clerk JWT viaawait window.Clerk.session.getToken()and sends it asAuthorization: Bearer, alongside thebrowser-token(anti-replay base64-encoded timestamp) anddevice-id(from thesuno_device_idcookie) headers that the studio API also requires. This matches the wire format observed in a real/api/generate/v2-web/request capture.Validation
Known gaps (intentional / deferrable)
Files
```
clis/suno/
├── utils.js (498) — session, Bearer auth, generation, polling, download helpers
├── generate.js (208) — main generate command (Simple + Custom + sliders)
├── download.js (120) — standalone clip download by id
├── status.js (54) — health check
├── list.js (65) — library pagination
├── utils.test.js (148) — pure-function tests (parseFormats, sliders, sanitizers)
├── generate.test.js (206) — generate flow + argument validation + paid-format guard
├── download.test.js (98) — download by id + clip-id parsing + paid-format guard
└── commands.test.js (125) — status + list end-to-end (mocked utils)
```
Total: +1522 lines, 47 tests.