docs(website): explain what the skill unlocks, and document config.toml
The agent-native copy described what the skill contains rather than what it buys you. A new home-page section shows the shape of the request that becomes possible — describe the outcome, let the agent write the pipeline — and names the specific knowledge --help cannot carry: which JSON keys a record has, that exit 4 means widen the query, that a remote path goes back byte-identical, that --follow never returns. The install page gains a reference for all nine config.toml settings with their defaults and an example file, next to the paths that were already there. The list is the one `config set` accepts, checked against FileConfig::KEYS.
This commit is contained in:
@@ -203,9 +203,16 @@ export const Docs = () => {
|
||||
<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>--dir</Code> for anything else. After it lands you describe the
|
||||
outcome instead of the pipeline: “mirror that peer’s FLAC
|
||||
folder”, “watch this room for an hour and save anything
|
||||
mentioning the label”.
|
||||
</p>
|
||||
<p>
|
||||
It carries what <Code>--help</Code> cannot: the JSON keys of every record,
|
||||
that exit 4 means widen the query rather than retry it, that remote paths go
|
||||
back byte-identical, and that <Code>--follow</Code> never returns so an agent
|
||||
passes <Code>--duration</Code>. Run it again after an upgrade to update it;{' '}
|
||||
<Code>skills uninstall</Code> takes it back out.
|
||||
</p>
|
||||
</Callout>
|
||||
|
||||
@@ -27,10 +27,10 @@ const FEATURES = [
|
||||
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.
|
||||
Run it once and asking beats scripting: your agent knows the record shapes, the
|
||||
exit codes, and the filters, so “find the 1998 pressing, lossless only”
|
||||
becomes a pipeline it writes itself. No MCP server, no wrapper — the skill ships
|
||||
inside the binary and finds Claude Code and opencode on its own.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -257,6 +257,53 @@ export const Home = () => {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section id="agents">
|
||||
<SectionHead eyebrow="agent native" title="Ask for the record. Skip the pipeline.">
|
||||
A CLI an agent can drive is not the same as a CLI an agent knows how to drive.
|
||||
<Code>skills install</Code> closes that gap in one command.
|
||||
</SectionHead>
|
||||
<Cols start>
|
||||
<Terminal
|
||||
label="your agent, after skills install"
|
||||
lines={[
|
||||
{ t: 'cm', text: '"get me Geogaddi in FLAC, only peers with a free slot"' },
|
||||
{
|
||||
t: 'cmd',
|
||||
text: 'soulseek-rs search "boards of canada geogaddi" \\',
|
||||
},
|
||||
{ t: 'code', text: ' --free-slots --json \\' },
|
||||
{ t: 'code', text: " | jq -c 'select(.path | endswith(\".flac\"))' \\" },
|
||||
{ t: 'code', text: ' | soulseek-rs download --stdin --json' },
|
||||
{ t: 'out', text: '{"file":"~/Music/Soulseek/01 Ready Lets Go.flac",…}' },
|
||||
{ t: 'cm', text: '# 23 tracks. no wrapper script, no hand-holding.' },
|
||||
]}
|
||||
/>
|
||||
<Prose>
|
||||
<p>
|
||||
<Code>--help</Code> lists flags. It cannot say which JSON keys a record
|
||||
carries, that exit 4 means widen the query rather than retry it, that a
|
||||
remote path is backslash-separated and has to go back byte-identical, or
|
||||
that <Code>--follow</Code> never returns and an agent wants{' '}
|
||||
<Code>--duration</Code> instead. An agent without that guesses, and guessing
|
||||
against someone else’s file server is how you get a hung job.
|
||||
</p>
|
||||
<p>
|
||||
The skill is that missing half. Once it lands, the interesting requests stop
|
||||
being pipelines you write: mirror a peer’s FLAC folder overnight, watch
|
||||
a room and save anything that mentions a label, check whether a user is worth
|
||||
queueing behind before committing to a 2 GB transfer.
|
||||
</p>
|
||||
<p>
|
||||
It stays a CLI — no daemon, no MCP server, no API key. Upgrading the binary
|
||||
upgrades the skill; run <Code>skills install</Code> again to take it.{' '}
|
||||
<Link to="/docs" className="text-link hover:text-link-hover">
|
||||
See the commands
|
||||
</Link>
|
||||
</p>
|
||||
</Prose>
|
||||
</Cols>
|
||||
</Section>
|
||||
|
||||
<Section band>
|
||||
<Cols center>
|
||||
<div className="flex flex-col gap-3.5">
|
||||
|
||||
@@ -67,6 +67,98 @@ const PLATFORMS: readonly PlatformNote[] = [
|
||||
},
|
||||
]
|
||||
|
||||
interface Setting {
|
||||
key: string
|
||||
fallback: string
|
||||
body: React.ReactNode
|
||||
}
|
||||
|
||||
const SETTINGS: readonly Setting[] = [
|
||||
{
|
||||
key: 'username',
|
||||
fallback: 'unset',
|
||||
body: <>Your Soulseek account name. The TUI writes it here after a first login.</>,
|
||||
},
|
||||
{
|
||||
key: 'server',
|
||||
fallback: 'server.slsknet.org:2416',
|
||||
body: <>The server to log in to, as host:port.</>,
|
||||
},
|
||||
{
|
||||
key: 'listener_port',
|
||||
fallback: '2234',
|
||||
body: (
|
||||
<>
|
||||
The TCP port peers connect back on. Forward it, or let <code>portmap</code> ask
|
||||
your router.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'disable_listener',
|
||||
fallback: 'false',
|
||||
body: (
|
||||
<>
|
||||
<code>true</code> refuses incoming peer connections. You can still download from
|
||||
reachable peers, but firewalled ones can no longer reach you.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'download_dir',
|
||||
fallback: 'the Downloads path above',
|
||||
body: <>Where finished transfers land.</>,
|
||||
},
|
||||
{
|
||||
key: 'shared_dirs',
|
||||
fallback: 'the download folder',
|
||||
body: (
|
||||
<>
|
||||
Folders offered to the network, as a list. An explicitly empty list shares
|
||||
nothing. <code>shared_dir</code> is the older single-folder spelling and still
|
||||
reads; <code>shares add</code> writes the list form.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'max_concurrent_downloads',
|
||||
fallback: '5',
|
||||
body: <>Transfers allowed to run at once. The rest queue.</>,
|
||||
},
|
||||
{
|
||||
key: 'search_timeout',
|
||||
fallback: '10',
|
||||
body: (
|
||||
<>
|
||||
Seconds a search collects replies before reporting. Results trickle in from
|
||||
separate peers, so a short window finds less.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'password_cmd',
|
||||
fallback: 'unset',
|
||||
body: (
|
||||
<>
|
||||
A shell command whose stdout is the password, used when the keychain has none.
|
||||
The password itself never belongs in this file.
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const CONFIG_EXAMPLE: TermLine[] = [
|
||||
'username = "your-name"',
|
||||
'download_dir = "~/Music/Soulseek"',
|
||||
'shared_dirs = ["~/Music/Soulseek", "~/Music/Library"]',
|
||||
'max_concurrent_downloads = 3',
|
||||
'search_timeout = 15',
|
||||
'listener_port = 2234',
|
||||
'',
|
||||
'# never store the password itself; shell out for it instead',
|
||||
'password_cmd = "pass show soulseek"',
|
||||
].map((text) => ({ t: 'code' as const, text }))
|
||||
|
||||
interface StepProps {
|
||||
n: number
|
||||
title: string
|
||||
@@ -308,6 +400,48 @@ export const Install = () => {
|
||||
</Cols>
|
||||
</Section>
|
||||
|
||||
<Section id="config">
|
||||
<SectionHead eyebrow="config.toml" title="Nine settings, all optional.">
|
||||
Every one has a working default, so an empty file — or no file — is valid. Flags
|
||||
beat environment variables, which beat this file, which beats the defaults.{' '}
|
||||
<Code>config list</Code> prints the effective value of each.
|
||||
</SectionHead>
|
||||
<Cols start>
|
||||
<div className="flex flex-col">
|
||||
{SETTINGS.map((setting) => (
|
||||
<div
|
||||
key={setting.key}
|
||||
className="grid grid-cols-1 gap-x-4 border-t border-hairline py-3.5 first:border-t-0 first:pt-0 sm:grid-cols-[minmax(0,15rem)_1fr]"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<code className="font-mono text-[12.5px] break-all text-primary">
|
||||
{setting.key}
|
||||
</code>
|
||||
<span className="text-[11px] uppercase tracking-[var(--tracking-label)] text-secondary">
|
||||
{setting.fallback}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[13px] leading-[21px] text-pretty text-secondary sm:mt-0 [&_code]:font-mono [&_code]:text-primary">
|
||||
{setting.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Terminal label="config.toml" lines={CONFIG_EXAMPLE} />
|
||||
<Callout title="editing it without an editor">
|
||||
<p>
|
||||
<Code>config path</Code> prints the file in use, <Code>config get</Code>{' '}
|
||||
reads one setting and <Code>config set</Code> writes one. Unknown keys are
|
||||
rejected rather than silently ignored, so a typo fails loudly instead of
|
||||
quietly doing nothing. <Code>--no-config</Code> ignores the file entirely
|
||||
and <Code>--config FILE</Code> points at another one.
|
||||
</p>
|
||||
</Callout>
|
||||
</div>
|
||||
</Cols>
|
||||
</Section>
|
||||
|
||||
<Section band>
|
||||
<SectionHead eyebrow="uninstall" title="Removing it completely.">
|
||||
Three things exist on disk: the binary, the config, and the state. Nothing else is
|
||||
|
||||
Reference in New Issue
Block a user