diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 2d651615..d1d0511a 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -41,6 +41,7 @@ pub mod orchestrator; pub mod org; pub mod project; pub mod prompt; +pub mod skill; pub mod system_agents; pub mod wrap; @@ -56,5 +57,6 @@ pub use orchestrator::OrchestratorCommand; pub use org::OrgCommand; pub use project::ProjectCommand; pub use prompt::PromptCommand; +pub use skill::SkillCommand; pub use system_agents::SystemAgentsCommand; pub use wrap::WrapCommand; diff --git a/crates/cli/src/commands/skill.rs b/crates/cli/src/commands/skill.rs new file mode 100644 index 00000000..4da0e9a7 --- /dev/null +++ b/crates/cli/src/commands/skill.rs @@ -0,0 +1,143 @@ +//! Skill management commands for the agentd CLI. +//! +//! Provides `agent skill list` and `agent skill show` for inspecting the skills +//! available to agents. Skills are discovered from the filesystem directly — +//! no orchestrator service call is required. +//! +//! # Discovery paths +//! +//! 1. `.agentd/skills/` — project-level (relative to the current working directory) +//! 2. `~/.config/agentd/skills/` — user-level fallback +//! +//! # Examples +//! +//! ```bash +//! # List all available skills +//! agent skill list +//! +//! # List as JSON (for scripting) +//! agent skill list --json +//! +//! # Show the full content of a skill +//! agent skill show git-spice +//! +//! # Show skill as JSON +//! agent skill show git-spice --json +//! ``` + +use anyhow::{bail, Result}; +use clap::Subcommand; +use colored::*; + +/// Skill management subcommands. +#[derive(Subcommand)] +pub enum SkillCommand { + /// List all discoverable skills from .agentd/skills/ + /// + /// Scans the project-level `.agentd/skills/` directory and the user-level + /// `~/.config/agentd/skills/` directory. Prints a formatted table with + /// each skill's name and description. + /// + /// Use `--json` for machine-readable output. + List, + + /// Show the full content of a named skill + /// + /// Prints the Markdown content (including frontmatter) of the named skill + /// to stdout. + Show { + /// Name of the skill to display + name: String, + }, +} + +impl SkillCommand { + pub async fn execute(&self, json: bool) -> Result<()> { + match self { + SkillCommand::List => list_skills(json), + SkillCommand::Show { name } => show_skill(name, json), + } + } +} + +// --------------------------------------------------------------------------- +// list +// --------------------------------------------------------------------------- + +fn list_skills(json: bool) -> Result<()> { + let skills = orchestrator::skills::discover_all_skills(); + + if json { + println!("{}", serde_json::to_string_pretty(&skills)?); + return Ok(()); + } + + if skills.is_empty() { + println!("{}", "No skills found.".yellow()); + println!( + "Create skills in {} or {}", + ".agentd/skills/".cyan(), + "~/.config/agentd/skills/".cyan() + ); + return Ok(()); + } + + // Compute column widths. + let name_width = skills.iter().map(|s| s.name.len()).max().unwrap_or(4).max(4); + + println!("{}", "agentd Skills".bold()); + println!("{}", "=".repeat(60)); + + for skill in &skills { + let desc = skill.description.as_deref().unwrap_or(""); + println!(" {: Result<()> { + let skills = orchestrator::skills::discover_all_skills(); + + let skill = match skills.into_iter().find(|s| s.name == name) { + Some(s) => s, + None => { + bail!("Skill '{}' not found. Run 'agent skill list' to see available skills.", name) + } + }; + + if json { + println!("{}", serde_json::to_string_pretty(&skill)?); + return Ok(()); + } + + println!("{}", skill.content); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_skill_command_list_variant_exists() { + // Confirm that the SkillCommand enum can be constructed — compile-time check. + let _cmd = SkillCommand::List; + } + + #[test] + fn test_skill_command_show_variant_exists() { + let _cmd = SkillCommand::Show { name: "git-spice".to_string() }; + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 70ef4583..ba0eb938 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -87,8 +87,8 @@ use commands::index::IndexClient; use commands::IndexCommand; use commands::{ AskCommand, AuthCommand, CommunicateCommand, ConfigCommand, MemoryCommand, NotifyCommand, - OrchestratorCommand, OrgCommand, ProjectCommand, PromptCommand, SystemAgentsCommand, - WrapCommand, + OrchestratorCommand, OrgCommand, ProjectCommand, PromptCommand, SkillCommand, + SystemAgentsCommand, WrapCommand, }; use communicate::client::CommunicateClient; use memory::client::MemoryClient; @@ -404,6 +404,25 @@ enum Commands { #[command(subcommand)] command: Box, }, + + /// Manage agent skills from .agentd/skills/ + /// + /// List and inspect skills that can be assigned to agents via the `skills` + /// field in agent templates. Skills are discovered from project-level + /// `.agentd/skills/` and user-level `~/.config/agentd/skills/`. + /// + /// # Examples + /// + /// ```bash + /// agent skill list + /// agent skill list --json + /// agent skill show git-spice + /// agent skill show git-spice --json + /// ``` + Skill { + #[command(subcommand)] + command: SkillCommand, + }, } /// Main entry point for the agent CLI. @@ -551,6 +570,9 @@ async fn main() -> Result<()> { let client = OrchestratorClient::new(url); command.execute(&client, cli.json).await?; } + Commands::Skill { command } => { + command.execute(cli.json).await?; + } } Ok(())