Command Line
spondulix is the command line for Spondulix Search: load and maintain a music catalog, manage its metadata, and search it in plain language, all through the API.
$ spondulix search "upbeat indie pop about a road trip, around 120 bpm"
Search 42: upbeat indie pop about a road trip, around 120 bpm
15 tracks matched (3.41s).
# SCORE ID TITLE BPM LENGTH
1 95.5 1001 Highway Windows Down 118 3:24
2 88.0 1042 Two Lane Summer 122 2:58
...
Install
With Go 1.22 or later:
$ go install github.com/mdchaney/spondulix-cli/cmd/spondulix@latest
Or from a checkout of the repository:
$ make install # into $(go env GOPATH)/bin
$ make build # or just build ./bin/spondulix
For completion in your shell, see spondulix completion --help.
Sign in
There are two ways to sign in.
With your API token
Your API token is on your account page. The CLI reads it from standard input so that it stays out of your shell history:
$ spondulix auth login --with-token
API token (from https://app.spondulix.ai/my/account):
Signed in to https://app.spondulix.ai with an API token.
$ spondulix auth login --with-token < token.txt
An API token may do anything you may, and works until you reset it on your account page.
With your browser
$ spondulix auth login
Opening your browser to sign in. If it does not open, visit:
https://app.spondulix.ai/oauth/authorize?...
Waiting for you to approve the request...
Signed in to https://app.spondulix.ai. The CLI may: read your catalog, run searches and change your catalog.
You sign in to Spondulix in the browser and approve the CLI there.
Nothing secret is typed into the terminal. The CLI gets only the
permissions you approve, its access lasts an hour at a time and is renewed
as needed, and you can disconnect it at any time from your account page or
with spondulix auth logout.
It asks to read your catalog, to run searches and to change your catalog.
It does not ask to administer your libraries unless you pass
--admin. That permission lets it make libraries and change their
settings, their AI provider keys and who may use them, and change your own
account and password: more than loading and searching a catalog needs.
A command that needs it and has not got it says so, and how to sign in
again with it.
| Flag | |
|---|---|
--admin |
Also ask for permission to administer your libraries and your account |
--no-browser |
Print the address instead of opening a browser. On a machine without one, open the address on another machine, approve, and paste the address that browser ends up at (it starts http://127.0.0.1 and will not load) back into the terminal. |
--scopes catalog,search |
Ask for less than everything. Without catalog_write the CLI can read and search but not change anything. |
--new-client |
Register the CLI with the server again. For when the server has forgotten it. |
A server from before its API accepted this kind of sign-in lets you sign in and then refuses the result. The CLI tells you when that happens, and to sign in with your API token instead.
Other servers
$ spondulix auth login --with-token --host spondulix.example.com
The first server you sign in to becomes the default. Credentials are kept
for each server separately; --host, or SPONDULIX_HOST, chooses between
them. A server is known by its address as you gave it, so localhost:3000
and 127.0.0.1:3000 are two servers, each with a sign-in of its own.
For a server on your own machine, plain http is assumed:
$ spondulix auth login --with-token --host localhost:3000
Checking and signing out
$ spondulix auth status
https://app.spondulix.ai
Signed in with: an API token
Signed in as: Floyd Lawson <[email protected]>
Default library: 2
Server: up, and it accepts these credentials
$ spondulix auth logout
Choose a library
Almost everything happens in a library. Choose one once:
$ spondulix libraries list
ID NAME ROLE TRACKS PROMPTS CHAT MODEL DESCRIPTION
2 Frivolous Music editor 22148 3 gpt-4o Production music for film and television
$ spondulix libraries use "Frivolous Music"
Now working in library 2, Frivolous Music.
Or name one each time with --library (-l), by ID or by name, or with
SPONDULIX_LIBRARY.
Your role in a library is what you may do there. A viewer can read and search; changing anything takes an editor, an admin or the owner.
Commands
auth login, logout, status, token |
Sign in and out |
account show, update, password, reset-token, sessions, apps |
Your own account |
config list, get, set, unset, path |
Settings |
ping |
Check the server and your credentials |
libraries list, get, create, update, use, models |
Your libraries |
providers list, get, add, update, remove, available |
The AI providers a library has keys for |
collaborators list, get, update, remove |
Who may use a library |
invitations list, get, rescind |
Invitations not yet taken up |
tracks list, get, create, update, delete, import, lyrics |
The catalog |
composers ..., publishers ... |
list, get, create, update, delete, import and tracks, for the people and companies credited |
metadata-types list, get, create, update, delete, sort, fields |
The fields of its own a library tags tracks with |
metadata-tags list, get, create, update, delete, bulk, tracks |
The values of a tag type; name the type with --type |
pros list, get |
Performing rights organizations |
search |
Search a library |
searches list, get, keep, delete |
Searches that were run |
prompts list, get, create, update, delete, copy, check, versions |
What the language model is told |
examples list, get, create, update, delete, hide, unhide, sort, suggest, suggested |
The worked examples a prompt is sent with; name the prompt with --prompt |
embeddings |
Whether the lyrics are ready to be searched by meaning |
api |
Any request to the API |
Every command has --help, with examples.
Records
Tracks, composers, publishers and tags are addressed by your IDs, which you choose when you create them. Tags of a type that has no external IDs are addressed by name. Libraries, metadata types and prompts have IDs the server gave them, and can be named instead.
$ spondulix composers create --id 125 --name "Johann Sebastian Bach" --birth-date 1685-03-31
$ spondulix composers update 125 --pro 35 --ipi 116
$ spondulix composers get 125
$ spondulix composers delete 125
$ spondulix metadata-tags list --type genre
$ spondulix metadata-tags get "hip hop" --type mood
list takes --query (-q) for tracks, composers, publishers and PROs.
Composers and publishers are matched on their names; tracks on every word
of the query, in their title, description, lyrics, tags and credits. It
finds records by what is written in them. To find music by what it is
like, use search.
$ spondulix composers list -q jones
$ spondulix tracks list -q "thousand feet"
tracks lists the tracks of a composer, a publisher or a tag:
$ spondulix composers tracks 186
$ spondulix metadata-tags tracks rock --type genre
update changes only what you name. To remove a value, set it to nothing:
an empty string for text (--description ""), or null with --set
(--set bpm=null).
delete asks first. A script must pass --yes.
Tracks
Besides the fields every track has, a track carries a value for each of
the library’s metadata types. Set those with --set, under the type’s
name:
$ spondulix tracks create --id 1001 --title "Highway Windows Down" \
--bpm 118 --duration 204 --lyrics @lyrics.txt \
--style-alike haim --style-alike "best coast" \
--composer 186:50 --composer 187:50 \
--set genre_name="indie pop" \
--set 'mood_names=["upbeat", "summer"]' \
--set release_date=2019-04-12
--set genre_id=3 |
A tag, by your ID for it |
--set genre_name=rock |
A tag, by name |
--set 'mood_names=["calm","warm"]' |
The tags of a type that allows several. The list replaces the ones the track had. |
--set 'genres=[{"id": 9, "name": "ska"}]' |
A tag by ID and name, created if it is new |
--set rating=4.5 |
A value of any other type |
--set rating=null |
No value |
A tag that is named and is not there is created if its type has no external IDs. If it has, a new tag needs an ID as well as a name, which is the fourth form. A key that is no field and no metadata type is refused, so a misspelling does not pass for success.
The value of --set is JSON when it parses as JSON, so 120, true,
null and ["a","b"] mean what they say, and text otherwise. To send
text that looks like JSON, quote it as JSON: --set 'title="123"'.
--composer and --publisher take ID or ID:SHARE. Shares left out are
split evenly. The list replaces the track’s credits.
--lyrics, --lyrics-chorus and --description read a file when given
@FILE.
Anything can also be given as JSON, with the fields the API documents. Flags override what is in it:
$ spondulix tracks create --data @track.json
$ spondulix tracks update 1001 --data '{"bpm": 124, "genre_name": "rock"}'
spondulix metadata-types fields lists every field of a library, its own
and the built-in ones.
Searching
Say what you want:
$ spondulix search "sad songs for a rainy montage, no vocals"
The library’s language model turns the request into a query. That takes a few seconds, and costs the library what a search on the site does, since it runs through the library’s own AI provider.
Every search is kept. Refine one by continuing it:
$ spondulix search --continue 42 "the same, but slower"
A filter, a boost and a sort can go alongside the request, and a filter can take its place. A filter is a hard constraint; a boost raises the score of the tracks that match without leaving the others out, so it needs a request or a filter to go with. Both are JSON, as the operators reference describes.
$ spondulix search "songs for a montage" --filter '{"instrumental": true}'
$ spondulix search --filter @filter.json --sort "bpm desc" --sort title
$ spondulix search "driving" --boost '{"lyrical_theme": {"value": "leaving a small town", "weight": 40}}'
| Flag | |
|---|---|
--filter, --boost |
JSON, or @FILE |
--sort |
FIELD, "FIELD desc" or -FIELD. May be repeated. |
--continue ID |
Refine an earlier search |
--prompt NAME |
The library prompt to use. spondulix prompts list shows them. |
--provider, --model |
Instead of the library’s own |
--limit N |
Results to show. 20 unless you say; 0 for all. |
--remote-id |
Your own identifier for the search. cli unless you say. |
Searches run from the CLI are marked cli in the library’s search
history, and spondulix searches list --from cli lists them.
To see what the model made of a request, and what ran:
$ spondulix searches get 42 --answer # its explanation, and the query it wrote
$ spondulix searches get 42 --sql # the SQL that ran
$ spondulix searches get 42 -o json # everything, each exchange with the model included
Prompts and examples
A prompt is what the library’s language model is told, to turn a request into a query. It is sent with worked examples: requests, each with the answer the model should give. Examples are how the model learns a catalog’s vocabulary.
$ spondulix prompts create --name main # starts as one written from the library's fields
$ spondulix prompts get main --text # as the model sees it
$ spondulix prompts update main --prompt @prompt.txt --note "Say what a boost is for"
$ spondulix prompts check main # what it and its examples get wrong
Changing what a prompt says makes a new version and makes it the current one. The others are kept:
$ spondulix prompts versions list main
$ spondulix prompts versions get main 34 --diff
$ spondulix prompts versions make-current main 33
To try a change without touching the prompt in use, spondulix prompts
copy main --name experiment, and search with --prompt experiment.
The quickest way to a good example is a search that read a request well:
$ spondulix search "hard rock songs"
$ spondulix searches keep 228 # now an example of the prompt it ran with
Or have the model write requests of its own and run them, then keep the ones it read well and throw the rest away. This costs a search for each:
$ spondulix examples suggest --prompt main --count 5 --focus "trailer editors"
$ spondulix examples suggested --prompt main
$ spondulix searches keep 230
$ spondulix searches delete 231
Examples can also be written by hand, put in order, and hidden, which keeps them but stops sending them:
$ spondulix examples create --prompt main --request "upbeat rock" --answer @answer.json
$ spondulix examples hide 31 --prompt main
$ spondulix examples sort 40 31 35 --prompt main
Loading a catalog
In this order, since each refers to the one before:
$ spondulix metadata-types create --name genre --data-type tag --include-in-prompt --has-external-id
$ spondulix metadata-types create --name mood --data-type tag --multiple --include-in-prompt --has-external-id=false
$ spondulix metadata-tags bulk --type genre genres.json
$ spondulix metadata-tags bulk --type mood moods.txt
$ spondulix composers import composers.json
$ spondulix publishers import publishers.json
$ spondulix tracks import tracks.jsonl
$ spondulix embeddings --wait
A tag type has external IDs unless you say --has-external-id=false. With
them, every tag needs your ID for it and is addressed by it. Without, tags
are addressed by name, and tracks can bring new ones with them
(--set mood_names=...).
Files
import and bulk read JSON: either a list of records or one record
after another (JSON Lines). - reads standard
input. Records have the fields the API documents, and each needs your ID
for it, as id.
{"id": 1001, "title": "Highway Windows Down", "bpm": 118, "genre_name": "indie pop",
"tracks_composers": [{"composer_id": 186, "share": 100}]}
metadata-tags bulk also reads a plain list of names, one to a line.
What list writes, import reads. To copy a library’s tracks into
another:
$ spondulix tracks list --all -o jsonl -l 2 | spondulix tracks import - -l 5
How import behaves
--mode |
|
|---|---|
upsert |
The default. A record that exists is updated; one that does not is created. |
create |
A record that exists is reported as a failure, and left as it was. |
update |
Changes records that exist; creates nothing. |
- The whole file is checked before anything is sent: a record without an
ID, or two with the same one, and nothing is changed.
--dry-runstops there. - With
--mode upsert, tracks are sent 200 to a request and composers and publishers 1000. With--mode createorupdatethey go one to a request. - A record that fails does not stop the rest, unless you pass
--fail-fast. The failures are listed at the end, by their place in the file, and the exit code is 1. - Loading a file again is safe.
- It stops at once if the server refuses your credentials or your role.
Requests are made one after another. --concurrency makes several at
once, up to 32, which may be quicker against a server with the means to
answer several at once. Against the application in development, 800
tracks took the same 40 seconds either way.
A server from before October 2026 is not safe with it when records create tags on the way: two records that bring the same new tag at the same moment can each create it, and the library ends up with the tag twice. Against such a server, load tags, composers and publishers first, as above.
metadata-tags bulk is all or nothing: if a tag is invalid none is
saved. The server takes 1000 tags in a request, so a longer file is sent as
several, each all or nothing on its own.
Lyrics
Lyrics are embedded for searching by meaning in the background, in batches, after tracks are loaded. Until a track’s lyrics are embedded, searches that go by what lyrics are about do not find it.
$ spondulix embeddings
Lyric chunks: 4 embedded, 10882 waiting, 0 in progress.
The lyrics are not all ready to be searched yet.
--wait keeps looking until nothing is waiting or in progress, which is
what a script that loads a catalog and then searches it wants.
Setting up a library
$ spondulix libraries create --name "Frivolous Music"
$ spondulix libraries use "Frivolous Music"
$ spondulix providers add openai < key.txt
$ spondulix libraries models
$ spondulix libraries update "Frivolous Music" \
--chat-provider openai --chat-model gpt-4o \
--embedding-provider openai --embedding-model text-embedding-3-large
Then its metadata types, its tracks and a prompt, as above.
Searches in plain language, and the embedding of lyrics, run through the library’s own accounts with its AI providers and are charged to them. A key is given once and never shown again; what is shown of it is its last four characters. It is read from the terminal without being shown, or from standard input, never from a flag, so that it stays out of your shell history.
People join a library by invitation, with a role. Invitations are sent on the web site: an invitation is mail from Spondulix to an address of the sender’s choosing, which is not something a token that has leaked should be able to do. The CLI lists those not yet taken up, and rescinds them.
| Role | May |
|---|---|
viewer |
Read and search |
editor |
Also change the catalog, prompts and examples |
admin |
Also change the library’s settings, its AI providers and who may use it |
owner |
The one the library belongs to |
$ spondulix invitations list
$ spondulix collaborators list
$ spondulix collaborators update [email protected] --role admin
$ spondulix collaborators remove [email protected]
Three things are done on the web site and not here, by the server’s decision: making an account, deleting a library, and inviting someone to one.
With a browser sign-in, everything in this section but the lists needs
spondulix auth login --admin.
Your account
$ spondulix account show
$ spondulix account update --name "Floyd Lawson"
$ spondulix account password
$ spondulix account sessions list # the browsers you are signed in from
$ spondulix account apps list # the applications signed in as you
spondulix account reset-token replaces your API token. The old one
stops working at once, everywhere it is used. If the CLI is signed in
with the API token it takes the new one in place of the old and goes on
working. Otherwise the new token is printed, once; the server does not
show it again.
With a browser sign-in, changing anything here needs spondulix auth login
--admin.
Output
--output (-o) chooses the format:
table |
The default. For reading. Long values are cut short. |
json |
What the server sent. A list is a JSON array of records. |
jsonl |
A record to a line |
csv |
With a header row, and every field rather than only the table’s columns |
Data goes to standard output, and everything said about it (what was created, how many more pages there are, progress) to standard error, so output can be piped as it is:
$ spondulix tracks list --all -o json | jq '.[] | select(.bpm > 120) | .title'
$ spondulix composers list --all -o csv > composers.csv
Lists show a page of 25. --page and --per-page (200 at most) choose
another; --all reads every page; --limit N reads the first N records.
Scripts
$ export SPONDULIX_TOKEN=... # instead of signing in
$ export SPONDULIX_HOST=https://app.spondulix.ai
$ export SPONDULIX_LIBRARY=2
$ spondulix tracks get 1001 -o json
| Exit code | |
|---|---|
| 0 | Success |
| 1 | An error, including an import in which some records failed |
| 2 | The command was misused |
| 3 | Not signed in, or the server refused the credentials |
| 4 | Not allowed: your role in the library, or the token’s permissions |
| 5 | Not found |
| 6 | The server rejected the data as invalid |
| 130 | Interrupted |
The server describes its whole API in OpenAPI, at
/api/v1/openapi.yaml (or /api/v1/openapi.json, the same document),
and holds every response to it; that document is the contract the CLI is
written to:
$ spondulix api /api/v1/openapi.yaml > openapi.yaml
For what the commands do not cover, spondulix api makes any request with
your credentials:
$ spondulix api libraries/2/tracks --query per_page=5
$ spondulix api libraries/2/composers --data '{"composer": {"external_id": 125, "name": "J. S. Bach"}}'
$ curl -H "Authorization: Bearer $(spondulix auth token)" https://app.spondulix.ai/api/v1/ping
API versions
The API has a version, and this program is built for one. It declares it with every request, and the server says how the two stand:
$ spondulix version
spondulix 1.0.0 (API 1.1.0)
https://app.spondulix.ai speaks API 1.1.0: the same as this program
When the server is newer, every command ends with a note to upgrade. When
the server is older than this program, nothing is said while things work;
when something fails, the error comes with a note that the server may not
have it yet, which is what a 404 from a server awaiting a deploy means.
version asks without credentials, so it works before signing in, and
auth status and --debug show both versions too.
--debug logs every request and response to standard error. It does not
log your token, and keys, passwords and tokens in what is sent and
received are left out of it.
Requests that are safe to repeat are tried three times when the server is unreachable or answers 502, 503 or 504. Creating something, and running a search, is never repeated: it may have happened.
Settings
| Environment | Setting | Flag | |
|---|---|---|---|
SPONDULIX_HOST |
host |
--host |
The server. https://app.spondulix.ai unless you say. |
SPONDULIX_LIBRARY |
library |
--library, -l |
The library, kept for each server |
SPONDULIX_TOKEN |
A token to use instead of the stored credentials | ||
SPONDULIX_CONFIG_DIR |
Where the files are kept | ||
--timeout |
How long to wait for an answer. 2m unless you say. |
A flag overrides the environment, which overrides the setting.
$ spondulix config set host spondulix.example.com
$ spondulix config list
Settings are in config.json and credentials in credentials.json, in
$SPONDULIX_CONFIG_DIR, else $XDG_CONFIG_HOME/spondulix, else
~/.config/spondulix. credentials.json is readable only by you. It holds
your tokens as they are, not encrypted: anyone who can read your files can
use them, as with ~/.netrc or an SSH key without a passphrase. On a
shared machine, prefer the browser sign-in, whose tokens can be
disconnected from your account page, or keep the token in
SPONDULIX_TOKEN.