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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user