mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
ee3791a1b2
Hermes v0.12 added three skills surfaces Scarf can now reach: - Direct-URL install: `hermes skills install <https://...>` lets users pull a one-off skill without going through a registry. Mac SkillsView grew an "Install from URL…" toolbar button (capability-gated on HermesCapabilities.hasSkillURLInstall) opening a sheet with the URL field plus optional --category / --name overrides. - Reload: `hermes skills audit` rescans `~/.hermes/skills/` and refreshes the agent's view of available skills without restarting. Wired to a "Reload" toolbar button next to the install button on Mac. - Enabled state: skills.disabled in config.yaml is now read at scan time (SkillsViewModel.readDisabledSkillNames). Disabled skills render strikethrough + an "OFF" pill on Mac and iOS rows so users see what Hermes won't load. iOS detail view explains the state in plain text. - Curator pin badge: pinned-skill names from `~/.hermes/skills/.curator_state` (SkillsViewModel.readPinnedSkillNames) surface as a pin glyph on each row. Mac sidebar + iOS list both show it; iOS detail view explains "pinned by curator — won't auto-archive." Model + scanner: - HermesSkill gains `enabled: Bool` (default true) and `pinned: Bool` (default false). Both default to backwards-compatible values so unmodified call sites keep compiling. - SkillsScanner.scan now takes optional `disabledNames` and `pinnedNames` sets and applies them per skill at scan time. - SkillsViewModel.load auto-fetches both sets internally so Mac/iOS callers don't have to plumb curator state manually; an opt-in `pinnedNames` override is available for the Curator screen which has a fresher snapshot in hand. Tests: 215 ScarfCore tests pass; both Mac and iOS schemes build clean. Note: the disable-toggle path (writing the array back into config.yaml) is deferred to v2.7 — Hermes ships `hermes skills config` as an interactive verb only, and we'd rather read accurately than risk clobbering the user's list with a half-tested write path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
3.7 KiB
Swift
88 lines
3.7 KiB
Swift
import SwiftUI
|
|
import ScarfCore
|
|
import ScarfDesign
|
|
|
|
/// v0.12+ direct-URL skill install. Hermes accepts an HTTPS URL pointing
|
|
/// at a SKILL.md (or a tarball) and installs it under
|
|
/// `~/.hermes/skills/<category>/<name>/`. Authors who don't ship via a
|
|
/// registry can use this to share a one-off skill with a single URL.
|
|
///
|
|
/// Capability-gated upstream — SkillsView only opens this sheet when
|
|
/// `HermesCapabilities.hasSkillURLInstall` is true.
|
|
struct InstallFromURLSheet: View {
|
|
let viewModel: SkillsViewModel
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var url: String = ""
|
|
@State private var category: String = ""
|
|
@State private var nameOverride: String = ""
|
|
|
|
/// Loose validity check — accept anything that starts with `https://`
|
|
/// (HTTP gets blocked because Hermes refuses non-TLS skill URLs by
|
|
/// default to keep MITM-injected SKILL.md off the host).
|
|
private var isValid: Bool {
|
|
let trimmed = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return trimmed.lowercased().hasPrefix("https://") && trimmed.count > 10
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: ScarfSpace.s3) {
|
|
Text("Install Skill from URL")
|
|
.scarfStyle(.headline)
|
|
.foregroundStyle(ScarfColor.foregroundPrimary)
|
|
|
|
Text("Paste an HTTPS URL pointing at a SKILL.md or a tarball. Hermes downloads, scans, and installs it under `~/.hermes/skills/<category>/<name>/`.")
|
|
.scarfStyle(.caption)
|
|
.foregroundStyle(ScarfColor.foregroundMuted)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("URL")
|
|
.scarfStyle(.captionUppercase)
|
|
.foregroundStyle(ScarfColor.foregroundMuted)
|
|
ScarfTextField("https://example.com/path/to/SKILL.md", text: $url)
|
|
}
|
|
|
|
DisclosureGroup("Optional overrides") {
|
|
VStack(alignment: .leading, spacing: ScarfSpace.s2) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Category")
|
|
.scarfStyle(.captionUppercase)
|
|
.foregroundStyle(ScarfColor.foregroundMuted)
|
|
ScarfTextField("e.g. productivity (defaults to `local`)", text: $category)
|
|
}
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Skill name")
|
|
.scarfStyle(.captionUppercase)
|
|
.foregroundStyle(ScarfColor.foregroundMuted)
|
|
ScarfTextField("Override if SKILL.md has no `name:`", text: $nameOverride)
|
|
}
|
|
}
|
|
.padding(.top, ScarfSpace.s2)
|
|
}
|
|
.scarfStyle(.body)
|
|
|
|
HStack {
|
|
Spacer()
|
|
Button("Cancel") { dismiss() }
|
|
.buttonStyle(ScarfGhostButton())
|
|
Button("Install") {
|
|
let trimmedURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let cat = category.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let name = nameOverride.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
viewModel.installFromURL(
|
|
trimmedURL,
|
|
categoryOverride: cat.isEmpty ? nil : cat,
|
|
nameOverride: name.isEmpty ? nil : name
|
|
)
|
|
dismiss()
|
|
}
|
|
.buttonStyle(ScarfPrimaryButton())
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(!isValid)
|
|
}
|
|
}
|
|
.padding(ScarfSpace.s5)
|
|
.frame(width: 460)
|
|
}
|
|
}
|