// Filesystem helpers for the skills store. All cross-target via
// moonbitlang/x/fs; the recursive walk is built on @fs.read_dir +
// @fs.is_dir so no backend-specific FFI remains here.
///| Exclude list matches the pre-existing JS walker (.git etc).
fn sks_excluded(name : String) -> Bool {
name == ".git" || name == ".github" || name == ".hub"
}
///| Skills fixture dir used by inline tests. Relative to cwd, matching
///| the previous JS resolution (`path.join(process.cwd(), ...)`).
fn ffi_skills_fixtures_path() -> String {
"./proto/fixtures/skills"
}
///| Recursively collect all `SKILL.md` files under `dir`, skipping
///| excluded directory names. Missing roots return an empty list
///| (no error) so callers can treat "no skills" uniformly.
fn ffi_list_skill_files(dir : String) -> Array[String] {
let results : Array[String] = []
sks_walk(dir, results)
results
}
///| Walker helper — split out to keep ffi_list_skill_files's signature
///| stable and to allow recursion without polluting the top-level API.
fn sks_walk(current : String, out : Array[String]) -> Unit {
if !@fs.path_exists(current) {
return
}
let entries = try {
@fs.read_dir(current)
} catch {
_ => return
}
for name in entries {
if sks_excluded(name) {
continue
}
let full = current + "/" + name
let is_d = (try? @fs.is_dir(full)).unwrap_or(false)
if is_d {
sks_walk(full, out)
} else if name == "SKILL.md" {
out.push(full)
}
}
}
///| Read a SKILL.md. Missing / unreadable → "".
fn ffi_read_skill_file(path : String) -> String {
(try? @fs.read_file_to_string(path)).unwrap_or("")
}
///| Derive a category from a skill file path. Mirrors the previous
///| relative-path heuristic: `skills///SKILL.md` → cat,
///| `skills//SKILL.md` → "".
fn ffi_infer_category(skill_path : String, skills_dir : String) -> String {
// Strip the skills_dir prefix (plus any trailing slash) to get
// the skill-relative path, then split by "/".
let prefix = if skills_dir.has_suffix("/") {
skills_dir
} else {
skills_dir + "/"
}
let rel = if skill_path.has_prefix(prefix) {
skill_path[prefix.length():].to_string()
} else {
skill_path
}
let parts = rel.split("/").filter(fn(s) { !s.is_empty() }).to_array()
if parts.length() >= 3 {
parts[0].to_string()
} else {
""
}
}
///|
pub struct SkillEntry {
name : String
skill_name : String
description : String
category : String // "" = no category = treated as "general" in prompt
content : String
frontmatter : SkillFrontmatter
}
///|
pub enum ViewResult {
ViewOk(String, String) // name, content
ViewErr(String) // error message
}
///|
pub struct SkillsStore {
skills : Array[SkillEntry]
}
let max_name_len : Int = 64
let max_desc_len : Int = 1024
///|
fn load_from_dir(dir : String, seen_names : Array[String]) -> Array[SkillEntry] {
let files = ffi_list_skill_files(dir)
let entries : Array[SkillEntry] = []
for file_path in files {
let content = ffi_read_skill_file(file_path)
if content.length() == 0 { continue }
let fm = parse_frontmatter(content)
// Extract parent directory name from path (second to last path component)
let parts : Array[String] = file_path.split("/").map(fn(sv) { sv.to_string() }).to_array()
let parts_len = parts.length()
let skill_dir = if parts_len >= 2 {
parts[parts_len - 2]
} else {
"unknown"
}
let raw_name = match fm.name {
Some(n) => n
None => skill_dir
}
let name = if raw_name.length() > max_name_len {
raw_name[0:max_name_len].to_string()
} else {
raw_name
}
if seen_names.contains(name) { continue }
let raw_desc = match fm.description {
Some(d) => d
None => ""
}
let description = if raw_desc.length() > max_desc_len {
raw_desc[0:max_desc_len - 3].to_string() + "..."
} else {
raw_desc
}
seen_names.push(name)
let category = ffi_infer_category(file_path, dir)
entries.push({
name,
skill_name: skill_dir,
description,
category,
content,
frontmatter: fm,
})
}
entries
}
///|
pub fn skills_store_load(skills_dir : String, external_dirs : Array[String]) -> SkillsStore {
let seen : Array[String] = []
let all : Array[SkillEntry] = []
for entry in load_from_dir(skills_dir, seen) {
all.push(entry)
}
for ext_dir in external_dirs {
for entry in load_from_dir(ext_dir, seen) {
all.push(entry)
}
}
{ skills: all }
}
///| Platform normalization: "mac" | "macos" → "darwin", "linux" → "linux", "windows" → "win32"
fn normalize_platform(hint : String) -> String {
let lower = hint.to_lower()
if lower == "mac" || lower == "macos" { "darwin" }
else if lower == "linux" { "linux" }
else if lower == "windows" { "win32" }
else { lower }
}
///|
fn skill_platform_ok(fm : SkillFrontmatter, platform_hint : Option[String]) -> Bool {
if fm.platforms.length() == 0 { return true }
match platform_hint {
None => true
Some(hint) => {
let hint_norm = normalize_platform(hint)
let mut ok = false
for p in fm.platforms {
let p_norm = normalize_platform(p)
if hint_norm.has_prefix(p_norm) || p_norm.has_prefix(hint_norm) {
ok = true
}
}
ok
}
}
}
///|
fn skill_should_show(
fm : SkillFrontmatter,
available_tools : Option[Array[String]],
available_toolsets : Option[Array[String]],
platform_hint : Option[String]
) -> Bool {
if !skill_platform_ok(fm, platform_hint) { return false }
// No tool/toolset filtering when both are absent (backward compat with proto)
if available_tools is None && available_toolsets is None { return true }
let at : Array[String] = match available_tools {
None => []
Some(arr) => arr
}
let ats : Array[String] = match available_toolsets {
None => []
Some(arr) => arr
}
// fallback_for: hide when primary tool IS available
for t in fm.fallback_for_tools {
if at.contains(t) { return false }
}
for ts in fm.fallback_for_toolsets {
if ats.contains(ts) { return false }
}
// requires: hide when required tool is NOT available
for t in fm.requires_tools {
if !at.contains(t) { return false }
}
for ts in fm.requires_toolsets {
if !ats.contains(ts) { return false }
}
true
}
///|
pub fn skills_store_list(
store : SkillsStore,
available_tools : Option[Array[String]],
platform_hint : Option[String],
available_toolsets~ : Option[Array[String]] = None,
disabled~ : Array[String] = []
) -> Array[SkillEntry] {
store.skills.filter(fn(s) {
if disabled.length() > 0 && (disabled.contains(s.name) || disabled.contains(s.skill_name)) {
return false
}
skill_should_show(s.frontmatter, available_tools, available_toolsets, platform_hint)
})
}
///| Find a SkillEntry by name field
fn find_skill_by_name(skills : Array[SkillEntry], name : String) -> Option[SkillEntry] {
let mut found : Option[SkillEntry] = None
for s in skills {
if found is None && s.name == name {
found = Some(s)
}
}
found
}
///| Find a SkillEntry by skill_name field
fn find_skill_by_skill_name(skills : Array[SkillEntry], skill_name : String) -> Option[SkillEntry] {
let mut found : Option[SkillEntry] = None
for s in skills {
if found is None && s.skill_name == skill_name {
found = Some(s)
}
}
found
}
///|
pub fn skills_store_view(store : SkillsStore, name : String) -> ViewResult {
match find_skill_by_name(store.skills, name) {
Some(s) => ViewOk(s.name, s.content)
None =>
match find_skill_by_skill_name(store.skills, name) {
Some(s) => ViewOk(s.name, s.content)
None => ViewErr("Skill '" + name + "' not found.")
}
}
}
///| Find index of category in categories array; returns -1 if not found
fn find_category_index(categories : Array[String], cat : String) -> Int {
let mut idx = -1
for i, c in categories {
if idx == -1 && c == cat {
idx = i
}
}
idx
}
///|
pub fn skills_store_build_prompt(
store : SkillsStore,
available_tools : Option[Array[String]],
platform_hint : Option[String],
disabled~ : Array[String] = []
) -> String {
let visible = skills_store_list(
store, available_tools, platform_hint, disabled=disabled,
)
if visible.length() == 0 { return "" }
// Group by category
// Use Array of (category, Array of (name, desc)) pairs — no Map available
let categories : Array[String] = []
let by_cat : Array[(String, Array[(String, String)])] = []
for skill in visible {
let cat = if skill.category.length() == 0 { "general" } else { skill.category }
let idx = find_category_index(categories, cat)
if idx >= 0 {
by_cat[idx].1.push((skill.name, skill.description))
} else {
categories.push(cat)
by_cat.push((cat, [(skill.name, skill.description)]))
}
}
// Sort categories alphabetically
// Simple insertion sort (no stdlib sort needed for small N)
let n = categories.length()
for i in 1..= 0 && categories[j] > key_cat {
categories[j + 1] = categories[j]
by_cat[j + 1] = by_cat[j]
j = j - 1
}
categories[j + 1] = key_cat
by_cat[j + 1] = key_entry
}
let lines : Array[String] = []
for i in 0..= 0 && sorted_skills[sj].0 > key.0 {
sorted_skills[sj + 1] = sorted_skills[sj]
sj = sj - 1
}
sorted_skills[sj + 1] = key
}
for skill_pair in sorted_skills {
let sname = skill_pair.0
let sdesc = skill_pair.1
if sdesc.length() > 0 {
lines.push(" - " + sname + ": " + sdesc)
} else {
lines.push(" - " + sname)
}
}
}
"## Skills (mandatory)\n" +
"Before replying, scan the skills below. If a skill matches or is even partially relevant " +
"to your task, you MUST load it with skill_view(name) and follow its instructions.\n" +
"\n" +
"\n" +
lines.join("\n") +
"\n\n" +
"\n" +
"Only proceed without loading a skill if genuinely none are relevant to the task."
}
///| Tests
test "skills_store: load from fixture dir" {
let dir = ffi_skills_fixtures_path()
let store = skills_store_load(dir, [])
let skills = skills_store_list(store, None, None)
assert_eq(skills.length(), 3)
}
test "skills_store: filter hides fallback skill when tool available" {
let dir = ffi_skills_fixtures_path()
let store = skills_store_load(dir, [])
let with_tool = skills_store_list(store, Some(["docker_build_tool"]), None)
let hidden = find_skill_by_name(with_tool, "docker-build")
match hidden {
Some(_) => fail("expected docker-build to be hidden")
None => ()
}
}
test "skills_store: view returns content for known skill" {
let dir = ffi_skills_fixtures_path()
let store = skills_store_load(dir, [])
let result = skills_store_view(store, "tdd-workflow")
match result {
ViewOk(name, content) => {
assert_eq(name, "tdd-workflow")
assert_eq(content.contains("Red"), true)
}
ViewErr(_) => fail("expected ViewOk")
}
}
test "skills_store: build_prompt contains skill names" {
let dir = ffi_skills_fixtures_path()
let store = skills_store_load(dir, [])
let prompt = skills_store_build_prompt(store, None, None)
assert_eq(prompt.contains("tdd-workflow"), true)
assert_eq(prompt.contains("## Skills (mandatory)"), true)
}
test "skills_store: disabled excludes skill by frontmatter name" {
let dir = ffi_skills_fixtures_path()
let store = skills_store_load(dir, [])
let before = skills_store_list(store, None, None)
let after = skills_store_list(store, None, None, disabled=["tdd-workflow"])
assert_eq(after.length(), before.length() - 1)
match find_skill_by_name(after, "tdd-workflow") {
Some(_) => fail("expected tdd-workflow to be disabled")
None => ()
}
}
test "skills_store: disabled unknown name is a no-op" {
let dir = ffi_skills_fixtures_path()
let store = skills_store_load(dir, [])
let before = skills_store_list(store, None, None)
let after = skills_store_list(store, None, None, disabled=["nonexistent"])
assert_eq(after.length(), before.length())
}
test "skills_store: build_prompt respects disabled" {
let dir = ffi_skills_fixtures_path()
let store = skills_store_load(dir, [])
let prompt = skills_store_build_prompt(
store, None, None, disabled=["tdd-workflow"],
)
assert_eq(prompt.contains("tdd-workflow"), false)
}