@@ -13,6 +13,8 @@ import (
1313 "path/filepath"
1414 "sort"
1515 "strings"
16+
17+ "go.yaml.in/yaml/v4"
1618)
1719
1820// Skill is a discovered skill's catalog metadata and the path to its SKILL.md.
@@ -89,9 +91,6 @@ func (r *Registry) load(md string) error {
8991 if dir := filepath .Base (filepath .Dir (md )); dir != name {
9092 log .Printf ("skills: %s: name %q does not match directory %q" , md , name , dir )
9193 }
92- if strings .Contains (desc , ":" ) {
93- log .Printf ("skills: %s: description for %q may contain an unquoted colon (subtly malformed YAML); loading anyway" , md , name )
94- }
9594 r .byName [name ] = Skill {
9695 Name : name ,
9796 Description : desc ,
@@ -139,15 +138,19 @@ func (r *Registry) catalog(keep func(Skill) bool) []Skill {
139138 return out
140139}
141140
142- // parseFrontmatter extracts the name, description and disable-model-invocation fields
143- // from a SKILL.md's YAML frontmatter block. It is deliberately lenient: it only needs
144- // the scalar keys and tolerates unquoted values containing colons, which strict YAML
145- // would reject.
141+ // frontmatter is the metadata block at the start of a SKILL.md file.
142+ type frontmatter struct {
143+ Name string `yaml:"name"`
144+ Description string `yaml:"description"`
145+ DisableModelInvocation bool `yaml:"disable-model-invocation"`
146+ }
147+
148+ // parseFrontmatter extracts the metadata from a SKILL.md's YAML frontmatter block.
146149func parseFrontmatter (content string ) (name , desc string , disableMD bool , ok bool ) {
147- // Must start with a standalone `---` line.
148150 if content != "---" && ! strings .HasPrefix (content , "---\n " ) {
149151 return "" , "" , false , false
150152 }
153+
151154 rest := content [3 :]
152155 nl := strings .IndexByte (rest , '\n' )
153156 if nl < 0 {
@@ -159,38 +162,12 @@ func parseFrontmatter(content string) (name, desc string, disableMD bool, ok boo
159162 if end < 0 {
160163 return "" , "" , false , false
161164 }
162- for _ , line := range strings .Split (rest [:end ], "\n " ) {
163- line = strings .TrimSpace (line )
164- if line == "" || strings .HasPrefix (line , "#" ) {
165- continue
166- }
167- colon := strings .IndexByte (line , ':' )
168- if colon < 0 {
169- continue
170- }
171- key := strings .TrimSpace (line [:colon ])
172- val := strings .TrimSpace (line [colon + 1 :])
173- switch key {
174- case "name" :
175- name = stripQuotes (val )
176- case "description" :
177- desc = stripQuotes (val )
178- case "disable-model-invocation" :
179- disableMD = parseBool (val )
180- }
181- }
182- return name , desc , disableMD , name != ""
183- }
184165
185- // parseBool leniently parses a YAML boolean. It returns false for the default/empty
186- // value, so an absent disable-model-invocation key never flips the flag.
187- func parseBool (v string ) bool {
188- switch strings .ToLower (strings .TrimSpace (v )) {
189- case "true" , "yes" , "y" , "on" , "1" :
190- return true
191- default :
192- return false
166+ var metadata frontmatter
167+ if err := yaml .Unmarshal ([]byte (rest [:end ]), & metadata ); err != nil {
168+ return "" , "" , false , false
193169 }
170+ return metadata .Name , metadata .Description , metadata .DisableModelInvocation , metadata .Name != ""
194171}
195172
196173// closingDelim returns the index of the first line exactly equal to `---`, or -1. It
@@ -208,14 +185,3 @@ func closingDelim(s string) int {
208185 s = s [i + 3 :]
209186 }
210187}
211-
212- // stripQuotes removes a single matching surrounding quote pair, leaving a value with
213- // stray or unbalanced quotes (e.g. an apostrophe) untouched.
214- func stripQuotes (v string ) string {
215- if len (v ) >= 2 {
216- if (v [0 ] == '"' && v [len (v )- 1 ] == '"' ) || (v [0 ] == '\'' && v [len (v )- 1 ] == '\'' ) {
217- return v [1 : len (v )- 1 ]
218- }
219- }
220- return v
221- }
0 commit comments