feat(spotify): interactive setup wizard + docs page (#15130)

Previously 'hermes auth spotify' crashed with 'HERMES_SPOTIFY_CLIENT_ID
is required' if the user hadn't manually created a Spotify developer
app and set env vars. Now the command detects a missing client_id and
walks the user through the one-time app registration inline:

- Opens https://developer.spotify.com/dashboard in the browser
- Tells the user exactly what to paste into the Spotify form
  (including the correct default redirect URI, 127.0.0.1:43827)
- Prompts for the Client ID
- Persists HERMES_SPOTIFY_CLIENT_ID to ~/.hermes/.env so subsequent
  runs skip the wizard
- Continues straight into the PKCE OAuth flow

Also prints the docs URL at both the start of the wizard and the end
of a successful login so users can find the full guide.

Adds website/docs/user-guide/features/spotify.md with the complete
setup walkthrough, tool reference, and troubleshooting, and wires it
into the sidebar under User Guide > Features > Advanced.

Fixes a stale redirect URI default in the hermes_cli/tools_config.py
TOOL_CATEGORIES entry (was 8888/callback from the PR description
instead of the actual DEFAULT_SPOTIFY_REDIRECT_URI value
43827/spotify/callback defined in auth.py).
This commit is contained in:
Teknium
2026-04-24 05:30:05 -07:00
committed by GitHub
parent 0d32411310
commit 05394f2f28
5 changed files with 250 additions and 2 deletions

View File

@@ -86,6 +86,8 @@ QWEN_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 120
DEFAULT_SPOTIFY_ACCOUNTS_BASE_URL = "https://accounts.spotify.com"
DEFAULT_SPOTIFY_API_BASE_URL = "https://api.spotify.com/v1"
DEFAULT_SPOTIFY_REDIRECT_URI = "http://127.0.0.1:43827/spotify/callback"
SPOTIFY_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/user-guide/features/spotify"
SPOTIFY_DASHBOARD_URL = "https://developer.spotify.com/dashboard"
SPOTIFY_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 120
DEFAULT_SPOTIFY_SCOPE = " ".join((
"user-modify-playback-state",
@@ -1917,9 +1919,83 @@ def get_spotify_auth_status() -> Dict[str, Any]:
}
def _spotify_interactive_setup(redirect_uri_hint: str) -> str:
"""Walk the user through creating a Spotify developer app, persist the
resulting client_id to ~/.hermes/.env, and return it.
Raises SystemExit if the user aborts or submits an empty value.
"""
from hermes_cli.config import save_env_value
print()
print("=" * 70)
print("Spotify first-time setup")
print("=" * 70)
print()
print("Spotify requires every user to register their own lightweight")
print("developer app. This takes about two minutes and only has to be")
print("done once per machine.")
print()
print(f"Full guide: {SPOTIFY_DOCS_URL}")
print()
print("Steps:")
print(f" 1. Opening {SPOTIFY_DASHBOARD_URL} in your browser...")
print(" 2. Click 'Create app' and fill in:")
print(" App name: anything (e.g. hermes-agent)")
print(" Description: anything")
print(f" Redirect URI: {redirect_uri_hint}")
print(" API/SDK: Web API")
print(" 3. Agree to the terms, click Save.")
print(" 4. Open the app's Settings page and copy the Client ID.")
print(" 5. Paste it below.")
print()
if not _is_remote_session():
try:
webbrowser.open(SPOTIFY_DASHBOARD_URL)
except Exception:
pass
try:
raw = input("Spotify Client ID: ").strip()
except (EOFError, KeyboardInterrupt):
print()
raise SystemExit("Spotify setup cancelled.")
if not raw:
print()
print(f"No Client ID entered. See {SPOTIFY_DOCS_URL} for the full guide.")
raise SystemExit("Spotify setup cancelled: empty Client ID.")
# Persist so subsequent `hermes auth spotify` runs skip the wizard.
save_env_value("HERMES_SPOTIFY_CLIENT_ID", raw)
# Only persist the redirect URI if it's non-default, to avoid pinning
# users to a value the default might later change to.
if redirect_uri_hint and redirect_uri_hint != DEFAULT_SPOTIFY_REDIRECT_URI:
save_env_value("HERMES_SPOTIFY_REDIRECT_URI", redirect_uri_hint)
print()
print("Saved HERMES_SPOTIFY_CLIENT_ID to ~/.hermes/.env")
print()
return raw
def login_spotify_command(args) -> None:
existing_state = get_provider_auth_state("spotify") or {}
client_id = _spotify_client_id(getattr(args, "client_id", None), existing_state)
# Interactive wizard: if no client_id is configured anywhere, walk the
# user through creating the Spotify developer app instead of crashing
# with "HERMES_SPOTIFY_CLIENT_ID is required".
explicit_client_id = getattr(args, "client_id", None)
try:
client_id = _spotify_client_id(explicit_client_id, existing_state)
except AuthError as exc:
if getattr(exc, "code", "") != "spotify_client_id_missing":
raise
client_id = _spotify_interactive_setup(
redirect_uri_hint=getattr(args, "redirect_uri", None) or DEFAULT_SPOTIFY_REDIRECT_URI,
)
redirect_uri = _spotify_redirect_uri(getattr(args, "redirect_uri", None), existing_state)
scope = _spotify_scope_string(getattr(args, "scope", None) or existing_state.get("scope"))
accounts_base_url = _spotify_accounts_base_url(existing_state)
@@ -1946,6 +2022,8 @@ def login_spotify_command(args) -> None:
print("Open this URL to authorize Hermes:")
print(authorize_url)
print()
print(f"Full setup guide: {SPOTIFY_DOCS_URL}")
print()
if open_browser and not _is_remote_session():
try:
@@ -1992,6 +2070,7 @@ def login_spotify_command(args) -> None:
print("Spotify login successful!")
print(f" Auth state: {saved_to}")
print(" Provider state saved under providers.spotify")
print(f" Docs: {SPOTIFY_DOCS_URL}")
# =============================================================================
# SSH / remote session detection

View File

@@ -373,7 +373,7 @@ TOOL_CATEGORIES = {
{"key": "HERMES_SPOTIFY_CLIENT_ID", "prompt": "Spotify app client_id",
"url": "https://developer.spotify.com/dashboard"},
{"key": "HERMES_SPOTIFY_REDIRECT_URI", "prompt": "Redirect URI (must be allow-listed in your Spotify app)",
"default": "http://127.0.0.1:8888/callback"},
"default": "http://127.0.0.1:43827/spotify/callback"},
],
},
],

View File

@@ -86,3 +86,53 @@ def test_auth_spotify_status_command_reports_logged_in(capsys, monkeypatch: pyte
output = capsys.readouterr().out
assert "spotify: logged in" in output
assert "client_id: spotify-client" in output
def test_spotify_interactive_setup_persists_client_id(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
capsys,
) -> None:
"""The wizard writes HERMES_SPOTIFY_CLIENT_ID to .env and returns the value."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setattr("builtins.input", lambda prompt="": "wizard-client-123")
# Prevent actually opening the browser during tests.
monkeypatch.setattr(auth_mod, "webbrowser", SimpleNamespace(open=lambda *_a, **_k: False))
monkeypatch.setattr(auth_mod, "_is_remote_session", lambda: True)
result = auth_mod._spotify_interactive_setup(
redirect_uri_hint=auth_mod.DEFAULT_SPOTIFY_REDIRECT_URI,
)
assert result == "wizard-client-123"
env_path = tmp_path / ".env"
assert env_path.exists()
env_text = env_path.read_text()
assert "HERMES_SPOTIFY_CLIENT_ID=wizard-client-123" in env_text
# Default redirect URI should NOT be persisted.
assert "HERMES_SPOTIFY_REDIRECT_URI" not in env_text
# Docs URL should appear in wizard output so users can find the guide.
output = capsys.readouterr().out
assert auth_mod.SPOTIFY_DOCS_URL in output
def test_spotify_interactive_setup_empty_aborts(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Empty input aborts cleanly instead of persisting an empty client_id."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setattr("builtins.input", lambda prompt="": "")
monkeypatch.setattr(auth_mod, "webbrowser", SimpleNamespace(open=lambda *_a, **_k: False))
monkeypatch.setattr(auth_mod, "_is_remote_session", lambda: True)
with pytest.raises(SystemExit):
auth_mod._spotify_interactive_setup(
redirect_uri_hint=auth_mod.DEFAULT_SPOTIFY_REDIRECT_URI,
)
env_path = tmp_path / ".env"
if env_path.exists():
assert "HERMES_SPOTIFY_CLIENT_ID" not in env_path.read_text()

View File

@@ -0,0 +1,118 @@
# Spotify
Hermes can control Spotify directly — playback, queue, search, playlists, saved tracks/albums, and listening history — using Spotify's official Web API with PKCE OAuth.
Unlike most Hermes integrations, Spotify requires every user to register their own lightweight developer app. Spotify does not let third parties ship a public OAuth app that anyone can use. The whole thing takes about two minutes.
## Prerequisites
- A Spotify account (Free works for most tools; **playback control requires Premium**)
- Hermes Agent installed and running
## Setup
### 1. Enable the toolset
```bash
hermes tools
```
Scroll to `🎵 Spotify`, press space to toggle it on, then `s` to save.
### 2. Run the login wizard
```bash
hermes auth spotify
```
If you don't have a Spotify app yet, Hermes walks you through creating one:
1. Opens the Spotify developer dashboard in your browser
2. Tells you exactly what values to paste into the Spotify form
3. Prompts you for the `Client ID` you get back
4. Saves it to `~/.hermes/.env` and continues straight into the OAuth flow
After the Spotify consent page, tokens are saved under `providers.spotify` in `~/.hermes/auth.json` and the integration is live.
### Creating the Spotify app (what the wizard asks for)
When you land on the dashboard, click **Create app** and fill in:
| Field | Value |
|-------|-------|
| App name | anything (e.g. `hermes-agent`) |
| App description | anything (e.g. `personal Hermes integration`) |
| Website | leave blank |
| Redirect URI | `http://127.0.0.1:43827/spotify/callback` |
| Which API/SDKs? | **Web API** |
Agree to the terms, click **Save**. On the next screen click **Settings** → copy the **Client ID**. That's the only value Hermes needs (no client secret — PKCE doesn't use one).
## Verify
```bash
hermes auth status spotify
```
Shows whether tokens are present and when the access token expires. Hermes automatically refreshes on 401.
## Using it
Once logged in, the agent has access to 9 Spotify tools:
| Tool | Actions |
|------|---------|
| `spotify_playback` | play, pause, skip, seek, volume, now playing, playback state |
| `spotify_devices` | list devices, transfer playback |
| `spotify_queue` | inspect queue, add tracks to queue |
| `spotify_search` | search tracks, albums, artists, playlists |
| `spotify_playlists` | list, get, create, update, add/remove tracks |
| `spotify_albums` | get album, list album tracks |
| `spotify_saved_tracks` | list, save, remove |
| `spotify_saved_albums` | list, save, remove |
| `spotify_activity` | recently played, now playing |
The agent picks the right tool automatically. Ask it to "play some Miles Davis," "what am I listening to," "add the current track to my starred playlist," etc.
## Sign out
```bash
hermes auth logout spotify
```
Removes tokens from `~/.hermes/auth.json`. To also clear the app config, delete `HERMES_SPOTIFY_CLIENT_ID` (and optionally `HERMES_SPOTIFY_REDIRECT_URI`) from `~/.hermes/.env`.
## Troubleshooting
**`403 Forbidden` on playback endpoints** — Spotify requires Premium for `play`, `pause`, `skip`, and volume control. Search, playlists, and library reads work on Free.
**`204 No Content` on `now_playing`** — nothing is currently playing; expected behavior, not an error.
**`INVALID_CLIENT: Invalid redirect URI`** — the redirect URI registered in your Spotify app doesn't match what Hermes is using. Default is `http://127.0.0.1:43827/spotify/callback`. If you picked something else, set `HERMES_SPOTIFY_REDIRECT_URI` in `~/.hermes/.env` to match.
**`429 Too Many Requests`** — Spotify rate limit. Hermes surfaces this as a friendly error; wait a minute and retry.
## Advanced: custom scopes
By default Hermes requests the scopes needed for every shipped tool. To override:
```bash
hermes auth spotify --scope "user-read-playback-state user-modify-playback-state playlist-read-private"
```
See Spotify's [scope reference](https://developer.spotify.com/documentation/web-api/concepts/scopes) for available values.
## Advanced: custom client ID / redirect URI
```bash
hermes auth spotify --client-id <id> --redirect-uri http://localhost:3000/callback
```
Or set them permanently in `~/.hermes/.env`:
```
HERMES_SPOTIFY_CLIENT_ID=<your_id>
HERMES_SPOTIFY_REDIRECT_URI=http://localhost:3000/callback
```
The redirect URI must be allow-listed in your Spotify app's settings.

View File

@@ -89,6 +89,7 @@ const sidebars: SidebarsConfig = {
label: 'Advanced',
items: [
'user-guide/features/rl-training',
'user-guide/features/spotify',
],
},
{