mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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()
|
||||
|
||||
118
website/docs/user-guide/features/spotify.md
Normal file
118
website/docs/user-guide/features/spotify.md
Normal 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.
|
||||
@@ -89,6 +89,7 @@ const sidebars: SidebarsConfig = {
|
||||
label: 'Advanced',
|
||||
items: [
|
||||
'user-guide/features/rl-training',
|
||||
'user-guide/features/spotify',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user