listSkills — inventory and skip reasons
Rust · crate toolnexus · SPEC §3
pub fn list_skills(opts: &LoadSkillsOptions) -> SkillInventorypub struct SkillInventory { pub skills: Vec<SkillInfo>, pub skipped: Vec<SkillSkip> }Enumerate discovered skills and, crucially, the ones that were skipped and why.
When to use it
Section titled “When to use it”To show what was found and why something was skipped (missing name, malformed frontmatter, duplicate, unreadable) — for a settings screen or a lint step — without building a tool.
Why this and not the alternative
Section titled “Why this and not the alternative”Use this rather than load_skills_with(..).skipped when you do not want the tool or the filter applied: it is the unfiltered inventory.
Examples
Section titled “Examples”1. Inventory the shared fixture
Section titled “1. Inventory the shared fixture”use std::path::Path;use toolnexus::skills::{LoadSkillsOptions, list_skills};
fn main() -> Result<(), Box<dyn std::error::Error>> { // TOOLNEXUS_REPO is set by the docs test runner; in your own code just use a path. let dir = Path::new(&std::env::var("TOOLNEXUS_REPO")?).join("examples/skills");
let inv = list_skills(&LoadSkillsOptions { dirs: vec![dir], ..Default::default() });
assert_eq!(inv.skills.len(), 1); assert_eq!(inv.skills[0].name, "hello-world"); // A clean directory skips nothing. assert!(inv.skipped.is_empty(), "{:?}", inv.skipped);
println!("ok: {} | skipped: {}", inv.skills[0].name, inv.skipped.len()); Ok(())}2. The reason a SKILL.md did not load
Section titled “2. The reason a SKILL.md did not load”Four typed reasons, and this is how each one shows up.
use std::path::Path;use toolnexus::skills::{LoadSkillsOptions, SkillSkipReason, list_skills};
fn write(root: &Path, name: &str, body: &str) { std::fs::create_dir_all(root.join(name)).unwrap(); std::fs::write(root.join(name).join("SKILL.md"), body).unwrap();}
fn main() { let root = std::env::temp_dir().join(format!("skills-list-{}", std::process::id())); write(&root, "good", "---\nname: good\ndescription: Fine.\n---\n\nBody.\n"); // Fences present, YAML invalid — and `[` opens a construct the lenient // rescue refuses, so no name can be recovered. write(&root, "broken", "---\nname: [oops\n---\n\nBody.\n"); // Parses, but has no name — nothing to call it by. write(&root, "unnamed", "---\ndescription: No name here.\n---\n\nBody.\n");
let inv = list_skills(&LoadSkillsOptions { dirs: vec![root.clone()], ..Default::default() }); std::fs::remove_dir_all(&root).ok();
assert_eq!(inv.skills.len(), 1); assert_eq!(inv.skills[0].name, "good"); let reasons: Vec<SkillSkipReason> = inv.skipped.iter().map(|s| s.reason).collect(); assert!(reasons.contains(&SkillSkipReason::MalformedFrontmatter), "{:?}", inv.skipped); assert!(reasons.contains(&SkillSkipReason::MissingName), "{:?}", inv.skipped); // Reasons are typed variants with stable string values. assert_eq!(SkillSkipReason::MalformedFrontmatter.as_str(), "malformed-frontmatter");
println!("ok: 1 skill, {} skips", inv.skipped.len());}3. Every source, duplicate detection, and authoring the allowlist
Section titled “3. Every source, duplicate detection, and authoring the allowlist”Directories and data skills go into the same inventory, and the second skill to claim a name is
reported as duplicate-name — first one wins.
use std::collections::HashMap;use std::path::Path;use toolnexus::skills::{LoadSkillsOptions, SkillDef, SkillSkipReason, list_skills, load_skills_with};
fn main() -> Result<(), Box<dyn std::error::Error>> { let shared = Path::new(&std::env::var("TOOLNEXUS_REPO")?).join("examples/skills");
let opts = LoadSkillsOptions { dirs: vec![shared], skills: vec![ SkillDef { name: "deploy".into(), description: "Ship the service to prod.".into(), content: "Run the taskfile.".into(), resources: vec!["skill://deploy/checklist.md".into()], ..Default::default() }, // Collides with the fixture on disk — dirs are collected first, so this loses. SkillDef { name: "hello-world".into(), description: "A shadow.".into(), content: "Ignored.".into(), ..Default::default() }, // No name at all. SkillDef { description: "Anonymous.".into(), content: "Ignored.".into(), ..Default::default() }, ], // filter is accepted but has NO effect on the inventory. filter: HashMap::from([("deploy".to_string(), true)]), ..Default::default() };
let inv = list_skills(&opts);
let mut names: Vec<&str> = inv.skills.iter().map(|s| s.name.as_str()).collect(); names.sort(); assert_eq!(names, ["deploy", "hello-world"], "unfiltered inventory expected");
let count = |r| inv.skipped.iter().filter(|s| s.reason == r).count(); assert_eq!(count(SkillSkipReason::DuplicateName), 1); assert_eq!(count(SkillSkipReason::MissingName), 1);
// The fixture on disk won: hello-world is still the fs-sourced one. for s in &inv.skills { match s.name.as_str() { "hello-world" => assert_eq!(s.origin, "fs"), "deploy" => assert_eq!(s.origin, "logical"), _ => {} } }
// Now author the allowlist from what you actually found, and load for real. let allow: HashMap<String, bool> = inv.skills.iter().map(|s| (s.name.clone(), s.name == "deploy")).collect(); let src = load_skills_with(LoadSkillsOptions { filter: allow, ..opts.clone() }); assert_eq!(src.skills.len(), 1);
println!("ok: {names:?} | skipped: {}", inv.skipped.len()); Ok(())}Options and results
Section titled “Options and results”LoadSkillsOptions — the same struct load_skills_with takes:
| Field | Type | Effect on list_skills |
|---|---|---|
dirs |
Vec<PathBuf> |
Roots walked for **/SKILL.md. A missing root contributes nothing. |
skills |
Vec<SkillDef> |
Skills supplied as data. Collected after dirs, so directories win a name clash. |
filter |
HashMap<String, bool> |
Ignored — the inventory is always unfiltered. |
sample_limit |
i64 |
Ignored — no tool is built, so nothing is sampled. |
SkillSkipReason — the typed values in skipped:
| Variant | as_str() |
Means |
|---|---|---|
MissingName |
missing-name |
Frontmatter parsed but carried no name (including “no frontmatter at all”). |
MalformedFrontmatter |
malformed-frontmatter |
--- fences present, YAML inside failed to parse. |
DuplicateName |
duplicate-name |
Another candidate already claimed that name; the first one is kept. |
Unreadable |
unreadable |
The SKILL.md could not be read (permissions, broken link). |
See also
Section titled “See also”load_skills— Glob a skills directory and expose one skill tool with progressive disclosure.load_skills_with— Supply skills as data or from a lazy provider, and narrow them per agent with an allowlist.