docs(website): lead with the agent-native story

`skills install` becomes the featured card on the home page, in the wide slot
the grid gives its first entry: one command hands a coding agent the JSON keys
of every record, the exit-code meanings, and the --json rule, with nothing to
clone because the skill ships inside the binary. The docs page gains a callout
and the command in the shell listing.

Also catches the site up with the scriptable command surface the last two
releases introduced: `get`, `shares add`, `serve`, `room list`, `message send`
and `--json` replace the flags and spellings that no longer exist.
This commit is contained in:
Michel de Graaf
2026-07-25 02:24:58 +02:00
parent 95bc18672d
commit 17e4bf9fd3
5 changed files with 111 additions and 51 deletions
+2 -2
View File
@@ -8,7 +8,7 @@
<title>soulseek-rs, a Soulseek client for your terminal</title>
<meta
name="description"
content="soulseek-rs is a keyboard-driven Soulseek client and a Rust protocol library. Search, share, browse, and chat from your terminal."
content="soulseek-rs is a keyboard-driven Soulseek client and a Rust protocol library, with scriptable subcommands. Search, share, browse, and chat from your terminal."
/>
<meta name="theme-color" content="#14110F" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F2EDE4" media="(prefers-color-scheme: light)" />
@@ -17,7 +17,7 @@
<meta property="og:title" content="soulseek-rs" />
<meta
property="og:description"
content="A Soulseek client and protocol library for Rust. Search, share, browse, and chat from your terminal."
content="A Soulseek client and protocol library for Rust. Search, share, browse, and chat from your terminal or a script."
/>
<meta property="og:url" content="https://re-invention.nl/soulseek-rs/" />
<link rel="canonical" href="https://re-invention.nl/soulseek-rs/" />
+11 -6
View File
@@ -32,8 +32,13 @@ const ALTERNATIVES: readonly Alternative[] = [
links: [{ label: 'Nicotine+', href: LINKS.nicotine }],
},
{
question: 'Want to point automation at the network?',
answer: <>slskd does that today. Daemon mode here is unbuilt.</>,
question: 'Want a queue that outlives the command?',
answer: (
<>
Every command runs one-shot and exits: JSON on stdout, an exit code, nothing left
running. No resident queue to pause, resume, or drive remotely. slskd has that.
</>
),
links: [{ label: 'slskd', href: LINKS.slskd }],
},
]
@@ -45,8 +50,8 @@ const CITIZENSHIP = [
title: 'Share, and share real files',
body: (
<>
Point <code>--shared-dir</code> at the collection you actually listen to. Full
albums, honest bitrates, nothing transcoded up from a lossy source and relabelled.
Point <code>shares add</code> at the collection you actually listen to. Full albums,
honest bitrates, nothing transcoded up from a lossy source and relabelled.
</>
),
},
@@ -67,8 +72,8 @@ const CITIZENSHIP = [
title: 'Stay online',
body: (
<>
Leave it running in a tmux session on the box that is already on. A share that
appears for an hour a week is close to no share at all.
Leave <code>serve --follow</code> running in a tmux session on the box that is
already on. A share that appears for an hour a week is close to no share at all.
</>
),
},
+39 -13
View File
@@ -15,6 +15,7 @@ import { Terminal } from '@/components/ui/terminal'
import { LINKS } from '@/lib/links'
const GLOBAL_KEYS: readonly (readonly [string, string])[] = [
['s', 'search the network'],
['Space', 'select a result'],
['Enter', 'download / send'],
['b', "browse the owner's files"],
@@ -41,7 +42,7 @@ const PANES: readonly { num?: number; title: string; body: string }[] = [
{
num: 1,
title: 'Searches',
body: 'Your queries and how many results each returned. Type / to search the network; each search keeps its own result set.',
body: 'Your queries and how many results each returned. Press s to search the network; each search keeps its own result set.',
},
{
num: 2,
@@ -153,27 +154,40 @@ export const Docs = () => {
eyebrow="4 · from the shell"
title="Every command also runs as a one-off."
>
Skip the TUI when you&rsquo;re scripting or mid-task.
Skip the TUI when you&rsquo;re scripting or mid-task. stdout is data, stderr is
progress, and the exit code is the verdict.
</SectionHead>
<Cols start>
<Terminal
label="shell"
lines={[
{ t: 'cmd', text: 'soulseek-rs "public domain field recordings"' },
{ t: 'cm', text: '# open the TUI straight onto a search' },
{ t: 'cmd', text: 'soulseek-rs message <username> "hello there"' },
{ t: 'cmd', text: 'soulseek-rs rooms' },
{ t: 'cm', text: '# list public rooms, busiest first' },
{ t: 'cmd', text: 'soulseek-rs chat <room> "hello room"' },
{ t: 'cmd', text: 'soulseek-rs portmap' },
{ t: 'cmd', text: 'soulseek-rs get "public domain field recordings"' },
{ t: 'cm', text: '# search, pick the best, download it' },
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --min-bitrate 320' },
{ t: 'cmd', text: 'soulseek-rs browse <username> --json' },
{ t: 'cmd', text: 'soulseek-rs room list' },
{ t: 'cm', text: '# public rooms, busiest first' },
{ t: 'cmd', text: 'soulseek-rs room say <room> "hello room"' },
{ t: 'cmd', text: 'soulseek-rs message send <username> "hello there"' },
{ t: 'cmd', text: 'soulseek-rs serve --follow' },
{ t: 'cm', text: '# stay online sharing; every upload as it happens' },
{ t: 'cmd', text: 'soulseek-rs skills install' },
{ t: 'cm', text: '# hand all of the above to your coding agent' },
]}
/>
<Prose>
<p>
The subcommands mirror the TUI: <Code>search</Code>, <Code>message</Code>,{' '}
<Code>browse</Code>, <Code>rooms</Code>, <Code>chat</Code>, and{' '}
<Code>portmap</Code>. Pass a room and a message to <Code>chat</Code> to say
one thing and exit.
The command groups: <Code>search</Code>, <Code>get</Code>,{' '}
<Code>download</Code>, <Code>browse</Code>, <Code>room</Code>,{' '}
<Code>message</Code>, <Code>serve</Code>, <Code>user</Code>,{' '}
<Code>whoami</Code>, <Code>shares</Code>, <Code>config</Code>,{' '}
<Code>portmap</Code>, <Code>skills</Code>. Records are tab-separated fields,
one per line, or NDJSON with <Code>--json</Code>.
</p>
<p>
No TTY, no daemon. <Code>search --json</Code> pipes through <Code>jq</Code>{' '}
into <Code>download --stdin</Code>. Every ending has its own exit code: 4 found
nothing, 3 could not reach the server, 6 the transfer died.
</p>
<p>
<Code>portmap</Code> tests whether your router will let peers connect back,
@@ -185,6 +199,18 @@ export const Docs = () => {
</Prose>
</Cols>
<div className="mt-6">
<Callout title="give it to your agent">
<p>
<Code>soulseek-rs skills install</Code> writes a <Code>SKILL.md</Code> into
every coding agent on the machine — Claude Code and opencode today,{' '}
<Code>--dir</Code> for anything else. It carries what <Code>--help</Code>{' '}
cannot: the JSON keys of every record, what each exit code means, and the
idioms worth reaching for. Run it again after an upgrade to update it, and{' '}
<Code>skills uninstall</Code> takes it back out.
</p>
</Callout>
</div>
<div className="mt-4">
<Callout tone="warn" title="the one limit worth knowing up front">
<p>
If both you and a peer are behind routers with no forwarded port, browsing
+34 -15
View File
@@ -20,22 +20,41 @@ const PLATFORMS: readonly { name: OsName; note: string; anchor: string }[] = [
]
const FEATURES = [
{
glyph: '✦',
color: 'var(--accent-text)',
title: 'Agent native',
cmd: 'soulseek-rs skills install',
body: (
<>
One command hands your coding agent the whole surface: the JSON keys every record
carries, what each exit code means, and the rule that it always asks for{' '}
<code>--json</code>. The skill ships inside the binary, so there is nothing to
clone and nothing to configure — it finds Claude Code and opencode on its own.
</>
),
},
{
glyph: '→',
color: 'var(--accent-text)',
title: 'Search & download',
cmd: 'soulseek-rs "field recordings"',
body: <>Search the network, pick results in the TUI, and queue downloads with pause, resume, and retry.</>,
cmd: 'soulseek-rs get "field recordings"',
body: (
<>
Queue downloads in the TUI with pause, resume, and retry, or let <code>get</code>{' '}
search, pick the best, and download in one command.
</>
),
},
{
glyph: '↑',
color: 'var(--status-success)',
title: 'Sharing',
cmd: 'soulseek-rs --shared-dir ~/Music',
cmd: 'soulseek-rs shares add ~/Music',
body: (
<>
Point <code>--shared-dir</code> at a directory and your files show up in searches;
peers browse and download them.
Point <code>shares add</code> at a directory and your files show up in searches;{' '}
<code>serve</code> stays online so peers can browse and download them.
</>
),
},
@@ -50,14 +69,14 @@ const FEATURES = [
glyph: '#',
color: 'var(--status-warning)',
title: 'Chat rooms',
cmd: 'soulseek-rs rooms',
cmd: 'soulseek-rs room list',
body: <>List, join, and talk in public rooms, several open at once as tabs.</>,
},
{
glyph: '@',
color: 'var(--status-info)',
title: 'Private messages',
cmd: 'soulseek-rs message <user> "hi"',
cmd: 'soulseek-rs message send <user> "hi"',
body: <>Send and receive PMs, with an inbox and an unread counter in the TUI.</>,
},
{
@@ -88,11 +107,11 @@ const FEATURES = [
glyph: '▮',
color: 'var(--accent-text)',
title: 'TUI and CLI',
cmd: 'soulseek-rs',
cmd: 'soulseek-rs search "rjd2" --json',
body: (
<>
A full terminal interface, plus scriptable subcommands: search, message, browse,
rooms, chat, portmap.
A full terminal interface, plus scriptable subcommands: stdout is data, stderr is
progress, the exit code is the verdict.
</>
),
},
@@ -203,10 +222,10 @@ export const Home = () => {
</Callout>
<Callout title="agents and automation">
<p>
Every command runs as a one-off from the shell: <Code>search</Code>,{' '}
<Code>browse</Code>, <Code>rooms</Code>, <Code>chat</Code>,{' '}
<Code>message</Code>, <Code>portmap</Code>. Point cron, a script, or an
agent at them and read the output back. No daemon, no API to stand up.{' '}
Every command runs as a one-off from the shell. Point cron, a script, or an
agent at them and read JSON records back off stdout. No daemon, no API to
stand up — and <Code>skills install</Code> teaches the agent the surface
for you.{' '}
<Link to="/docs" className="text-link hover:text-link-hover">
See the commands
</Link>
@@ -243,7 +262,7 @@ export const Home = () => {
<div className="flex flex-col gap-3.5">
<Eyebrow>get started</Eyebrow>
<h2 className="text-[26px] leading-[34px] font-medium text-balance sm:text-title sm:leading-[var(--text-title--line-height)]">
Install it, then press <span className="text-accent-text">/</span> to search.
Install it, then press <span className="text-accent-text">s</span> to search.
</h2>
<p className="text-secondary">
Needs a Rust toolchain. From there it&rsquo;s one command, then the TUI opens.
+25 -15
View File
@@ -11,7 +11,7 @@ use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::new("username", "password");
client.connect();
client.connect()?;
client.login()?;
let results = client.search("public domain field recordings", Duration::from_secs(10))?;
@@ -117,8 +117,8 @@ export const Install = () => {
]}
/>
<p className="text-[13px] text-muted">
Cargo puts the binary on your PATH. Run <Code>soulseek-rs</Code> and the TUI
opens.
Cargo puts the binary on your PATH. Run <Code>soulseek-rs</Code> in a terminal
and the TUI opens, in a script or a pipe it wants a subcommand instead.
</p>
<div className="flex flex-wrap gap-2 sm:gap-3">
<Button href={LINKS.releases}>Prebuilt binaries</Button>
@@ -205,7 +205,7 @@ export const Install = () => {
<Section>
<SectionHead eyebrow="being reachable" title="Let peers connect back.">
Browsing and downloading are peer-to-peer, so at least one side has to accept an
incoming connection.
incoming connection. <Code>serve</Code> refuses to start with the listener off.
</SectionHead>
<Cols start>
<Prose>
@@ -218,13 +218,18 @@ export const Install = () => {
yourself.
</p>
<p>
The port is your <Code>--listener-port</Code> (env <Code>LISTENER_PORT</Code>,
default <Code>2234</Code>); it&rsquo;s renewed automatically and removed on
exit. Pass <Code>--disable-listener</Code> to turn it off. Check your own
network without launching the client:
The port is your <Code>--listener-port</Code> (env{' '}
<Code>SOULSEEK_LISTENER_PORT</Code>, default <Code>2234</Code>); it&rsquo;s
renewed automatically and removed on exit. Pass <Code>--no-listener</Code> to
turn it off. Check your own network without launching the client:
</p>
<div className="mt-4">
<Terminal lines={[{ t: 'cmd', text: 'soulseek-rs portmap' }]} />
<Terminal
lines={[
{ t: 'cmd', text: 'soulseek-rs portmap' },
{ t: 'cm', text: "# exits 0 when it works, 4 when it doesn't" },
]}
/>
</div>
</Prose>
<Callout tone="warn" title="honest about limits">
@@ -288,13 +293,17 @@ export const Install = () => {
<FeatureCard title="Secrets">
Your password goes to the OS keychain, never a plain-text file. If you would
rather it never be stored at all, have <code>password_cmd</code> shell out to
your own password manager and print it on demand.
your own password manager and print it on demand. For CI and containers,{' '}
<code>--password-stdin</code> reads it off a pipe so it never reaches{' '}
<code>ps</code> or the environment.
</FeatureCard>
<FeatureCard title="Settings and state">
<code>config.toml</code> is layered: flags beat environment variables, which
beat the file, which beats the defaults. State (searches, downloads, rooms) is
versioned JSON and restores on restart; anything unreadable is set aside as{' '}
<code>.bak</code> instead of being overwritten.
beat the file, which beats the defaults. <code>config get</code> and{' '}
<code>config set</code> read and write that file without opening an editor.
State (searches, downloads, rooms) is versioned JSON and restores on restart;
anything unreadable is set aside as <code>.bak</code> instead of being
overwritten.
</FeatureCard>
</Cols>
</Section>
@@ -302,14 +311,15 @@ export const Install = () => {
<Section band>
<SectionHead eyebrow="uninstall" title="Removing it completely.">
Three things exist on disk: the binary, the config, and the state. Nothing else is
written, and there is no telemetry to opt out of.
written unless you pointed <Code>--log-file</Code> at a file, and there is no
telemetry to opt out of.
</SectionHead>
<Cols start>
<Terminal
label="macOS · Linux"
lines={[
{ t: 'cmd', text: 'cargo uninstall soulseek-rs' },
{ t: 'cm', text: '# config, state and any cached shares' },
{ t: 'cm', text: '# config and state: searches, downloads, rooms, messages' },
{
t: 'cmd',
text: 'rm -rf ~/.config/soulseek-rs ~/.local/share/soulseek-rs',