docs: document the skills command and the scriptable surface

The READMEs still described the flags and spellings the one-shot rework
replaced, and said nothing about `skills`. The root README gains an agent
skills section covering install, update and removal; the crate READMEs catch
up with the current command set, record shapes and library API.
This commit is contained in:
Michel de Graaf
2026-07-25 02:32:05 +02:00
parent cd41688803
commit 14b0321dbc
3 changed files with 207 additions and 44 deletions
+105 -27
View File
@@ -10,10 +10,10 @@ music enthusiasts around the world to share niche music.
## Features
- **Search & download** — search the network, pick results in the TUI, and
queue downloads with pause, resume, and retry
- **Sharing** — point `--shared-dir` at a directory and your files show up in
searches; peers can browse and download them
- **Search & download** — search the network and queue downloads in the TUI,
or fetch a track in one command with `get`
- **Sharing** — `shares add` a directory and your files show up in searches;
`serve` stays online so peers can browse and download them
- **Browse** — list any user's shared files and download straight from the tree
- **Chat rooms** — list, join, and talk in public rooms, several open at once
- **Private messages** — send and receive messages, with an inbox in the TUI
@@ -136,16 +136,25 @@ soulseek-rs user <NAME> # a peer's status and share counts
soulseek-rs shares list|add|remove|status|reindex
soulseek-rs config path|list|get|set
soulseek-rs portmap # test automatic port mapping
soulseek-rs skills install|uninstall|list # teach a coding agent this CLI
```
`whoami`, `config` and `shares list|add|remove` are the ones a script runs
first: the config commands need no account at all, and `whoami` answers
"are these credentials good and what am I offering" in one call.
Every command emits records except the two that perform an action —
`room say` and `message send` say nothing and answer with their exit code
alone. `room listen` and `message read` stream records as they arrive, until
`--duration` seconds pass or, with `--follow`, until they are interrupted.
Eleven of these need no credentials, because they never touch the network:
`config path|list|get|set`, `shares list|add|remove`, `portmap` and
`skills install|uninstall|list`. Note that
`shares status` and `shares reindex` are not among them — they report what the
network will actually see, which means logging in and letting the library scan
the folders.
Every command emits records except the four that perform an action —
`room say`, `message send`, `shares add` and `shares remove` say nothing on
stdout and answer with their exit code alone. `room listen`, `message read`
and `serve` stream records as they arrive, until `--duration` seconds pass or,
with `--follow`, until they are interrupted.
#### Record shapes
@@ -154,12 +163,37 @@ a path may contain anything except a tab (tabs and newlines in peer-supplied
text are replaced with spaces so a record is always exactly one line):
```
search user size bitrate path
browse user size path
room users room
download local-path
search user size bitrate path
browse user size path
download / get local-path
room list users room
room users user
room listen type room user message
message read timestamp user message
serve status user sent/size path
user user status average-speed shared-files
whoami user server shared-folders shared-files
shares list ok|missing directory
shares status folders files
config get|set|list|path key value
portmap ok|failed backend external
```
`room listen`'s `type` is `message`, `join` or `leave`; a join or leave leaves
the message field empty. `serve`'s `status` is `uploading`, `completed`,
`cancelled` or `failed`. `user`'s `status` is `online`, `away` or `offline`,
and any field the server did not answer prints as `-`.
`--json` carries more than text does, because an object has room for fields a
column layout does not: `search` adds `duration`, `slots`, `speed` and
`free_slot`; `browse` adds `directory`; `download` adds the `user`, remote
`path` and `size` alongside the local `file`; `whoami` adds `listening`,
`listen_port` and `download_dir`; `user` adds `privileged` and
`shared_folders`; `shares status` adds the `directories` array; `serve` adds
`bytes_sent`, `size`, `speed` and a `reason` on failure; `portmap` adds
`port`. Fields the server never answered are `null` rather than absent, so a
missing reply cannot be misread as a zero.
`download --stdin` reads back either shape it emits — a JSON object per line,
or tab-separated text whose first field is the user and last field is the
path — so `search` and `browse` pipe straight into it.
@@ -199,13 +233,46 @@ soulseek-rs serve --follow --json | tee -a uploads.ndjson
soulseek-rs user someuser --json | jq -e '.status != "offline"'
```
### Agent skills
The scriptable surface above is meant to be driven by coding agents as much as
by shell scripts, but `--help` cannot tell an agent which JSON keys a record
carries or what exit 4 means. That lives in a skill file shipped inside the
binary, so a `cargo install` is all you need to hand it to your agent:
```bash
soulseek-rs skills install
```
That writes `SKILL.md` into every agent it finds in your home directory —
currently Claude Code (`~/.claude/skills`) and opencode
(`~/.config/opencode/skill`) — and prints one record per target:
```
installed claude /home/you/.claude/skills/soulseek-rs/SKILL.md
unchanged opencode /home/you/.config/opencode/skill/soulseek-rs/SKILL.md
```
Running it again is how you update after upgrading the binary; an identical
copy reports `unchanged` rather than rewriting it. `skills list` shows what is
there without touching anything, and `skills uninstall` removes it again —
only when the file it finds is the one this binary wrote.
For an agent this table does not cover, name the directory yourself, or commit
the skill next to a project so everyone working on it gets the same brief:
```bash
soulseek-rs skills install --dir ~/.config/some-agent/skills
soulseek-rs skills install --dir .claude/skills # commit it with the repo
```
### Sharing
`serve` is the mode that makes this client a source: it stays logged in, keeps
the listener and the share index alive, answers searches and browse requests
from the network, and prints one record per upload as it changes state
(`uploading`, `completed`, `cancelled`, `failed`). It ends after `--duration`
seconds, or runs until interrupted with `--follow`.
seconds (an hour by default), or runs until interrupted with `--follow`.
```bash
soulseek-rs shares add ~/Music # remembered in config.toml
@@ -228,26 +295,37 @@ Downloads run under a deadline (`--timeout`, 300s by default) and
Every setting can come from a flag, an environment variable, or the config
file, in that order of precedence. Flags work before or after the subcommand.
| Flag | Environment variable | `config.toml` key |
| ---- | -------------------- | ----------------- |
| `--username` | `SOULSEEK_USERNAME` | `username` |
| `--password` | `SOULSEEK_PASSWORD` | — (never stored in the file) |
| `--password-cmd` | `SOULSEEK_PASSWORD_CMD` | `password_cmd` |
| `--server` | `SOULSEEK_SERVER` | `server` |
| `--download-dir` | `SOULSEEK_DOWNLOAD_DIR` | `download_dir` |
| `--shared-dir` (repeatable) | `SOULSEEK_SHARED_DIR` | `shared_dir` / `shared_dirs` |
| `--listener-port` | `SOULSEEK_LISTENER_PORT` | `listener_port` |
| `--no-listener` / `--listener` | `SOULSEEK_NO_LISTENER` | `disable_listener` |
| `--max-concurrent-downloads` | `SOULSEEK_MAX_CONCURRENT_DOWNLOADS` | `max_concurrent_downloads` |
| `--search-timeout` | `SOULSEEK_SEARCH_TIMEOUT` | `search_timeout` |
| `--log-file` | `SOULSEEK_LOG_FILE` | — |
| Flag | Environment variable | `config.toml` key | Default |
| ---- | -------------------- | ----------------- | ------- |
| `--username` | `SOULSEEK_USERNAME` | `username` | — (required) |
| `--password` | `SOULSEEK_PASSWORD` | — (never stored in the file) | — (required) |
| `--password-cmd` | `SOULSEEK_PASSWORD_CMD` | `password_cmd` | — |
| `--server` | `SOULSEEK_SERVER` | `server` | `server.slsknet.org:2416` |
| `--download-dir` | `SOULSEEK_DOWNLOAD_DIR` | `download_dir` | `<Downloads>/Soulseek` |
| `--shared-dir` (repeatable) | `SOULSEEK_SHARED_DIR` | `shared_dir` / `shared_dirs` | the download dir |
| `--listener-port` | `SOULSEEK_LISTENER_PORT` | `listener_port` | `2234` |
| `--no-listener` / `--listener` | `SOULSEEK_NO_LISTENER` | `disable_listener` | listener on |
| `--max-concurrent-downloads` | `SOULSEEK_MAX_CONCURRENT_DOWNLOADS` | `max_concurrent_downloads` | `5` |
| `--search-timeout` | `SOULSEEK_SEARCH_TIMEOUT` | `search_timeout` | `10` |
| `--log-file` | `SOULSEEK_LOG_FILE` | — | stderr |
Boolean environment variables accept the usual `1`/`0`/`true`/`false`/`yes`/`no`.
`--config <FILE>` (or `SOULSEEK_CONFIG`) reads a different config file and
`--no-config` ignores it entirely, which is what an isolated or containerised
run wants. The config file lives at `~/.config/soulseek-rs/config.toml` on
macOS and Linux; `SOULSEEK_CONFIG_DIR` and `SOULSEEK_STATE_DIR` relocate the
config and state directories wholesale.
config and state directories wholesale. Every variable above is also read from
a `.env` file in the working directory, which is often the tidiest way to hand
a container its credentials.
`config get` and `config set` cover the nine settings the file can hold —
`username`, `server`, `listener_port`, `disable_listener`, `download_dir`,
`shared_dirs`, `max_concurrent_downloads`, `search_timeout`, `password_cmd` —
and name the lot back at you when you ask for something else. Setting a key to
an empty string clears it, and `shared_dirs` takes a comma-separated list.
Every wait expressed in seconds (`--search-timeout`, each `--timeout`, each
`--duration`) is bounded to one day, so a mistyped flag is rejected rather than
hanging until the heat death of the universe.
#### Credentials for unattended runs
+50 -5
View File
@@ -28,7 +28,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create and connect to Soulseek server
let mut client = Client::new("username", "password");
client.connect();
client.connect()?;
client.login()?;
// Search for files
@@ -37,13 +37,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Download first available file
if let Some(result) = results.iter().find(|r| !r.files.is_empty()) {
let file = &result.files[0];
client.download(
let (_download, status) = client.download(
file.name.clone(),
file.username.clone(),
file.size,
"~/Downloads".to_string(),
)?;
println!("Downloaded: {}", file.name);
// `download` returns as soon as the request is queued: the transfer is
// finished when the channel says so, not when this call returns.
for update in status {
println!("{}: {update:?}", file.name);
}
}
Ok(())
@@ -58,16 +63,56 @@ use soulseek_rs::{Client, ClientSettings, PeerAddress};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client with custom settings
let settings = ClientSettings {
server_address: PeerAddress::new("server.slsknet.org".to_string(), 2242),
server_address: PeerAddress::new("server.slsknet.org".to_string(), 2416),
enable_listen: true,
listen_port: 3000,
..ClientSettings::new("username", "password")
};
let mut client = Client::with_settings(settings);
client.connect();
client.connect()?;
client.login()?;
Ok(())
}
```
### Asking about a peer
The server-backed lookups are request/poll pairs: you ask, then read the
answer once it lands. `request_user_info` clears any previous answer first, so
the loop below observes this request rather than the last one.
```rust
use soulseek_rs::Client;
use std::thread::sleep;
use std::time::{Duration, Instant};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::new("username", "password");
client.connect()?;
client.login()?;
client.request_user_info("someuser")?;
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if let Some(info) = client.user_info("someuser") {
// Presence and stats are separate replies, so each is an Option:
// a reply that never arrived is None, not "offline" or "shares nothing".
println!("{:?} {:?}", info.presence, info.stats);
break;
}
sleep(Duration::from_millis(100));
}
// Room membership is kept current from join and leave events.
println!("{:?}", client.room_members("lobby"));
Ok(())
}
```
`ServerMessage` and `ClientOperation` are `#[non_exhaustive]`: every protocol
message the client learns to handle adds a variant, so match them with a
wildcard arm.
+52 -12
View File
@@ -1,26 +1,66 @@
# soulseek-rs
A command-line client for the Soulseek peer-to-peer network.
A Soulseek client for the terminal: a full-screen TUI, and a one-shot command
surface built for scripts and agents.
## Installation
`bash cargo install soulseek-rs `
This will install the `soulseek-rs` binary to your system.
```bash
cargo install soulseek-rs
```
## Usage
The client provides a simple interface to search and download files from the
Soulseek network.
Run it with no subcommand for the interactive TUI:
```bash
soulseek-rs
```
Anything else is a one-shot command that runs headless. stdout carries records
only, stderr carries progress, and the exit code carries the verdict:
```bash
soulseek-rs search <QUERY> # print matching files
soulseek-rs get <QUERY> # search, pick the best, download
soulseek-rs download <USER> <PATH> # fetch one known file
soulseek-rs download --stdin # fetch files listed on stdin
soulseek-rs browse <USER> # list a user's shared files
soulseek-rs serve [--follow] # stay online sharing, stream uploads
soulseek-rs whoami # confirm credentials and connection
soulseek-rs user <NAME> # a peer's status and share counts
soulseek-rs room list|say|users|listen
soulseek-rs message send|read
soulseek-rs shares list|add|remove|status|reindex
soulseek-rs config path|list|get|set
soulseek-rs portmap # test automatic port mapping
```
Add `--json` for newline-delimited JSON instead of tab-separated text, so
commands compose:
```bash
soulseek-rs search "rjd2" --json \
| jq -c 'select(.size > 5000000 and .free_slot)' \
| soulseek-rs download --stdin
```
Exit codes: `0` success, `2` usage or config, `3` connect or login, `4` nothing
found, `5` timed out, `6` transfer failed, `1` unexpected error.
The [full README](https://github.com/michel/soulseek-rs#readme) documents every
command, record shape, flag, environment variable and `config.toml` key.
## Building from Source
````bash git clone https://github.com/yourusername/soulseek-rs cd soulseek-rs
cargo build --release ```
```bash
git clone https://github.com/michel/soulseek-rs.git
cd soulseek-rs
cargo build --release
```
## Library
If you want to build your own Soulseek client, check out the
[soulseek-rs-lib](https://crates.io/crates/soulseek-rs-lib) library crate.
````
To build your own Soulseek client, use the
[soulseek-rs-lib](https://crates.io/crates/soulseek-rs-lib) crate: the protocol
implementation this client is built on.