diff --git a/src/config/request_context.rs b/src/config/request_context.rs index c72140f..57d9b02 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -1,3 +1,4 @@ +use super::agent::AgentConfig; use super::rag_cache::{RagCache, RagKey}; use super::session::Session; use super::skill::{SKILL_SCAFFOLD, Skill}; @@ -87,6 +88,37 @@ pub fn should_inject_skill_instructions(app: &AppConfig, policy: &SkillPolicy) - app.function_calling_support && policy.skills_enabled && !policy.compatible_enabled.is_empty() } +fn print_asset_names(kind: &str, names: &[String]) -> Result<()> { + if names.is_empty() { + println!("No {kind} found."); + return Ok(()); + } + + let mut header: Vec = kind.chars().collect(); + header[0] = header[0].to_ascii_uppercase(); + let header: String = header.into_iter().collect(); + + println!("{header}:"); + for name in names { + println!(" • {name}"); + } + + Ok(()) +} + +fn complete_skills_with_descriptions(names: Vec) -> Vec<(String, Option)> { + names + .into_iter() + .map(|name| { + let description = Skill::load(&name) + .ok() + .map(|s| s.description().to_string()) + .filter(|d| !d.is_empty()); + (name, description) + }) + .collect() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum RenderMode { #[default] @@ -1911,6 +1943,90 @@ impl RequestContext { Ok(()) } + pub fn list_assets(&self, kind: &str) -> Result<()> { + match kind { + "roles" => print_asset_names("roles", &paths::list_roles(true)), + "sessions" => print_asset_names("sessions", &self.list_sessions()), + "rags" => print_asset_names("RAGs", &paths::list_rags()), + "macros" => print_asset_names("macros", &paths::list_macros()), + "agents" => { + let names = list_agents(); + if names.is_empty() { + println!("No agents found."); + return Ok(()); + } + + println!("Agents:"); + for name in names { + let description = AgentConfig::load(&paths::agent_config_file(&name)) + .ok() + .map(|c| c.description) + .filter(|d| !d.is_empty()); + match description { + Some(description) => println!(" • {name} — {description}"), + None => println!(" • {name}"), + } + } + + Ok(()) + } + "skills" => { + let policy = SkillPolicy::effective( + &self.app.config, + self.role.as_ref(), + self.agent.as_ref(), + self.session.as_ref(), + )?; + + if !policy.skills_enabled { + bail!("Skills are disabled in this context"); + } + + let visible_names: Vec = match self.app.config.visible_skills.as_deref() { + Some(list) => list.to_vec(), + None => paths::list_skills(), + }; + + let mut entries = Vec::new(); + for name in visible_names { + if !policy.compatible_enabled.contains(&name) { + continue; + } + let skill = match Skill::load(&name) { + Ok(s) => s, + Err(e) => { + warn!("Failed to open skill '{name}' for listing: {e}"); + continue; + } + }; + + let loaded = self.skill_registry.is_loaded(skill.name()); + entries.push(( + skill.name().to_string(), + skill.description().to_string(), + loaded, + )); + } + + if entries.is_empty() { + println!("No skills found."); + return Ok(()); + } + + println!("Skills:"); + for (name, description, loaded) in entries { + let marker = if loaded { " (loaded)" } else { "" }; + println!(" • {name}{marker} — {description}"); + } + + Ok(()) + } + _ => bail!( + "Unknown kind '{kind}'. Valid kinds: roles, sessions, agents, rags, macros, skills" + ), + } + } + pub fn delete(&self, kind: &str) -> Result<()> { let (dir, file_ext) = match kind { "role" => (paths::roles_dir(), Some(".md")), @@ -2327,7 +2443,16 @@ impl RequestContext { } } ".rag" => super::map_completion_values(paths::list_rags()), - ".agent" => super::map_completion_values(list_agents()), + ".agent" => list_agents() + .into_iter() + .map(|name| { + let description = AgentConfig::load(&paths::agent_config_file(&name)) + .ok() + .map(|c| c.description) + .filter(|d| !d.is_empty()); + (name, description) + }) + .collect(), ".install" => { let mut values: Vec = AssetCategory::NAMES.iter().map(|s| s.to_string()).collect(); @@ -2391,6 +2516,9 @@ impl RequestContext { "skill", "agent-data", ]), + ".list" => super::map_completion_values(vec![ + "roles", "sessions", "agents", "rags", "macros", "skills", + ]), ".vault" => { let mut values = vec!["add", "get", "update", "delete", "list"]; values.sort_unstable(); @@ -2415,9 +2543,9 @@ impl RequestContext { } else if (cmd == ".edit" && args.first() == Some(&"skill") && args.len() == 2) || (cmd == ".skill" && args.first() == Some(&"load") && args.len() == 2) { - values = super::map_completion_values(paths::list_skills()); + values = complete_skills_with_descriptions(paths::list_skills()); } else if cmd == ".skill" && args.first() == Some(&"unload") && args.len() == 2 { - values = super::map_completion_values(self.skill_registry.loaded_names()); + values = complete_skills_with_descriptions(self.skill_registry.loaded_names()); } else if cmd == ".install" && args.first() == Some(&"remote") && args.len() >= 2 { let prev = args.get(args.len() - 2).copied().unwrap_or(""); if prev == "--filter" { diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 071e5aa..2e39797 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -52,7 +52,7 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {" 4. Continue with the next pending item now. Call tools immediately." }; -static REPL_COMMANDS: LazyLock<[ReplCommand; 52]> = LazyLock::new(|| { +static REPL_COMMANDS: LazyLock<[ReplCommand; 53]> = LazyLock::new(|| { [ ReplCommand::new(".help", "Show this help guide", AssertState::pass()), ReplCommand::new(".info", "Show system info", AssertState::pass()), @@ -269,6 +269,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 52]> = LazyLock::new(|| { "Delete roles, sessions, RAGs, or agents", AssertState::pass(), ), + ReplCommand::new( + ".list", + "List roles, sessions, agents, RAGs, macros, or skills", + AssertState::pass(), + ), ReplCommand::new( ".vault", "View or modify the Coyote vault", @@ -1092,6 +1097,14 @@ pub async fn run_repl_command( println!("Usage: .delete ") } }, + ".list" => match args { + Some(args) => { + ctx.list_assets(args.trim())?; + } + _ => { + println!("Usage: .list ") + } + }, ".copy" => { let output = match ctx .last_message @@ -1614,8 +1627,8 @@ mod tests { } #[test] - fn repl_commands_has_52_entries() { - assert_eq!(REPL_COMMANDS.len(), 52); + fn repl_commands_has_53_entries() { + assert_eq!(REPL_COMMANDS.len(), 53); } #[test]