///|
const MOONGATE_SKILLS_STATE_FILE = "skills_state.json"

///|
const MOONGATE_SKILLS_BACKUP_RETAIN_COUNT = 20

///|
fn moon_skill_supported_apps() -> Array[String] {
  ["claude", "codex", "gemini", "opencode", "hermes"]
}

///|
fn moon_skill_state_path() -> String {
  @pathx.join(moon_app_config_dir(), MOONGATE_SKILLS_STATE_FILE)
}

///|
fn moon_skill_backup_dir() -> String {
  @pathx.join(moon_app_config_dir(), "skill-backups")
}

///|
fn moon_skill_default_repos_json() -> Array[Json] {
  [
    moon_skill_repo_json("anthropics", "skills", "main", true),
    moon_skill_repo_json("ComposioHQ", "awesome-claude-skills", "master", true),
    moon_skill_repo_json("cexll", "myclaude", "master", true),
    moon_skill_repo_json("JimLiu", "baoyu-skills", "main", true),
  ]
}

///|
fn moon_skill_repo_json(
  owner : String,
  name : String,
  branch : String,
  enabled : Bool,
) -> Json {
  {
    "owner": Json::string(owner),
    "name": Json::string(name),
    "branch": Json::string(branch),
    "enabled": Json::boolean(enabled),
  }.to_json()
}

///|
fn moon_skill_apps_json(app? : String? = None) -> Json {
  let apps : Map[String, Json] = Map([], capacity=0)
  for name in moon_skill_supported_apps() {
    apps[name] = Json::boolean(false)
  }
  match app {
    Some(value) => {
      let normalized = moon_normalize_app_type(value)
      if moon_skill_supported_apps().contains(normalized) {
        apps[normalized] = Json::boolean(true)
      }
    }
    None => ()
  }
  Json::object(apps)
}

///|
fn moon_skill_state_default_json() -> Json {
  {
    "installed": Json::array([]),
    "repos": Json::array(moon_skill_default_repos_json()),
    "backups": Json::array([]),
    "skillStorageLocation": Json::string("unified"),
  }.to_json()
}

///|
async fn moon_skill_state_load(
  path? : String = moon_skill_state_path(),
) -> Json {
  let content = @fs.read_file(path).text() catch {
    _ => return moon_skill_state_default_json()
  }
  let parsed = @json.parse(content) catch {
    _ => return moon_skill_state_default_json()
  }
  guard parsed is Object(obj) else { return moon_skill_state_default_json() }
  if obj.get("repos") is None {
    obj["repos"] = Json::array(moon_skill_default_repos_json())
  }
  if obj.get("installed") is None {
    obj["installed"] = Json::array([])
  }
  if obj.get("backups") is None {
    obj["backups"] = Json::array([])
  }
  if obj.get("skillStorageLocation") is None {
    obj["skillStorageLocation"] = Json::string("unified")
  }
  Json::object(obj)
}

///|
async fn moon_skill_state_save(
  state : Json,
  path? : String = moon_skill_state_path(),
) -> Bool {
  moon_skill_make_directory(@pathx.dirname(path).to_owned()) catch {
    _ => return false
  }
  @fs.write_file(
    path,
    @encoding/utf8.encode(state.stringify(indent=2)),
    create_mode=CreateOrTruncate,
    permission=0o644,
  ) catch {
    _ => return false
  }
  true
}

///|
async fn moon_skill_make_directory(path : String) -> Unit {
  let dirs : Array[String] = []
  for dir = path {
    if dir == "." || dir == "" || @fs.exists(dir) {
      break
    }
    dirs.push(dir)
    continue @pathx.dirname(dir).to_owned()
  }
  for i in 0.. ()
    }
  }
}

///|
async fn moon_get_skill_repos_json(
  path? : String = moon_skill_state_path(),
) -> Json {
  guard moon_skill_state_load(path~) is Object(obj) else {
    return Json::array(moon_skill_default_repos_json())
  }
  match obj.get("repos") {
    Some(Array(repos)) => Json::array(repos)
    _ => Json::array(moon_skill_default_repos_json())
  }
}

///|
async fn moon_get_installed_skills_json(
  path? : String = moon_skill_state_path(),
) -> Json {
  guard moon_skill_state_load(path~) is Object(obj) else {
    return Json::array([])
  }
  match obj.get("installed") {
    Some(Array(skills)) => Json::array(skills)
    _ => Json::array([])
  }
}

///|
async fn moon_get_skill_backups_json(
  path? : String = moon_skill_state_path(),
) -> Json {
  guard moon_skill_state_load(path~) is Object(obj) else {
    return Json::array([])
  }
  match obj.get("backups") {
    Some(Array(backups)) => Json::array(backups)
    _ => Json::array([])
  }
}

///|
async fn moon_add_skill_repo_json(
  repo : Json,
  path? : String = moon_skill_state_path(),
) -> Json {
  guard moon_skill_repo_from_json(repo) is Some(normalized) else {
    return Json::boolean(false)
  }
  let state = moon_skill_state_load(path~)
  guard state is Object(obj) else { return Json::boolean(false) }
  let repos : Array[Json] = match obj.get("repos") {
    Some(Array(items)) => items
    _ => moon_skill_default_repos_json()
  }
  guard normalized is Object(repo_obj) else { return Json::boolean(false) }
  let owner = store_json_string_or_default(repo_obj, "owner", "")
  let name = store_json_string_or_default(repo_obj, "name", "")
  let next : Array[Json] = []
  let mut replaced = false
  for item in repos {
    guard item is Object(item_obj) else { continue }
    if store_json_string_or_default(item_obj, "owner", "") == owner &&
      store_json_string_or_default(item_obj, "name", "") == name {
      next.push(normalized)
      replaced = true
    } else {
      next.push(item)
    }
  }
  if !replaced {
    next.push(normalized)
  }
  obj["repos"] = Json::array(next)
  Json::boolean(moon_skill_state_save(Json::object(obj), path~))
}

///|
async fn moon_remove_skill_repo_json(
  owner : String,
  name : String,
  path? : String = moon_skill_state_path(),
) -> Json {
  let state = moon_skill_state_load(path~)
  guard state is Object(obj) else { return Json::boolean(false) }
  let repos : Array[Json] = match obj.get("repos") {
    Some(Array(items)) => items
    _ => moon_skill_default_repos_json()
  }
  let next : Array[Json] = []
  for item in repos {
    guard item is Object(item_obj) else { continue }
    if store_json_string_or_default(item_obj, "owner", "") == owner &&
      store_json_string_or_default(item_obj, "name", "") == name {
      continue
    }
    next.push(item)
  }
  obj["repos"] = Json::array(next)
  Json::boolean(moon_skill_state_save(Json::object(obj), path~))
}

///|
fn moon_skill_repo_from_json(json : Json) -> Json? {
  guard json is Object(obj) else { return None }
  let owner = store_json_string_or_default(obj, "owner", "").trim().to_owned()
  let name = store_json_string_or_default(obj, "name", "").trim().to_owned()
  if owner.is_empty() || name.is_empty() {
    return None
  }
  let branch = store_json_string_or_default(obj, "branch", "main").trim()
  let enabled = match obj.get("enabled") {
    Some(True) => true
    Some(False) => false
    _ => true
  }
  Some(
    moon_skill_repo_json(
      owner,
      name,
      if branch.is_empty() {
        "main"
      } else {
        branch.to_owned()
      },
      enabled,
    ),
  )
}

///|
fn moon_skill_json_from_discoverable(
  skill : Json,
  current_app : String,
  installed_at? : Int = @os.unix_seconds(),
) -> Json? {
  guard skill is Object(obj) else { return None }
  let directory = moon_skill_install_name(
    store_json_string_or_default(obj, "directory", ""),
  )
  if directory.is_empty() {
    return None
  }
  let repo_owner = store_json_string_or_default(obj, "repoOwner", "")
  let repo_name = store_json_string_or_default(obj, "repoName", "")
  let key = store_json_string_or_default(
    obj,
    "key",
    if repo_owner.is_empty() || repo_name.is_empty() {
      "local:\{directory}"
    } else {
      "\{repo_owner}/\{repo_name}:\{directory}"
    },
  )
  let name = store_json_string_or_default(obj, "name", directory)
  let description = store_json_optional_string(obj, "description")
  let repo_branch = store_json_optional_string(obj, "repoBranch")
  let readme_url = store_json_optional_string(obj, "readmeUrl")
  Some(
    moon_installed_skill_json(
      id=key,
      name~,
      description~,
      directory~,
      repo_owner=if repo_owner.is_empty() { None } else { Some(repo_owner) },
      repo_name=if repo_name.is_empty() { None } else { Some(repo_name) },
      repo_branch~,
      readme_url~,
      apps=moon_skill_apps_json(app=Some(current_app)),
      installed_at~,
      content_hash=None,
      updated_at=0,
    ),
  )
}

///|
fn moon_skill_request_payload(body : Json?) -> Json? {
  match body {
    Some(Object(obj)) =>
      match obj.get("skill") {
        Some(Object(skill)) => Some(Json::object(skill))
        _ => Some(Json::object(obj))
      }
    _ => None
  }
}

///|
fn moon_installed_skill_json(
  id~ : String,
  name~ : String,
  description~ : String?,
  directory~ : String,
  repo_owner~ : String?,
  repo_name~ : String?,
  repo_branch~ : String?,
  readme_url~ : String?,
  apps~ : Json,
  installed_at~ : Int,
  content_hash~ : String?,
  updated_at~ : Int,
) -> Json {
  let obj : Map[String, Json] = {
    "id": Json::string(id),
    "name": Json::string(name),
    "directory": Json::string(directory),
    "apps": apps,
    "installedAt": Json::number(installed_at.to_double()),
    "updatedAt": Json::number(updated_at.to_double()),
  }
  if description is Some(value) && !value.is_empty() {
    obj["description"] = Json::string(value)
  }
  if repo_owner is Some(value) && !value.is_empty() {
    obj["repoOwner"] = Json::string(value)
  }
  if repo_name is Some(value) && !value.is_empty() {
    obj["repoName"] = Json::string(value)
  }
  if repo_branch is Some(value) && !value.is_empty() {
    obj["repoBranch"] = Json::string(value)
  }
  if readme_url is Some(value) && !value.is_empty() {
    obj["readmeUrl"] = Json::string(value)
  }
  if content_hash is Some(value) && !value.is_empty() {
    obj["contentHash"] = Json::string(value)
  }
  Json::object(obj)
}

///|
async fn moon_install_skill_unified_json(
  skill : Json,
  current_app : String,
  state_path? : String = moon_skill_state_path(),
) -> Json {
  guard moon_skill_json_from_discoverable(skill, current_app) is Some(installed) else {
    return Json::null()
  }
  moon_upsert_installed_skill_json(installed, state_path~)
}

///|
async fn moon_upsert_installed_skill_json(
  installed : Json,
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let state = moon_skill_state_load(path=state_path)
  guard state is Object(obj) else { return installed }
  guard installed is Object(installed_obj) else { return installed }
  let id = store_json_string_or_default(installed_obj, "id", "")
  let directory = store_json_string_or_default(installed_obj, "directory", "")
  let existing : Array[Json] = match obj.get("installed") {
    Some(Array(items)) => items
    _ => []
  }
  let next : Array[Json] = []
  let mut replaced = false
  for item in existing {
    guard item is Object(item_obj) else { continue }
    let same_id = !id.is_empty() &&
      store_json_string_or_default(item_obj, "id", "") == id
    let same_dir = !directory.is_empty() &&
      store_json_string_or_default(item_obj, "directory", "") == directory
    if same_id || same_dir {
      next.push(moon_merge_installed_skill_apps(item, installed))
      replaced = true
    } else {
      next.push(item)
    }
  }
  if !replaced {
    next.push(installed)
  }
  obj["installed"] = Json::array(next)
  ignore(moon_skill_state_save(Json::object(obj), path=state_path))
  if replaced {
    for item in next {
      guard item is Object(item_obj) else { continue }
      if store_json_string_or_default(item_obj, "id", "") == id ||
        store_json_string_or_default(item_obj, "directory", "") == directory {
        return item
      }
    }
  }
  installed
}

///|
fn moon_merge_installed_skill_apps(old : Json, incoming : Json) -> Json {
  guard old is Object(old_obj) else { return incoming }
  guard incoming is Object(new_obj) else { return old }
  let apps = moon_merge_skill_apps_json(
    old_obj.get("apps"),
    new_obj.get("apps"),
  )
  for key, value in new_obj {
    old_obj[key] = value
  }
  old_obj["apps"] = apps
  Json::object(old_obj)
}

///|
fn moon_merge_skill_apps_json(old : Json?, incoming : Json?) -> Json {
  let merged : Map[String, Json] = Map([], capacity=0)
  for name in moon_skill_supported_apps() {
    let enabled = moon_skill_app_enabled_json(old, name) ||
      moon_skill_app_enabled_json(incoming, name)
    merged[name] = Json::boolean(enabled)
  }
  Json::object(merged)
}

///|
fn moon_skill_app_enabled_json(apps : Json?, app : String) -> Bool {
  match apps {
    Some(Object(obj)) =>
      match obj.get(moon_normalize_app_type(app)) {
        Some(True) => true
        _ => false
      }
    _ => false
  }
}

///|
async fn moon_toggle_skill_app_json(
  id : String,
  app : String,
  enabled : Bool,
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let normalized = moon_normalize_app_type(app)
  if !moon_skill_supported_apps().contains(normalized) {
    return Json::boolean(false)
  }
  let state = moon_skill_state_load(path=state_path)
  guard state is Object(obj) else { return Json::boolean(false) }
  let existing : Array[Json] = match obj.get("installed") {
    Some(Array(items)) => items
    _ => []
  }
  let next : Array[Json] = []
  let mut changed = false
  for item in existing {
    guard item is Object(item_obj) else { continue }
    let matches = store_json_string_or_default(item_obj, "id", "") == id ||
      store_json_string_or_default(item_obj, "directory", "") == id
    if matches {
      let apps = moon_skill_apps_with_enabled(
        item_obj.get("apps"),
        normalized,
        enabled,
      )
      item_obj["apps"] = apps
      changed = true
      next.push(Json::object(item_obj))
    } else {
      next.push(item)
    }
  }
  obj["installed"] = Json::array(next)
  Json::boolean(
    changed && moon_skill_state_save(Json::object(obj), path=state_path),
  )
}

///|
fn moon_skill_apps_with_enabled(
  apps : Json?,
  app : String,
  enabled : Bool,
) -> Json {
  let obj : Map[String, Json] = Map([], capacity=0)
  for name in moon_skill_supported_apps() {
    obj[name] = Json::boolean(moon_skill_app_enabled_json(apps, name))
  }
  obj[app] = Json::boolean(enabled)
  Json::object(obj)
}

///|
async fn moon_uninstall_skill_unified_json(
  id : String,
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let state = moon_skill_state_load(path=state_path)
  guard state is Object(obj) else { return Json::null() }
  let existing : Array[Json] = match obj.get("installed") {
    Some(Array(items)) => items
    _ => []
  }
  let next : Array[Json] = []
  let mut removed : Json? = None
  for item in existing {
    guard item is Object(item_obj) else { continue }
    if store_json_string_or_default(item_obj, "id", "") == id ||
      store_json_string_or_default(item_obj, "directory", "") == id {
      removed = Some(Json::object(item_obj))
    } else {
      next.push(item)
    }
  }
  obj["installed"] = Json::array(next)
  let backup_path = match removed {
    Some(skill) => {
      let entry = moon_skill_backup_entry_json(skill)
      obj["backups"] = moon_skill_prepend_backup(obj.get("backups"), entry)
      match entry {
        Object(entry_obj) => store_json_optional_string(entry_obj, "backupPath")
        _ => None
      }
    }
    None => None
  }
  ignore(moon_skill_state_save(Json::object(obj), path=state_path))
  {
    "backupPath": match backup_path {
      Some(path) => Json::string(path)
      None => Json::null()
    },
  }.to_json()
}

///|
fn moon_skill_backup_entry_json(skill : Json) -> Json {
  let now = @os.unix_seconds()
  let directory = match skill {
    Object(obj) => store_json_string_or_default(obj, "directory", "skill")
    _ => "skill"
  }
  let backup_id = "\{directory}-\{now}"
  {
    "backupId": Json::string(backup_id),
    "backupPath": Json::string(@pathx.join(moon_skill_backup_dir(), backup_id)),
    "createdAt": Json::number(now.to_double()),
    "skill": skill,
  }.to_json()
}

///|
fn moon_skill_prepend_backup(existing : Json?, entry : Json) -> Json {
  let backups : Array[Json] = [entry]
  match existing {
    Some(Array(items)) =>
      for item in items {
        if backups.length() < MOONGATE_SKILLS_BACKUP_RETAIN_COUNT {
          backups.push(item)
        }
      }
    _ => ()
  }
  Json::array(backups)
}

///|
async fn moon_delete_skill_backup_json(
  backup_id : String,
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let state = moon_skill_state_load(path=state_path)
  guard state is Object(obj) else { return Json::boolean(false) }
  let existing : Array[Json] = match obj.get("backups") {
    Some(Array(items)) => items
    _ => []
  }
  let next : Array[Json] = []
  let mut removed = false
  for item in existing {
    guard item is Object(item_obj) else { continue }
    if store_json_string_or_default(item_obj, "backupId", "") == backup_id {
      removed = true
    } else {
      next.push(item)
    }
  }
  obj["backups"] = Json::array(next)
  Json::boolean(
    removed && moon_skill_state_save(Json::object(obj), path=state_path),
  )
}

///|
async fn moon_restore_skill_backup_json(
  backup_id : String,
  current_app : String,
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let backups = moon_get_skill_backups_json(path=state_path)
  guard backups is Array(items) else { return Json::null() }
  for item in items {
    guard item is Object(item_obj) else { continue }
    if store_json_string_or_default(item_obj, "backupId", "") == backup_id {
      guard item_obj.get("skill") is Some(Object(skill_obj)) else {
        return Json::null()
      }
      skill_obj["apps"] = moon_skill_apps_with_enabled(
        skill_obj.get("apps"),
        moon_normalize_app_type(current_app),
        true,
      )
      return moon_upsert_installed_skill_json(
        Json::object(skill_obj),
        state_path~,
      )
    }
  }
  Json::null()
}

///|
fn moon_check_skill_updates_json() -> Json {
  Json::array([])
}

///|
async fn moon_update_skill_json(
  id : String,
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let installed = moon_get_installed_skills_json(path=state_path)
  guard installed is Array(items) else { return Json::null() }
  for item in items {
    guard item is Object(obj) else { continue }
    if store_json_string_or_default(obj, "id", "") == id ||
      store_json_string_or_default(obj, "directory", "") == id {
      obj["updatedAt"] = Json::number(@os.unix_seconds().to_double())
      return moon_upsert_installed_skill_json(Json::object(obj), state_path~)
    }
  }
  Json::null()
}

///|
async fn moon_migrate_skill_storage_json(
  target : String,
  state_path? : String = moon_skill_state_path(),
  moongate_dir? : String = moon_moongate_skills_dir(),
  unified_dir? : String = moon_unified_skills_dir(),
) -> Json {
  guard moon_normalize_skill_storage_location(target) is Some(target_location) else {
    return moon_skill_migration_result_json(0, 0, [
      Json::string("invalid target storage location: \{target}"),
    ])
  }
  let state = moon_skill_state_load(path=state_path)
  guard state is Object(obj) else {
    return moon_skill_migration_result_json(0, 0, [
      Json::string("failed to load skill state"),
    ])
  }
  let current_location = moon_normalize_skill_storage_location(
    store_json_string_or_default(obj, "skillStorageLocation", "unified"),
  ).unwrap_or("unified")
  if current_location == target_location {
    return moon_skill_migration_result_json(0, 0, [])
  }
  let source_dir = moon_skill_storage_dir(
    current_location, moongate_dir, unified_dir,
  )
  let dest_dir = moon_skill_storage_dir(
    target_location, moongate_dir, unified_dir,
  )
  moon_skill_make_directory(dest_dir) catch {
    err =>
      return moon_skill_migration_result_json(0, 0, [
        Json::string("failed to create target directory: \{err}"),
      ])
  }
  let installed : Array[Json] = match obj.get("installed") {
    Some(Array(items)) => items
    _ => []
  }
  let mut migrated_count = 0
  let mut skipped_count = 0
  let errors : Array[Json] = []
  for item in installed {
    guard item is Object(skill) else {
      skipped_count = skipped_count + 1
      continue
    }
    let directory = moon_skill_install_name(
      store_json_string_or_default(skill, "directory", ""),
    )
    if directory.is_empty() {
      skipped_count = skipped_count + 1
      continue
    }
    let source_path = @pathx.join(source_dir, directory)
    let dest_path = @pathx.join(dest_dir, directory)
    if !@fs.exists(source_path) {
      skipped_count = skipped_count + 1
      continue
    }
    if @fs.exists(dest_path) {
      skipped_count = skipped_count + 1
      continue
    }
    match moon_copy_skill_directory(source_path, dest_path) {
      Ok(_) => {
        @fs.rmdir(source_path, recursive=true) catch {
          err =>
            errors.push(
              Json::string(
                "failed to remove source directory \{source_path}: \{err}",
              ),
            )
        }
        migrated_count = migrated_count + 1
      }
      Err(message) => errors.push(Json::string(message))
    }
  }
  obj["skillStorageLocation"] = Json::string(target_location)
  ignore(moon_skill_state_save(Json::object(obj), path=state_path))
  moon_skill_migration_result_json(migrated_count, skipped_count, errors)
}

///|
fn moon_normalize_skill_storage_location(value : String) -> String? {
  match value.trim().to_owned().to_lower() {
    "moongate" | "suite" | "local" => Some("moongate")
    "unified" => Some("unified")
    _ => None
  }
}

///|
fn moon_moongate_skills_dir() -> String {
  @pathx.join(moon_app_config_dir(), "skills")
}

///|
fn moon_unified_skills_dir() -> String {
  @pathx.join(moon_home_dir(), ".agents/skills")
}

///|
fn moon_skill_storage_dir(
  location : String,
  moongate_dir : String,
  unified_dir : String,
) -> String {
  if location == "unified" {
    unified_dir
  } else {
    moongate_dir
  }
}

///|
async fn moon_copy_skill_directory(
  source_path : String,
  dest_path : String,
) -> Result[Unit, String] {
  try {
    let kind = @fs.kind(source_path, follow_symlink=false)
    match kind {
      Directory => {
        moon_skill_make_directory(dest_path)
        let entries = @fsx.list_directory(source_path)
        for entry in entries {
          let child_source = entry.path
          let child_dest = @pathx.join(dest_path, entry.name)
          match entry.kind {
            Directory =>
              match moon_copy_skill_directory(child_source, child_dest) {
                Ok(_) => ()
                Err(message) => return Err(message)
              }
            Regular | SymLink => {
              let bytes = @fs.read_file(child_source)
              @fs.write_file(
                child_dest,
                bytes,
                create_mode=CreateOrTruncate,
                permission=0o644,
              )
            }
            _ => ()
          }
        }
      }
      Regular | SymLink => {
        moon_skill_make_directory(@pathx.dirname(dest_path).to_owned())
        let bytes = @fs.read_file(source_path)
        @fs.write_file(
          dest_path,
          bytes,
          create_mode=CreateOrTruncate,
          permission=0o644,
        )
      }
      _ => return Err("unsupported skill entry kind: \{source_path}")
    }
    Ok(())
  } catch {
    err => Err("failed to copy skill directory \{source_path}: \{err}")
  }
}

///|
fn moon_skill_migration_result_json(
  migrated_count : Int,
  skipped_count : Int,
  errors : Array[Json],
) -> Json {
  {
    "migratedCount": Json::number(migrated_count.to_double()),
    "skippedCount": Json::number(skipped_count.to_double()),
    "errors": Json::array(errors),
  }.to_json()
}

///|
fn moon_search_skills_sh_json(
  query : String,
  limit : Int,
  offset : Int,
) -> Json {
  ignore(offset)
  let skills : Array[Json] = []
  if query.trim().length() >= 2 {
    for repo in moon_skill_default_repos_json() {
      guard repo is Object(repo_obj) else { continue }
      if skills.length() >= limit {
        break
      }
      let owner = store_json_string_or_default(repo_obj, "owner", "")
      let name = store_json_string_or_default(repo_obj, "name", "")
      let branch = store_json_string_or_default(repo_obj, "branch", "main")
      skills.push(
        {
          "key": Json::string("\{owner}/\{name}:skills"),
          "name": Json::string("\{name} skills"),
          "directory": Json::string("skills"),
          "repoOwner": Json::string(owner),
          "repoName": Json::string(name),
          "repoBranch": Json::string(branch),
          "installs": Json::number(0.0),
          "readmeUrl": Json::string(
            "https://github.com/\{owner}/\{name}/tree/\{branch}",
          ),
        }.to_json(),
      )
    }
  }
  {
    "skills": Json::array(skills),
    "totalCount": Json::number(skills.length().to_double()),
    "query": Json::string(query),
  }.to_json()
}

///|
async fn moon_discover_available_skills_json(
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let repos = moon_get_skill_repos_json(path=state_path)
  guard repos is Array(items) else { return Json::array([]) }
  let skills : Array[Json] = []
  for item in items {
    guard item is Object(repo) else { continue }
    if repo.get("enabled") == Some(Json::boolean(false)) {
      continue
    }
    let owner = store_json_string_or_default(repo, "owner", "")
    let name = store_json_string_or_default(repo, "name", "")
    let branch = store_json_string_or_default(repo, "branch", "main")
    if owner.is_empty() || name.is_empty() {
      continue
    }
    skills.push(
      {
        "key": Json::string("\{owner}/\{name}:skills"),
        "name": Json::string("\{name} skills"),
        "description": Json::string("Discovered from \{owner}/\{name}"),
        "directory": Json::string("skills"),
        "repoOwner": Json::string(owner),
        "repoName": Json::string(name),
        "repoBranch": Json::string(branch),
        "readmeUrl": Json::string(
          "https://github.com/\{owner}/\{name}/tree/\{branch}",
        ),
      }.to_json(),
    )
  }
  Json::array(skills)
}

///|
async fn moon_get_skills_json(
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let installed = moon_get_installed_skills_json(path=state_path)
  let installed_dirs : Array[String] = []
  if installed is Array(items) {
    for item in items {
      guard item is Object(obj) else { continue }
      installed_dirs.push(store_json_string_or_default(obj, "directory", ""))
    }
  }
  let discovered = moon_discover_available_skills_json(state_path~)
  guard discovered is Array(items) else { return Json::array([]) }
  let result : Array[Json] = []
  for item in items {
    guard item is Object(obj) else { continue }
    let directory = store_json_string_or_default(obj, "directory", "")
    result.push(
      {
        "key": Json::string(store_json_string_or_default(obj, "key", directory)),
        "name": Json::string(
          store_json_string_or_default(obj, "name", directory),
        ),
        "description": Json::string(
          store_json_string_or_default(obj, "description", ""),
        ),
        "directory": Json::string(directory),
        "readmeUrl": obj.get("readmeUrl").unwrap_or(Json::null()),
        "installed": Json::boolean(installed_dirs.contains(directory)),
        "repoOwner": obj.get("repoOwner").unwrap_or(Json::null()),
        "repoName": obj.get("repoName").unwrap_or(Json::null()),
        "repoBranch": obj.get("repoBranch").unwrap_or(Json::null()),
      }.to_json(),
    )
  }
  Json::array(result)
}

///|
async fn moon_scan_unmanaged_skills_json() -> Json {
  let result : Array[Json] = []
  for app in moon_skill_supported_apps() {
    let dir = moon_app_skills_dir(app)
    if !@fs.exists(dir) {
      continue
    }
    let directory = @fs.opendir(dir) catch { _ => continue }
    defer directory.close()
    for entry_name in directory.read_all() {
      let entry_path = @pathx.join(dir, entry_name)
      let skill_file = @pathx.join(entry_path, "SKILL.md")
      if !@fs.exists(skill_file) {
        continue
      }
      let (skill_name, description) = moon_skill_metadata_from_file(skill_file)
      result.push(
        {
          "directory": Json::string(entry_name),
          "name": Json::string(skill_name.unwrap_or(entry_name)),
          "description": match description {
            Some(value) => Json::string(value)
            None => Json::null()
          },
          "foundIn": Json::array([Json::string(app)]),
          "path": Json::string(entry_path),
        }.to_json(),
      )
    }
  }
  Json::array(result)
}

///|
async fn moon_import_skills_from_apps_json(
  imports : Json,
  state_path? : String = moon_skill_state_path(),
) -> Json {
  let imported : Array[Json] = []
  guard imports is Array(items) else { return Json::array(imported) }
  for item in items {
    guard item is Object(obj) else { continue }
    let directory = moon_skill_install_name(
      store_json_string_or_default(obj, "directory", ""),
    )
    if directory.is_empty() {
      continue
    }
    let apps = match obj.get("apps") {
      Some(apps) => apps
      None => moon_skill_apps_json()
    }
    let skill = moon_installed_skill_json(
      id="local:\{directory}",
      name=directory,
      description=None,
      directory~,
      repo_owner=None,
      repo_name=None,
      repo_branch=None,
      readme_url=None,
      apps~,
      installed_at=@os.unix_seconds(),
      content_hash=None,
      updated_at=0,
    )
    imported.push(moon_upsert_installed_skill_json(skill, state_path~))
  }
  Json::array(imported)
}

///|
async fn moon_install_skills_from_zip_json(
  file_path : String,
  current_app : String,
  state_path? : String = moon_skill_state_path(),
  moongate_dir? : String = moon_moongate_skills_dir(),
  unified_dir? : String = moon_unified_skills_dir(),
  app_dir? : String = moon_app_skills_dir(current_app),
) -> Json {
  if !@fs.exists(file_path) {
    return Json::array([])
  }
  if moon_path_is_directory(file_path) {
    return moon_install_skills_from_extracted_dir_json(
      file_path,
      current_app,
      state_path~,
      moongate_dir~,
      unified_dir~,
      app_dir~,
      source_name=@pathx.basename(file_path).to_owned(),
    )
  }

  @fsx.with_temporary_directory("moongate-skills-zip-XXXXXX", async fn(tmpdir) {
    if !moon_extract_local_zip(file_path, tmpdir) {
      return Json::array([])
    }
    moon_install_skills_from_extracted_dir_json(
      tmpdir,
      current_app,
      state_path~,
      moongate_dir~,
      unified_dir~,
      app_dir~,
      source_name=@pathx.basename(file_path).to_owned(),
    )
  })
}

///|
async fn moon_install_skills_from_extracted_dir_json(
  root_dir : String,
  current_app : String,
  state_path? : String = moon_skill_state_path(),
  moongate_dir? : String = moon_moongate_skills_dir(),
  unified_dir? : String = moon_unified_skills_dir(),
  app_dir? : String = moon_app_skills_dir(current_app),
  source_name? : String = @pathx.basename(root_dir).to_owned(),
) -> Json {
  let skill_dirs = moon_scan_skill_dirs_in_dir(root_dir)
  if skill_dirs.is_empty() {
    return Json::array([])
  }
  let state = moon_skill_state_load(path=state_path)
  guard state is Object(state_obj) else { return Json::array([]) }
  let storage_dir = moon_skill_storage_dir_for_state(
    Json::object(state_obj),
    moongate_dir,
    unified_dir,
  )
  let existing : Array[Json] = match state_obj.get("installed") {
    Some(Array(items)) => items
    _ => []
  }
  let installed : Array[Json] = []
  for skill_dir in skill_dirs {
    let (name, description) = moon_skill_metadata_from_file(
      @pathx.join(skill_dir, "SKILL.md"),
    )
    guard moon_skill_zip_install_name(skill_dir, root_dir, name, source_name~)
      is Some(directory) else {
      continue
    }
    if moon_installed_skill_directory_exists(existing, directory) {
      continue
    }
    let dest = @pathx.join(storage_dir, directory)
    if @fs.exists(dest) {
      @fs.rmdir(dest, recursive=true) catch {
        _ => ()
      }
    }
    match moon_copy_skill_directory(skill_dir, dest) {
      Ok(_) => ()
      Err(_) => continue
    }
    if !app_dir.is_empty() {
      let app_dest = @pathx.join(app_dir, directory)
      if @fs.exists(app_dest) {
        @fs.rmdir(app_dest, recursive=true) catch {
          _ => ()
        }
      }
      ignore(moon_copy_skill_directory(dest, app_dest))
    }
    let skill = moon_installed_skill_json(
      id="local:\{directory}",
      name=name.unwrap_or(directory),
      description~,
      directory~,
      repo_owner=None,
      repo_name=None,
      repo_branch=None,
      readme_url=None,
      apps=moon_skill_apps_json(app=Some(current_app)),
      installed_at=@os.unix_seconds(),
      content_hash=moon_skill_dir_hash(dest),
      updated_at=0,
    )
    installed.push(moon_upsert_installed_skill_json(skill, state_path~))
  }
  Json::array(installed)
}

///|
async fn moon_extract_local_zip(zip_path : String, dest_dir : String) -> Bool {
  let result = @spawn.spawn(
    "unzip",
    ["-qq", "-o", zip_path, "-d", dest_dir],
    timeout=60000,
  ) catch {
    _ => return false
  }
  result.status == 0
}

///|
async fn moon_scan_skill_dirs_in_dir(root_dir : String) -> Array[String] {
  let result : Array[String] = []
  moon_scan_skill_dirs_recursive(root_dir, result, depth=0)
  result
}

///|
async fn moon_scan_skill_dirs_recursive(
  dir : String,
  result : Array[String],
  depth~ : Int,
) -> Unit {
  if depth > 16 || !moon_path_is_directory(dir) {
    return
  }
  if @fs.exists(@pathx.join(dir, "SKILL.md")) {
    result.push(dir)
  }
  let entries = @fsx.list_directory(dir) catch { _ => return }
  for entry in entries {
    if entry.name.has_prefix(".") {
      continue
    }
    if entry.kind is Directory {
      moon_scan_skill_dirs_recursive(entry.path, result, depth=depth + 1)
    }
  }
}

///|
fn moon_skill_zip_install_name(
  skill_dir : String,
  root_dir : String,
  skill_name : String?,
  source_name~ : String,
) -> String? {
  let dir_name = @pathx.basename(skill_dir).to_owned()
  if skill_dir == root_dir || dir_name.is_empty() || dir_name.has_prefix(".") {
    match skill_name {
      Some(name) =>
        match moon_skill_install_name(name) {
          "" => moon_skill_install_name_optional(source_name)
          value => Some(value)
        }
      None => moon_skill_install_name_optional(source_name)
    }
  } else {
    match moon_skill_install_name(dir_name) {
      "" =>
        match skill_name {
          Some(name) => moon_skill_install_name_optional(name)
          None => moon_skill_install_name_optional(source_name)
        }
      value => Some(value)
    }
  }
}

///|
fn moon_skill_install_name_optional(value : String) -> String? {
  match moon_skill_install_name(value) {
    "" => None
    normalized => Some(normalized)
  }
}

///|
fn moon_installed_skill_directory_exists(
  installed : Array[Json],
  directory : String,
) -> Bool {
  for item in installed {
    guard item is Object(obj) else { continue }
    if store_json_string_or_default(obj, "directory", "").to_lower() ==
      directory.to_lower() {
      return true
    }
  }
  false
}

///|
fn moon_skill_storage_dir_for_state(
  state : Json,
  moongate_dir : String,
  unified_dir : String,
) -> String {
  guard state is Object(obj) else { return unified_dir }
  let location = moon_normalize_skill_storage_location(
    store_json_string_or_default(obj, "skillStorageLocation", "unified"),
  ).unwrap_or("unified")
  moon_skill_storage_dir(location, moongate_dir, unified_dir)
}

///|
async fn moon_skill_dir_hash(dir : String) -> String? {
  let files : Array[String] = []
  moon_collect_skill_hash_files(dir, dir, files, depth=0)
  files.sort()
  let bytes : Array[Byte] = []
  for relative in files {
    for byte in @encoding/utf8.encode(relative.replace(old="\\", new="/")) {
      bytes.push(byte)
    }
    bytes.push(0)
    let content = @fs.read_file(@pathx.join(dir, relative)) catch {
      _ => return None
    }
    for byte in content.binary() {
      bytes.push(byte)
    }
    bytes.push(0)
  }
  Some(moon_skill_sha256_hex(Bytes::from_array(bytes)))
}

///|
async fn moon_collect_skill_hash_files(
  base_dir : String,
  current_dir : String,
  files : Array[String],
  depth~ : Int,
) -> Unit {
  if depth > 16 {
    return
  }
  let entries = @fsx.list_directory(current_dir) catch { _ => return }
  for entry in entries {
    if entry.name.has_prefix(".") {
      continue
    }
    match entry.kind {
      Directory =>
        moon_collect_skill_hash_files(
          base_dir,
          entry.path,
          files,
          depth=depth + 1,
        )
      Regular | SymLink => files.push(moon_relative_path(base_dir, entry.path))
      _ => ()
    }
  }
}

///|
fn moon_relative_path(base_dir : String, path : String) -> String {
  let prefix = if base_dir.has_suffix("/") { base_dir } else { "\{base_dir}/" }
  if path.has_prefix(prefix) {
    path[prefix.length():].to_owned()
  } else {
    @pathx.basename(path).to_owned()
  }
}

///|
fn moon_skill_sha256_hex(bytes : Bytes) -> String {
  let digest = @crypto.sha256(bytes)
  let builder = StringBuilder::new()
  for byte in digest {
    let value = byte.to_int()
    builder.write_char(moon_skill_hex_digit(value / 16))
    builder.write_char(moon_skill_hex_digit(value % 16))
  }
  builder.to_string()
}

///|
fn moon_skill_hex_digit(n : Int) -> Char {
  if n < 10 {
    ('0'.to_int() + n).unsafe_to_char()
  } else {
    ('a'.to_int() + n - 10).unsafe_to_char()
  }
}

///|
async fn moon_path_is_directory(path : String) -> Bool {
  (@fs.kind(path, follow_symlink=false) is Directory) catch {
    _ => false
  }
}

///|
async fn moon_skill_metadata_from_file(path : String) -> (String?, String?) {
  let content = @fs.read_file(path).text() catch { _ => return (None, None) }
  let mut name : String? = None
  let mut description : String? = None
  let mut in_frontmatter = false
  for line_view in content.split("\n") {
    let line = line_view.to_owned().trim().to_owned()
    if line == "---" {
      in_frontmatter = !in_frontmatter
      continue
    }
    if !in_frontmatter {
      if line.has_prefix("# ") && name is None {
        name = Some(line[2:].trim().to_owned())
      }
      continue
    }
    if line.has_prefix("name:") {
      name = Some(moon_yaml_scalar(line[5:]))
    } else if line.has_prefix("description:") {
      description = Some(moon_yaml_scalar(line[12:]))
    }
  }
  (name, description)
}

///|
fn moon_yaml_scalar(value : StringView) -> String {
  let trimmed = value.to_owned().trim().to_owned()
  if trimmed.length() >= 2 &&
    (
      (trimmed.has_prefix("\"") && trimmed.has_suffix("\"")) ||
      (trimmed.has_prefix("'") && trimmed.has_suffix("'"))
    ) {
    trimmed[1:trimmed.length() - 1].to_owned()
  } else {
    trimmed
  }
}

///|
fn moon_skill_install_name(directory : String) -> String {
  let normalized = directory.replace(old="\\", new="/").trim().to_owned()
  let base = @pathx.basename(normalized).to_owned().trim().to_owned()
  if base == "." || base == ".." || base.has_prefix(".") || base.contains("/") {
    ""
  } else {
    base
  }
}

///|
fn moon_app_skills_dir(app : String) -> String {
  @pathx.join(moon_config_dir(app), "skills")
}