CLI¶
The sonagram command drives the whole pipeline: scan a library, optionally
enrich it, build the graph, probe freshness, and materialize playlists. The same
command is available two ways from one shared Rust code path — the pip install sonagram console script and the standalone binary run identical parsing, output
strings, and exit codes, so they cannot drift.
Progress and stage lines go to stderr; results (reports, paths, counts) go to stdout, so the CLI composes in a pipeline.
Global flags (any position): -h/--help prints usage, -V/--version
prints the version.
Exit codes: 0 on success, 1 on error. status is the exception — it
returns a freshness code (0 fresh, 1 needs scan, 2 no cache).
sonagram scan¶
sonagram scan <library_root>
Walk <library_root> for MP3s, content-hash each file, and run sonara analysis
on anything unseen, caching per-track records under <library_root>/.sonagram/.
Reuses cached analysis wherever the content hash (or file stats) is unchanged, so
a rescan only analyzes changed files. Prints a scan report: total files, analyzed
(new), reused (hash match / stat match), failed (with per-file messages), and
elapsed time.
sonagram enrich¶
sonagram enrich <library_root>
Fetch Last.fm metadata — popularity, folksonomy tags, MBIDs, similar
artists/tracks, and original-album mapping — for the library’s artists, tracks,
and albums, caching it under <library_root>/.sonagram/lastfm/. Needs a
LASTFM_API_KEY (environment variable or a .env file in the current directory
or the library root). Re-runs skip already-fetched entities (incremental); a
per-entity fetch failure is recorded, never fatal. After enrich, build folds
the cache in automatically. Prints per-entity fetched / skipped / failed counts.
sonagram build¶
sonagram build <library_root> <out.kgl>
Build the knowledge graph from the cached analysis records and save it to
<out.kgl>. Run scan first — with no cached records this errors. Auto-loads
the Last.fm enrichment cache when present (run enrich to populate it),
producing an enriched build; otherwise a plain build. Prints the track count
and the output path. See the graph schema for what the .kgl
contains.
sonagram playlist¶
sonagram playlist <library_root> <graph.kgl> \
(--cypher '<query>' | --ids <hash1,hash2,...>) \
(--out <file.m3u8> and/or --copy-to <dir>)
Resolve a track set from the graph and materialize it. Track order is preserved verbatim — never re-sorted.
Pass exactly one selector:
--cypher '<query>'— a read-only query whose result is aTrack-node orcontent_hashcolumn (RETURN t.content_hashorRETURN t).--ids <hash1,hash2,...>— comma-separated content hashes directly.
Pass at least one destination (both are allowed together):
--out <file.m3u8>— write a UTF-8 extended-M3U playlist with absolute paths.--copy-to <dir>— write a self-contained portable folder: the tracks copied asNN - Artist - Title.<ext>next to a relative-path.m3u8. The.m3u8is named after--out’s file stem when given, else the destination folder’s own name, elseplaylist. Copies only — source files are never moved, retagged, or modified.
Each content hash resolves to its on-disk path (library_root + the track’s
stored relative path). A hash matching no Track is reported, not silently
dropped.
Central store (--name): pass --name "<name>" (with an optional
--description "<text>") to save the playlist into the configured playlist store
(~/.sonagram/playlists/) as <slug>.m3u8 + a <slug>.meta.json sidecar,
retrievable later with sonagram playlists. --name is a destination in its own
right — combine it with --out/--copy-to to also write those, or use it alone.
In the config-driven form (no path args), --name reads the configured graph.
sonagram status¶
sonagram status <library_root> [--format json]
A read-only freshness probe (mutates nothing): report how the cache under
<library_root>/.sonagram/ compares to the files on disk, without hashing a file
or running analysis.
Exit code is the result: 0 = fresh, 1 = needs scan, 2 = no cache. The
default output is human-readable lines; --format json emits one stable object
with these keys:
Key |
Type |
Meaning |
|---|---|---|
|
string |
the probed root |
|
bool |
|
|
int |
|
|
int |
indexed, stats + record still fresh |
|
int |
stats changed or record stale/missing |
|
int |
on disk, never scanned |
|
int |
indexed, file now gone |
|
bool |
Last.fm cache present & non-empty |
|
int |
current sonara analysis schema |
|
int |
current sonara embedding version |
|
bool |
any stale/missing/deleted |
|
string |
|
|
int |
|
Config-driven form (sonagram status, no path): probes every configured
source and also reports graph freshness — the graph self-describes whether it
still reflects the library. Each source is compared against the configured graph’s
Source.scan_fingerprint (a blake3 over the sorted rel_path|size|mtime scan
state, recomputed cheaply from disk with a stat-only walk). The aggregate JSON
adds:
Key |
Type |
Meaning |
|---|---|---|
|
bool|null |
source’s fingerprint matches the graph (null if no graph) |
|
string|null |
the configured graph path |
|
bool |
the graph file exists |
|
bool |
the graph must be rebuilt (missing, or a source drifted) |
A stale graph is action-worthy: exit 1 even when every cache is fresh
(status needs_build) — the fix is a sonagram build (~1s from cache), the
add/delete/rebuild mechanism.
sonagram sources¶
sonagram sources add <dir> # register a library folder (canonicalized, deduped)
sonagram sources remove <dir> # unregister
sonagram sources list # show the registry
Manage the configured source registry (~/.sonagram/config.json). Once a source
is registered, the bare config-driven forms (sonagram scan / build / status
/ enrich, and playlist ... --name) fan out over every source with no path
arguments.
sonagram config¶
sonagram config # show the resolved config
sonagram config set graph <path> # override the central graph location
sonagram config set playlists_dir <path> # override the playlist-store location
Show the resolved config — sources, the central graph and playlist-store paths
(defaults under ~/.sonagram/, flagged [default]), whether each file exists,
and whether a Last.fm key is configured (the location only, never the key).
sonagram playlists¶
sonagram playlists # list stored playlists (newest first)
sonagram playlists show <slug> # full metadata + tracklist for one playlist
Read the central playlist store built by playlist --name.
sonagram skill¶
sonagram skill show # print the bundled agent skill
sonagram skill install [--dir <skills_root>] [--force]
show prints the embedded sonagram-playlist skill to stdout. install writes
it to <skills_root>/sonagram-playlist/SKILL.md (default ~/.claude/skills),
creating any missing directories. Install personalizes the file from your config
(substituting the library-root placeholder with your first configured source),
refuses to overwrite an existing file unless --force, and prints a reminder to
read and follow the file now, in-session — skills only auto-load at the next
session start.
Examples¶
sonagram scan ~/Music
sonagram build ~/Music music.kgl
sonagram status ~/Music --format json
sonagram playlist ~/Music music.kgl \
--cypher 'MATCH (t:Track) WHERE t.bpm > 120 RETURN t.content_hash ORDER BY t.energy' \
--out set.m3u8
sonagram playlist ~/Music music.kgl \
--ids h1,h2,h3 --copy-to ~/Desktop/roadtrip