///|
/// Skills loader (in-memory) shared across apps.
///|
pub struct SkillsLoader {
workspace_skills : Map[String, SkillEntry]
builtin_skills : Map[String, SkillEntry]
}
///|
pub fn SkillsLoader::new() -> SkillsLoader {
{ workspace_skills: {}, builtin_skills: {} }
}
///|
pub fn SkillsLoader::register_workspace_skill(
self : SkillsLoader,
name : String,
content : String,
path? : String = "",
) -> Unit {
let skill_path = if path.is_empty() {
"skills/" + name + "/SKILL.md"
} else {
path
}
self.workspace_skills[name] = SkillEntry::new(
name, skill_path, "workspace", content,
)
}
///|
pub fn SkillsLoader::register_builtin_skill(
self : SkillsLoader,
name : String,
content : String,
path? : String = "",
) -> Unit {
let skill_path = if path.is_empty() {
"builtin/skills/" + name + "/SKILL.md"
} else {
path
}
self.builtin_skills[name] = SkillEntry::new(
name, skill_path, "builtin", content,
)
}
///|
pub fn SkillsLoader::list_skills(
self : SkillsLoader,
filter_unavailable? : Bool = true,
bins? : Map[String, Bool] = {},
env? : Map[String, String] = {},
) -> Array[SkillInfo] {
let out : Array[SkillInfo] = []
for entry in self.workspace_skills.to_array() {
let skill = entry.1
let available = self.is_skill_available(skill.name, bins, env)
if not(filter_unavailable) || available {
out.push(SkillInfo::new(skill.name, skill.path, skill.source, available))
}
}
for entry in self.builtin_skills.to_array() {
let name = entry.0
if self.workspace_skills.contains(name) {
continue
}
let skill = entry.1
let available = self.is_skill_available(skill.name, bins, env)
if not(filter_unavailable) || available {
out.push(SkillInfo::new(skill.name, skill.path, skill.source, available))
}
}
out
}
///|
pub fn SkillsLoader::load_skill(self : SkillsLoader, name : String) -> String? {
match self.workspace_skills.get(name) {
Some(skill) => Some(skill.content)
None =>
match self.builtin_skills.get(name) {
Some(skill) => Some(skill.content)
None => None
}
}
}
///|
pub fn SkillsLoader::load_skills_for_context(
self : SkillsLoader,
skill_names : Array[String],
) -> String {
let parts : Array[String] = []
for name in skill_names {
match self.load_skill(name) {
Some(content) => {
let stripped = strip_skill_frontmatter(content)
parts.push("### Skill: " + name + "\n\n" + stripped)
}
None => ()
}
}
parts.join("\n\n---\n\n")
}
///|
pub fn SkillsLoader::build_skills_summary(
self : SkillsLoader,
bins? : Map[String, Bool] = {},
env? : Map[String, String] = {},
) -> String {
let all = self.list_skills(filter_unavailable=false, bins~, env~)
if all.is_empty() {
return ""
}
let lines : Array[String] = [""]
for skill in all {
lines.push(" ")
lines.push(" " + escape_xml(skill.name) + "")
lines.push(
" " +
escape_xml(self.get_skill_description(skill.name)) +
"",
)
lines.push(" " + escape_xml(skill.path) + "")
if not(skill.available) {
let missing = self.get_missing_requirements(skill.name, bins, env)
if not(missing.is_empty()) {
lines.push(" " + escape_xml(missing) + "")
}
}
lines.push(" ")
}
lines.push("")
lines.join("\n")
}
///|
pub fn SkillsLoader::get_always_skills(
self : SkillsLoader,
bins? : Map[String, Bool] = {},
env? : Map[String, String] = {},
) -> Array[String] {
let out : Array[String] = []
for skill in self.list_skills(filter_unavailable=true, bins~, env~) {
let metadata = self.get_skill_metadata(skill.name)
match metadata {
Some(meta) =>
if skill_metadata_is_always(Some(meta)) {
out.push(skill.name)
}
None => ()
}
}
out
}
///|
pub fn SkillsLoader::get_skill_metadata(
self : SkillsLoader,
name : String,
) -> Map[String, String]? {
match self.load_skill(name) {
Some(content) => parse_skill_frontmatter(content).0
None => None
}
}
///|
pub fn SkillsLoader::get_skill_description(
self : SkillsLoader,
name : String,
) -> String {
skill_metadata_description(self.get_skill_metadata(name), name)
}
///|
fn SkillsLoader::is_skill_available(
self : SkillsLoader,
name : String,
bins : Map[String, Bool],
env : Map[String, String],
) -> Bool {
match self.get_skill_metadata(name) {
Some(meta) => skill_metadata_is_available(Some(meta), bins~, env~)
None => true
}
}
///|
fn SkillsLoader::get_missing_requirements(
self : SkillsLoader,
name : String,
bins : Map[String, Bool],
env : Map[String, String],
) -> String {
match self.get_skill_metadata(name) {
Some(meta) => skill_metadata_missing_requirements(Some(meta), bins~, env~)
None => ""
}
}
///|
fn escape_xml(s : String) -> String {
s
.replace_all(old="&", new="&")
.replace_all(old="<", new="<")
.replace_all(old=">", new=">")
}
///|
fn bool_text(v : Bool) -> String {
if v {
"true"
} else {
"false"
}
}