mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
feat(skills): SKILL.md frontmatter v0.11 fields (Phase 3.3)
Hermes v2026.4.23 SKILL.md files carry richer YAML frontmatter: allowed_tools, related_skills, dependencies. Surface them as chip rows in the skill detail view on both platforms. ScarfCore HermesSkill: - Three new optional fields: allowedTools, relatedSkills, dependencies. Default-nil so older skills (no SKILL.md, or SKILL.md without these fields) load unchanged. Mac HermesFileService.parseSkillFrontmatter: - Reads `<skill>/SKILL.md`, splits at `---` markers, parses the frontmatter via HermesYAML.parseNestedYAML, and extracts the three list fields. Tuple-of-optionals return; nil-everything when the file is absent or has no frontmatter. iOS IOSSkillsViewModel.parseFrontmatter: - Mirror with the iOS transport (over SFTP). Same parser, same return shape. Mac SkillsView: - skillChipSection(title:items:) helper renders a labelled chip row. Three rows added between the existing missing-config / Spotify / npx surfaces and the file list — only shown when the corresponding field is non-empty. iOS SkillDetailView: - chipRow(_:) helper using a small in-file FlowLayout (built-in Layout protocol, no third-party dep) so the chips wrap onto multiple lines on iPhone-narrow screens. Three sections matching Mac. Verified: ScarfCore + Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,20 @@ public struct HermesSkill: Identifiable, Sendable {
|
||||
public let path: String
|
||||
public let files: [String]
|
||||
public let requiredConfig: [String]
|
||||
/// Tools the skill author declared the skill is allowed to invoke
|
||||
/// (Hermes v2026.4.23 SKILL.md frontmatter `allowed_tools`).
|
||||
/// `nil` when the skill ships no SKILL.md or the frontmatter
|
||||
/// doesn't declare the field — pre-v0.11 behaviour preserved.
|
||||
public let allowedTools: [String]?
|
||||
/// Skill names the author cross-references as related (`related_skills`
|
||||
/// in SKILL.md frontmatter). Surfaced as chips in the skill detail
|
||||
/// view so users can hop between connected skills.
|
||||
public let relatedSkills: [String]?
|
||||
/// External runtime dependencies the skill needs on the host
|
||||
/// (`dependencies` in SKILL.md frontmatter; e.g. `npx`, `ffmpeg`,
|
||||
/// Python packages). Used by `SkillPrereqService` to know what to
|
||||
/// probe; nil when the field is absent.
|
||||
public let dependencies: [String]?
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
@@ -30,7 +44,10 @@ public struct HermesSkill: Identifiable, Sendable {
|
||||
category: String,
|
||||
path: String,
|
||||
files: [String],
|
||||
requiredConfig: [String]
|
||||
requiredConfig: [String],
|
||||
allowedTools: [String]? = nil,
|
||||
relatedSkills: [String]? = nil,
|
||||
dependencies: [String]? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
@@ -38,5 +55,8 @@ public struct HermesSkill: Identifiable, Sendable {
|
||||
self.path = path
|
||||
self.files = files
|
||||
self.requiredConfig = requiredConfig
|
||||
self.allowedTools = allowedTools
|
||||
self.relatedSkills = relatedSkills
|
||||
self.dependencies = dependencies
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +64,25 @@ public final class IOSSkillsViewModel {
|
||||
let skillPath = categoryPath + "/" + skillName
|
||||
guard transport.stat(skillPath)?.isDirectory == true else { continue }
|
||||
let files: [String] = (try? transport.listDirectory(skillPath)) ?? []
|
||||
// v2.5: parse SKILL.md frontmatter for the
|
||||
// Hermes v2026.4.23 fields (allowed_tools,
|
||||
// related_skills, dependencies). Falls back
|
||||
// to nil-everything on absent/malformed
|
||||
// frontmatter — old skills behave as before.
|
||||
let frontmatter = Self.parseFrontmatter(
|
||||
skillMdPath: skillPath + "/SKILL.md",
|
||||
transport: transport
|
||||
)
|
||||
skills.append(HermesSkill(
|
||||
id: categoryName + "/" + skillName,
|
||||
name: skillName,
|
||||
category: categoryName,
|
||||
path: skillPath,
|
||||
files: files.filter { !$0.hasPrefix(".") }.sorted(),
|
||||
requiredConfig: [] // Skills frontmatter parsing deferred.
|
||||
requiredConfig: [], // skill.yaml parsing still deferred for iOS
|
||||
allowedTools: frontmatter.allowedTools,
|
||||
relatedSkills: frontmatter.relatedSkills,
|
||||
dependencies: frontmatter.dependencies
|
||||
))
|
||||
}
|
||||
|
||||
@@ -97,4 +109,32 @@ public final class IOSSkillsViewModel {
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
/// Read `<skill>/SKILL.md`'s YAML frontmatter and pull the v2.5
|
||||
/// fields (allowed_tools, related_skills, dependencies). Returns
|
||||
/// nil-filled tuple on missing file, missing frontmatter, or empty
|
||||
/// fields. Mirrors Mac's `HermesFileService.parseSkillFrontmatter`.
|
||||
nonisolated static func parseFrontmatter(
|
||||
skillMdPath: String,
|
||||
transport: any ServerTransport
|
||||
) -> (allowedTools: [String]?, relatedSkills: [String]?, dependencies: [String]?) {
|
||||
guard transport.fileExists(skillMdPath),
|
||||
let data = try? transport.readFile(skillMdPath),
|
||||
let raw = String(data: data, encoding: .utf8)
|
||||
else { return (nil, nil, nil) }
|
||||
let lines = raw.components(separatedBy: "\n")
|
||||
guard lines.first == "---",
|
||||
let endIdx = lines.dropFirst().firstIndex(of: "---")
|
||||
else { return (nil, nil, nil) }
|
||||
let frontmatter = lines[1..<endIdx].joined(separator: "\n")
|
||||
let parsed = HermesYAML.parseNestedYAML(frontmatter)
|
||||
let allowed = parsed.lists["allowed_tools"]
|
||||
let related = parsed.lists["related_skills"]
|
||||
let deps = parsed.lists["dependencies"]
|
||||
return (
|
||||
allowedTools: (allowed?.isEmpty ?? true) ? nil : allowed,
|
||||
relatedSkills: (related?.isEmpty ?? true) ? nil : related,
|
||||
dependencies: (deps?.isEmpty ?? true) ? nil : deps
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user