feat: Created a new .list <kind> REPL command to make discoveribility easier in the REPL
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use super::agent::AgentConfig;
|
||||||
use super::rag_cache::{RagCache, RagKey};
|
use super::rag_cache::{RagCache, RagKey};
|
||||||
use super::session::Session;
|
use super::session::Session;
|
||||||
use super::skill::{SKILL_SCAFFOLD, Skill};
|
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()
|
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<char> = 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<String>) -> Vec<(String, Option<String>)> {
|
||||||
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub enum RenderMode {
|
pub enum RenderMode {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -1911,6 +1943,90 @@ impl RequestContext {
|
|||||||
Ok(())
|
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<String> = 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<()> {
|
pub fn delete(&self, kind: &str) -> Result<()> {
|
||||||
let (dir, file_ext) = match kind {
|
let (dir, file_ext) = match kind {
|
||||||
"role" => (paths::roles_dir(), Some(".md")),
|
"role" => (paths::roles_dir(), Some(".md")),
|
||||||
@@ -2327,7 +2443,16 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
".rag" => super::map_completion_values(paths::list_rags()),
|
".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" => {
|
".install" => {
|
||||||
let mut values: Vec<String> =
|
let mut values: Vec<String> =
|
||||||
AssetCategory::NAMES.iter().map(|s| s.to_string()).collect();
|
AssetCategory::NAMES.iter().map(|s| s.to_string()).collect();
|
||||||
@@ -2391,6 +2516,9 @@ impl RequestContext {
|
|||||||
"skill",
|
"skill",
|
||||||
"agent-data",
|
"agent-data",
|
||||||
]),
|
]),
|
||||||
|
".list" => super::map_completion_values(vec![
|
||||||
|
"roles", "sessions", "agents", "rags", "macros", "skills",
|
||||||
|
]),
|
||||||
".vault" => {
|
".vault" => {
|
||||||
let mut values = vec!["add", "get", "update", "delete", "list"];
|
let mut values = vec!["add", "get", "update", "delete", "list"];
|
||||||
values.sort_unstable();
|
values.sort_unstable();
|
||||||
@@ -2415,9 +2543,9 @@ impl RequestContext {
|
|||||||
} else if (cmd == ".edit" && args.first() == Some(&"skill") && args.len() == 2)
|
} else if (cmd == ".edit" && args.first() == Some(&"skill") && args.len() == 2)
|
||||||
|| (cmd == ".skill" && args.first() == Some(&"load") && 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 {
|
} 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 {
|
} else if cmd == ".install" && args.first() == Some(&"remote") && args.len() >= 2 {
|
||||||
let prev = args.get(args.len() - 2).copied().unwrap_or("");
|
let prev = args.get(args.len() - 2).copied().unwrap_or("");
|
||||||
if prev == "--filter" {
|
if prev == "--filter" {
|
||||||
|
|||||||
+16
-3
@@ -52,7 +52,7 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
|
|||||||
4. Continue with the next pending item now. Call tools immediately."
|
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(".help", "Show this help guide", AssertState::pass()),
|
||||||
ReplCommand::new(".info", "Show system info", 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",
|
"Delete roles, sessions, RAGs, or agents",
|
||||||
AssertState::pass(),
|
AssertState::pass(),
|
||||||
),
|
),
|
||||||
|
ReplCommand::new(
|
||||||
|
".list",
|
||||||
|
"List roles, sessions, agents, RAGs, macros, or skills",
|
||||||
|
AssertState::pass(),
|
||||||
|
),
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".vault",
|
".vault",
|
||||||
"View or modify the Coyote vault",
|
"View or modify the Coyote vault",
|
||||||
@@ -1092,6 +1097,14 @@ pub async fn run_repl_command(
|
|||||||
println!("Usage: .delete <role|session|rag|macro|skill|agent-data>")
|
println!("Usage: .delete <role|session|rag|macro|skill|agent-data>")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
".list" => match args {
|
||||||
|
Some(args) => {
|
||||||
|
ctx.list_assets(args.trim())?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("Usage: .list <roles|sessions|agents|rags|macros|skills>")
|
||||||
|
}
|
||||||
|
},
|
||||||
".copy" => {
|
".copy" => {
|
||||||
let output = match ctx
|
let output = match ctx
|
||||||
.last_message
|
.last_message
|
||||||
@@ -1614,8 +1627,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_commands_has_52_entries() {
|
fn repl_commands_has_53_entries() {
|
||||||
assert_eq!(REPL_COMMANDS.len(), 52);
|
assert_eq!(REPL_COMMANDS.len(), 53);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user