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> <title>soulseek-rs, a Soulseek client for your terminal</title>
<meta <meta
name="description" 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="#14110F" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F2EDE4" media="(prefers-color-scheme: light)" /> <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:title" content="soulseek-rs" />
<meta <meta
property="og:description" 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/" /> <meta property="og:url" content="https://re-invention.nl/soulseek-rs/" />
<link rel="canonical" href="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 }], links: [{ label: 'Nicotine+', href: LINKS.nicotine }],
}, },
{ {
question: 'Want to point automation at the network?', question: 'Want a queue that outlives the command?',
answer: <>slskd does that today. Daemon mode here is unbuilt.</>, 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 }], links: [{ label: 'slskd', href: LINKS.slskd }],
}, },
] ]
@@ -45,8 +50,8 @@ const CITIZENSHIP = [
title: 'Share, and share real files', title: 'Share, and share real files',
body: ( body: (
<> <>
Point <code>--shared-dir</code> at the collection you actually listen to. Full Point <code>shares add</code> at the collection you actually listen to. Full albums,
albums, honest bitrates, nothing transcoded up from a lossy source and relabelled. honest bitrates, nothing transcoded up from a lossy source and relabelled.
</> </>
), ),
}, },
@@ -67,8 +72,8 @@ const CITIZENSHIP = [
title: 'Stay online', title: 'Stay online',
body: ( body: (
<> <>
Leave it running in a tmux session on the box that is already on. A share that Leave <code>serve --follow</code> running in a tmux session on the box that is
appears for an hour a week is close to no share at all. 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' import { LINKS } from '@/lib/links'
const GLOBAL_KEYS: readonly (readonly [string, string])[] = [ const GLOBAL_KEYS: readonly (readonly [string, string])[] = [
['s', 'search the network'],
['Space', 'select a result'], ['Space', 'select a result'],
['Enter', 'download / send'], ['Enter', 'download / send'],
['b', "browse the owner's files"], ['b', "browse the owner's files"],
@@ -41,7 +42,7 @@ const PANES: readonly { num?: number; title: string; body: string }[] = [
{ {
num: 1, num: 1,
title: 'Searches', 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, num: 2,
@@ -153,27 +154,40 @@ export const Docs = () => {
eyebrow="4 · from the shell" eyebrow="4 · from the shell"
title="Every command also runs as a one-off." 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> </SectionHead>
<Cols start> <Cols start>
<Terminal <Terminal
label="shell" label="shell"
lines={[ lines={[
{ t: 'cmd', text: 'soulseek-rs "public domain field recordings"' }, { t: 'cmd', text: 'soulseek-rs get "public domain field recordings"' },
{ t: 'cm', text: '# open the TUI straight onto a search' }, { t: 'cm', text: '# search, pick the best, download it' },
{ t: 'cmd', text: 'soulseek-rs message <username> "hello there"' }, { t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --min-bitrate 320' },
{ t: 'cmd', text: 'soulseek-rs rooms' }, { t: 'cmd', text: 'soulseek-rs browse <username> --json' },
{ t: 'cm', text: '# list public rooms, busiest first' }, { t: 'cmd', text: 'soulseek-rs room list' },
{ t: 'cmd', text: 'soulseek-rs chat <room> "hello room"' }, { t: 'cm', text: '# public rooms, busiest first' },
{ t: 'cmd', text: 'soulseek-rs portmap' }, { 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> <Prose>
<p> <p>
The subcommands mirror the TUI: <Code>search</Code>, <Code>message</Code>,{' '} The command groups: <Code>search</Code>, <Code>get</Code>,{' '}
<Code>browse</Code>, <Code>rooms</Code>, <Code>chat</Code>, and{' '} <Code>download</Code>, <Code>browse</Code>, <Code>room</Code>,{' '}
<Code>portmap</Code>. Pass a room and a message to <Code>chat</Code> to say <Code>message</Code>, <Code>serve</Code>, <Code>user</Code>,{' '}
one thing and exit. <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>
<p> <p>
<Code>portmap</Code> tests whether your router will let peers connect back, <Code>portmap</Code> tests whether your router will let peers connect back,
@@ -185,6 +199,18 @@ export const Docs = () => {
</Prose> </Prose>
</Cols> </Cols>
<div className="mt-6"> <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"> <Callout tone="warn" title="the one limit worth knowing up front">
<p> <p>
If both you and a peer are behind routers with no forwarded port, browsing 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 = [ 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: '→', glyph: '→',
color: 'var(--accent-text)', color: 'var(--accent-text)',
title: 'Search & download', title: 'Search & download',
cmd: 'soulseek-rs "field recordings"', cmd: 'soulseek-rs get "field recordings"',
body: <>Search the network, pick results in the TUI, and queue downloads with pause, resume, and retry.</>, 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: '↑', glyph: '↑',
color: 'var(--status-success)', color: 'var(--status-success)',
title: 'Sharing', title: 'Sharing',
cmd: 'soulseek-rs --shared-dir ~/Music', cmd: 'soulseek-rs shares add ~/Music',
body: ( body: (
<> <>
Point <code>--shared-dir</code> at a directory and your files show up in searches; Point <code>shares add</code> at a directory and your files show up in searches;{' '}
peers browse and download them. <code>serve</code> stays online so peers can browse and download them.
</> </>
), ),
}, },
@@ -50,14 +69,14 @@ const FEATURES = [
glyph: '#', glyph: '#',
color: 'var(--status-warning)', color: 'var(--status-warning)',
title: 'Chat rooms', 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.</>, body: <>List, join, and talk in public rooms, several open at once as tabs.</>,
}, },
{ {
glyph: '@', glyph: '@',
color: 'var(--status-info)', color: 'var(--status-info)',
title: 'Private messages', 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.</>, body: <>Send and receive PMs, with an inbox and an unread counter in the TUI.</>,
}, },
{ {
@@ -88,11 +107,11 @@ const FEATURES = [
glyph: '▮', glyph: '▮',
color: 'var(--accent-text)', color: 'var(--accent-text)',
title: 'TUI and CLI', title: 'TUI and CLI',
cmd: 'soulseek-rs', cmd: 'soulseek-rs search "rjd2" --json',
body: ( body: (
<> <>
A full terminal interface, plus scriptable subcommands: search, message, browse, A full terminal interface, plus scriptable subcommands: stdout is data, stderr is
rooms, chat, portmap. progress, the exit code is the verdict.
</> </>
), ),
}, },
@@ -203,10 +222,10 @@ export const Home = () => {
</Callout> </Callout>
<Callout title="agents and automation"> <Callout title="agents and automation">
<p> <p>
Every command runs as a one-off from the shell: <Code>search</Code>,{' '} Every command runs as a one-off from the shell. Point cron, a script, or an
<Code>browse</Code>, <Code>rooms</Code>, <Code>chat</Code>,{' '} agent at them and read JSON records back off stdout. No daemon, no API to
<Code>message</Code>, <Code>portmap</Code>. Point cron, a script, or an stand up — and <Code>skills install</Code> teaches the agent the surface
agent at them and read the output back. No daemon, no API to stand up.{' '} for you.{' '}
<Link to="/docs" className="text-link hover:text-link-hover"> <Link to="/docs" className="text-link hover:text-link-hover">
See the commands See the commands
</Link> </Link>
@@ -243,7 +262,7 @@ export const Home = () => {
<div className="flex flex-col gap-3.5"> <div className="flex flex-col gap-3.5">
<Eyebrow>get started</Eyebrow> <Eyebrow>get started</Eyebrow>
<h2 className="text-[26px] leading-[34px] font-medium text-balance sm:text-title sm:leading-[var(--text-title--line-height)]"> <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> </h2>
<p className="text-secondary"> <p className="text-secondary">
Needs a Rust toolchain. From there it&rsquo;s one command, then the TUI opens. 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>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::new("username", "password"); let mut client = Client::new("username", "password");
client.connect(); client.connect()?;
client.login()?; client.login()?;
let results = client.search("public domain field recordings", Duration::from_secs(10))?; 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"> <p className="text-[13px] text-muted">
Cargo puts the binary on your PATH. Run <Code>soulseek-rs</Code> and the TUI Cargo puts the binary on your PATH. Run <Code>soulseek-rs</Code> in a terminal
opens. and the TUI opens, in a script or a pipe it wants a subcommand instead.
</p> </p>
<div className="flex flex-wrap gap-2 sm:gap-3"> <div className="flex flex-wrap gap-2 sm:gap-3">
<Button href={LINKS.releases}>Prebuilt binaries</Button> <Button href={LINKS.releases}>Prebuilt binaries</Button>
@@ -205,7 +205,7 @@ export const Install = () => {
<Section> <Section>
<SectionHead eyebrow="being reachable" title="Let peers connect back."> <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 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> </SectionHead>
<Cols start> <Cols start>
<Prose> <Prose>
@@ -218,13 +218,18 @@ export const Install = () => {
yourself. yourself.
</p> </p>
<p> <p>
The port is your <Code>--listener-port</Code> (env <Code>LISTENER_PORT</Code>, The port is your <Code>--listener-port</Code> (env{' '}
default <Code>2234</Code>); it&rsquo;s renewed automatically and removed on <Code>SOULSEEK_LISTENER_PORT</Code>, default <Code>2234</Code>); it&rsquo;s
exit. Pass <Code>--disable-listener</Code> to turn it off. Check your own renewed automatically and removed on exit. Pass <Code>--no-listener</Code> to
network without launching the client: turn it off. Check your own network without launching the client:
</p> </p>
<div className="mt-4"> <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> </div>
</Prose> </Prose>
<Callout tone="warn" title="honest about limits"> <Callout tone="warn" title="honest about limits">
@@ -288,13 +293,17 @@ export const Install = () => {
<FeatureCard title="Secrets"> <FeatureCard title="Secrets">
Your password goes to the OS keychain, never a plain-text file. If you would 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 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>
<FeatureCard title="Settings and state"> <FeatureCard title="Settings and state">
<code>config.toml</code> is layered: flags beat environment variables, which <code>config.toml</code> is layered: flags beat environment variables, which
beat the file, which beats the defaults. State (searches, downloads, rooms) is beat the file, which beats the defaults. <code>config get</code> and{' '}
versioned JSON and restores on restart; anything unreadable is set aside as{' '} <code>config set</code> read and write that file without opening an editor.
<code>.bak</code> instead of being overwritten. 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> </FeatureCard>
</Cols> </Cols>
</Section> </Section>
@@ -302,14 +311,15 @@ export const Install = () => {
<Section band> <Section band>
<SectionHead eyebrow="uninstall" title="Removing it completely."> <SectionHead eyebrow="uninstall" title="Removing it completely.">
Three things exist on disk: the binary, the config, and the state. Nothing else is 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> </SectionHead>
<Cols start> <Cols start>
<Terminal <Terminal
label="macOS · Linux" label="macOS · Linux"
lines={[ lines={[
{ t: 'cmd', text: 'cargo uninstall soulseek-rs' }, { 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', t: 'cmd',
text: 'rm -rf ~/.config/soulseek-rs ~/.local/share/soulseek-rs', text: 'rm -rf ~/.config/soulseek-rs ~/.local/share/soulseek-rs',