feat(cli): ship the agent skill inside the binary

A `cargo install` leaves a binary and nothing else, so there is no file on
disk for a coding agent to read. `skills install` writes an embedded SKILL.md
into every agent found on the machine — Claude Code and opencode today, any
other via --dir — and installing over an older copy is what updating means.

The skill carries what --help cannot: the JSON keys of every record, what each
exit code means, and the rule that an agent always passes --json. A unit test
walks the clap command tree and holds SKILL.md to a documented entry for every
command, so a new subcommand fails the build until it is written up.
This commit is contained in:
Michel de Graaf
2026-07-25 02:17:11 +02:00
parent 03e8924a6b
commit 95bc18672d
7 changed files with 543 additions and 3 deletions
+45
View File
@@ -26,6 +26,7 @@ Examples:
soulseek-rs room listen lobby --follow stream room chat
soulseek-rs serve --follow --json share files, stream upload events
soulseek-rs user someuser is this peer online and worth it
soulseek-rs skills install teach your coding agent this CLI
Exit codes:
0 success 2 usage/config 3 connect or login 4 no results
@@ -193,6 +194,32 @@ pub enum Commands {
/// Check whether the router opens the listen port (UPnP/NAT-PMP)
Portmap,
/// Teach a local coding agent this command surface
#[command(subcommand)]
Skills(SkillsCommand),
}
#[derive(Subcommand, Debug)]
pub enum SkillsCommand {
/// Write the skill into every agent found on this machine, replacing an
/// older copy
Install(SkillsArgs),
/// Remove the skill again
Uninstall(SkillsArgs),
/// Show where the skill goes and what is there now
List(SkillsArgs),
}
#[derive(Args, Debug)]
pub struct SkillsArgs {
/// Skills directory to use instead of the agents found on this machine;
/// repeat for several. Use `--dir .claude/skills` to commit the skill
/// alongside a project
#[arg(long, value_name = "PATH", action = ArgAction::Append)]
pub dir: Vec<PathBuf>,
}
#[derive(Args, Debug)]
@@ -588,6 +615,24 @@ mod tests {
));
}
#[test]
fn skills_dir_repeats_into_a_list() {
let cli = parse(&[
"soulseek-rs",
"skills",
"install",
"--dir",
"/a",
"--dir",
"/b",
]);
let Some(Commands::Skills(SkillsCommand::Install(args))) = cli.command
else {
panic!("expected skills install");
};
assert_eq!(args.dir, [PathBuf::from("/a"), PathBuf::from("/b")]);
}
#[test]
fn get_defaults_to_picking_the_best_result() {
let cli = parse(&["soulseek-rs", "get", "query"]);