Add list-skills + register-skill to colibri CLI

- DaemonClient: list_skills() and register_skill() methods
- CLI: list-skills and register-skill subcommands
- Parsing: --description and --category options for register-skill
- Usage text and examples updated

The daemon socket already had ListSkills and RegisterSkill commands;
this exposes them through the colibri CLI binary.
This commit is contained in:
Clawdie Operator 2026-06-04 12:12:19 +00:00
parent 3aee4637d8
commit 6e7c4c022b
2 changed files with 67 additions and 0 deletions

View file

@ -61,6 +61,8 @@ fn usage() -> &'static str {
colibri [--socket PATH] list-tasks [--status STATUS]
colibri [--socket PATH] create-task --title TEXT [--description TEXT]
colibri [--socket PATH] intake-task --title TEXT [--description TEXT] [--capability CAP]...
colibri [--socket PATH] list-skills
colibri [--socket PATH] register-skill NAME [--description TEXT] [--category CAT]
Socket defaults to COLIBRI_DAEMON_SOCKET, then the daemon's configured default.
@ -71,6 +73,8 @@ Examples:
colibri create-task --title "verify OSA smoke" --description "manual follow-up"
colibri intake-task --title "triage watchdog" --capability freebsd
colibri list-tasks --status queued
colibri register-skill freebsd-smoke --description "Live USB smoke test" --category freebsd
colibri list-skills
"#
}
@ -155,6 +159,19 @@ where
capabilities,
})
}
"list-skills" => expect_arity(&args, 1).map(|()| Command::ListSkills),
"register-skill" => {
if args.len() < 2 {
Err("register-skill requires NAME\n\n".to_string() + usage())
} else {
let (description, category) = parse_skill_options(&args[2..])?;
Ok(Command::RegisterSkill {
name: args[1].clone(),
description,
category,
})
}
}
other => Err(format!("unknown command: {other}\n\n{}", usage())),
}?;
@ -305,6 +322,32 @@ fn parse_spawn_options(args: &[String]) -> Result<(Option<String>, Option<String
Ok((session_id, system_prompt))
}
fn parse_skill_options(args: &[String]) -> Result<(Option<String>, Option<String>), String> {
let mut description = None;
let mut category = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--description" => {
let Some(value) = args.get(i + 1) else {
return Err("--description requires TEXT\n\n".to_string() + usage());
};
description = Some(value.clone());
i += 2;
}
"--category" => {
let Some(value) = args.get(i + 1) else {
return Err("--category requires CAT\n\n".to_string() + usage());
};
category = Some(value.clone());
i += 2;
}
other => return Err(format!("unknown register-skill option: {other}\n\n{}", usage())),
}
}
Ok((description, category))
}
async fn run(options: Options) -> Result<(), ClientError> {
let client = DaemonClient::new(options.socket_path);
match options.command {
@ -335,6 +378,12 @@ async fn run(options: Options) -> Result<(), ClientError> {
description,
capabilities,
} => print_json(&client.intake_task(title, description, capabilities).await?),
Command::ListSkills => print_json(&client.list_skills().await?),
Command::RegisterSkill {
name,
description,
category,
} => print_json(&client.register_skill(name, description, category).await?),
}
}

View file

@ -175,6 +175,24 @@ impl DaemonClient {
})
.await
}
pub async fn list_skills(&self) -> Result<serde_json::Value, ClientError> {
self.request(&HerdrCommand::ListSkills).await
}
pub async fn register_skill(
&self,
name: impl Into<String>,
description: Option<String>,
category: Option<String>,
) -> Result<serde_json::Value, ClientError> {
self.request(&HerdrCommand::RegisterSkill {
name: name.into(),
description,
category,
})
.await
}
}
#[cfg(test)]