Agent Skills for MoonBit: discover skill directories, read SKILL.md, validate it against the specification.
moon add moonbitstack/moonskillimport {
"moonbitstack/moonskill",
"moonbitstack/moonskill/fs",
}---
name: pdf-tools
description: Extract text from PDFs. Use when a task hands you a PDF.
---
# PDF tools
Run `scripts/extract.py` on the file.test {
let text = "---\nname: pdf-tools\ndescription: Extract text from PDFs.\n---\n# PDF tools\n"
let skill = @moonskill.parse(text, root="pdf-tools")
inspect(skill.name, content="pdf-tools")
inspect(skill.body, content="# PDF tools")
}test {
let text = "---\nname: PDF_Tools\ndescription: Extract text from PDFs.\n---\nBody\n"
let faults = @moonskill.faults(text, root="pdf-tools")
inspect(faults.length(), content="3")
for fault in faults {
println(fault.kind() + ": " + fault.message())
}
}name-not-lowercase: Skill name 'PDF_Tools' must be lowercase
name-bad-character: Skill name 'PDF_Tools' contains invalid characters. Only letters, digits, and hyphens are allowed.
name-mismatch: Directory name 'pdf-tools' must match skill name 'PDF_Tools'async test {
let tree = @fs.Dir::at("docs/skills")
let skills = @moonskill.discover(tree)
for skill in skills {
println(skill.summary())
}
}test {
let skills : Array[@moonskill.Skill] = []
let block = @moonskill.prompt(skills)
inspect(block, content="<available_skills>\n</available_skills>")
}| Tier | What it is | How to read it |
|---|---|---|
| 1 | name and description of every skill | Skill::summary, prompt |
| 2 | the body of the chosen skill | Skill::body |
| 3 | a file the body refers to | Skill::resolve then Source::read |
async test {
let tree = @fs.Dir::at("docs/skills")
let skills = @moonskill.discover(tree)
guard skills.length() > 0 else { return }
let skill = skills[0]
if skill.resolve("SKILL.md") is Some(path) {
let bytes = tree.read(path)
println("\{bytes.length()} bytes")
}
}moon run src/cmd/skillcheck --target native -- docs/skills/moonskill
ok docs/skills/moonskillmoon run src/cmd/skillcheck --target native -- docs/skills/broken
FAIL docs/skills/broken
- name-not-lowercase: Skill name 'Broken' must be lowercase| Item | Signature |
|---|---|
| parse | parse(text : StringView, root? : String) -> Skill raise Refused |
| validate | validate(skill : Skill, limits~ : Limits = limits) -> Array[Fault] |
| faults | faults(text : StringView, root? : String, limits~ : Limits = limits) -> Array[Fault] |
| discover | async discover(source : &Source) -> Array[Skill] raise |
| load | async load(source : &Source, root~ : String) -> Skill raise |
| prompt | prompt(skills : ArrayView[Skill]) -> String |
| Source | async list(Self) -> Array[String] raise, async read(Self, path : String) -> Bytes raise |
| Skill | name, description, body, meta : Map[String, Json], root |
| Skill::summary | Skill::summary(Skill) -> String |
| Skill::location | Skill::location(Skill) -> String |
| Skill::resolve | Skill::resolve(Skill, StringView) -> String? |
| Skill::license | Skill::license(Skill) -> String? |
| Skill::compatibility | Skill::compatibility(Skill) -> String? |
| Skill::allowed_tools | Skill::allowed_tools(Skill) -> String? |
| Skill::metadata | Skill::metadata(Skill) -> Map[String, String]? |
| Fault | message, kind |
| Limits | name, description, compatibility, with limits as the preset |
| @fs.Dir | Dir::at(StringView) -> Dir, Dir::path(Dir) -> String |
| @fs.load | async load(StringView) -> Skill raise |
| @fs.check | async check(StringView) -> Array[Fault] raise |
test {
let text = "---\nname: my-skill\ndescription: A description long enough to break.\n---\nBody\n"
let tight : @moonskill.Limits = { name: 64, description: 8, compatibility: 500 }
inspect(@moonskill.faults(text, root="my-skill", limits=tight).length(), content="1")
}pub(open) trait Source {
async fn list(Self) -> Array[String]
async fn read(Self, path : String) -> Bytes
}pub(all) enum Fault {
UnexpectedFields(Array[String])
MissingName
NameEmpty
NameTooLong(String, Int, Int)
NameNotLowercase(String)
NameEdgeHyphen
NameDoubleHyphen
NameBadCharacter(String)
NameMismatch(String, String)
MissingDescription
DescriptionEmpty
DescriptionTooLong(Int, Int)
CompatibilityNotText
CompatibilityTooLong(Int, Int)
Malformed(Refused)
MissingPath(String)
NotADirectory(String)
MissingDocument
} derive(Eq, Debug)pub(all) struct Limits {
name : Int
description : Int
compatibility : Int
}fn base_name(path : StringView) -> Stringfn join_path(a : StringView, b : StringView) -> StringInstall
Download zipAgent Skills for MoonBit: discover skill directories, read SKILL.md, validate it against the specification.