@@ -16,18 +16,32 @@ const (
1616 formatCodex skillFormat = "codex"
1717
1818 skillFile = "SKILL.md"
19+
20+ maxNameLength = 64
21+ maxDescriptionLength = 1024
22+ maxCompatLength = 500
1923)
2024
25+ var namePattern = regexp .MustCompile (`^[a-z0-9]+(-[a-z0-9]+)*$` )
26+
2127type Skill struct {
22- Name string
23- Description string
24- FilePath string
25- BaseDir string
28+ Name string
29+ Description string
30+ FilePath string
31+ BaseDir string
32+ License string
33+ Compatibility string
34+ Metadata map [string ]string
35+ AllowedTools []string
2636}
2737
2838type frontmatter struct {
29- Name string
30- Description string
39+ Name string
40+ Description string
41+ License string
42+ Compatibility string
43+ Metadata map [string ]string
44+ AllowedTools []string
3145}
3246
3347func stripQuotes (value string ) string {
@@ -40,6 +54,13 @@ func stripQuotes(value string) string {
4054 return value
4155}
4256
57+ func isValidName (name string ) bool {
58+ if name == "" || len (name ) > maxNameLength {
59+ return false
60+ }
61+ return namePattern .MatchString (name )
62+ }
63+
4364func parseFrontmatter (content string ) (frontmatter , string ) {
4465 fm := frontmatter {}
4566
@@ -58,8 +79,25 @@ func parseFrontmatter(content string) (frontmatter, string) {
5879 frontmatterBlock := normalizedContent [4 : endIndex + 3 ]
5980 body := strings .TrimSpace (normalizedContent [endIndex + 7 :])
6081
61- lineRegex := regexp .MustCompile (`^(\w+):\s*(.*)$` )
82+ lineRegex := regexp .MustCompile (`^([\w-]+):\s*(.*)$` )
83+ metadataRegex := regexp .MustCompile (`^\s+(\w+):\s*(.*)$` )
84+ inMetadata := false
85+
6286 for line := range strings .SplitSeq (frontmatterBlock , "\n " ) {
87+ if inMetadata {
88+ matches := metadataRegex .FindStringSubmatch (line )
89+ if matches != nil {
90+ key := matches [1 ]
91+ value := stripQuotes (strings .TrimSpace (matches [2 ]))
92+ if fm .Metadata == nil {
93+ fm .Metadata = make (map [string ]string )
94+ }
95+ fm .Metadata [key ] = value
96+ continue
97+ }
98+ inMetadata = false
99+ }
100+
63101 matches := lineRegex .FindStringSubmatch (line )
64102 if matches != nil {
65103 key := matches [1 ]
@@ -69,6 +107,17 @@ func parseFrontmatter(content string) (frontmatter, string) {
69107 fm .Name = value
70108 case "description" :
71109 fm .Description = value
110+ case "license" :
111+ fm .License = value
112+ case "compatibility" :
113+ fm .Compatibility = value
114+ case "metadata" :
115+ inMetadata = true
116+ fm .Metadata = make (map [string ]string )
117+ case "allowed-tools" :
118+ if value != "" {
119+ fm .AllowedTools = strings .Fields (value )
120+ }
72121 }
73122 }
74123 }
@@ -106,14 +155,14 @@ func loadSkillsFromDir(dir string, format skillFormat) []Skill {
106155 continue
107156 }
108157
109- skillFile := filepath .Join (fullPath , skillFile )
110- rawContent , err := os .ReadFile (skillFile )
158+ skillFilePath := filepath .Join (fullPath , skillFile )
159+ rawContent , err := os .ReadFile (skillFilePath )
111160 if err != nil {
112161 continue
113162 }
114163
115164 fm , _ := parseFrontmatter (string (rawContent ))
116- if fm . Description == "" {
165+ if ! isValidFrontmatter ( fm , entry . Name ()) {
117166 continue
118167 }
119168
@@ -123,10 +172,14 @@ func loadSkillsFromDir(dir string, format skillFormat) []Skill {
123172 }
124173
125174 skills = append (skills , Skill {
126- Name : name ,
127- Description : fm .Description ,
128- FilePath : skillFile ,
129- BaseDir : fullPath ,
175+ Name : name ,
176+ Description : fm .Description ,
177+ FilePath : skillFilePath ,
178+ BaseDir : fullPath ,
179+ License : fm .License ,
180+ Compatibility : fm .Compatibility ,
181+ Metadata : fm .Metadata ,
182+ AllowedTools : fm .AllowedTools ,
130183 })
131184
132185 case formatCodex :
@@ -138,22 +191,28 @@ func loadSkillsFromDir(dir string, format skillFormat) []Skill {
138191 continue
139192 }
140193
194+ skillDir := filepath .Dir (fullPath )
195+ dirName := filepath .Base (skillDir )
196+
141197 fm , _ := parseFrontmatter (string (rawContent ))
142- if fm . Description == "" {
198+ if ! isValidFrontmatter ( fm , dirName ) {
143199 continue
144200 }
145201
146- skillDir := filepath .Dir (fullPath )
147202 name := fm .Name
148203 if name == "" {
149- name = filepath . Base ( skillDir )
204+ name = dirName
150205 }
151206
152207 skills = append (skills , Skill {
153- Name : name ,
154- Description : fm .Description ,
155- FilePath : fullPath ,
156- BaseDir : skillDir ,
208+ Name : name ,
209+ Description : fm .Description ,
210+ FilePath : fullPath ,
211+ BaseDir : skillDir ,
212+ License : fm .License ,
213+ Compatibility : fm .Compatibility ,
214+ Metadata : fm .Metadata ,
215+ AllowedTools : fm .AllowedTools ,
157216 })
158217 }
159218 }
@@ -162,6 +221,27 @@ func loadSkillsFromDir(dir string, format skillFormat) []Skill {
162221 return skills
163222}
164223
224+ func isValidFrontmatter (fm frontmatter , dirName string ) bool {
225+ if fm .Description == "" || len (fm .Description ) > maxDescriptionLength {
226+ return false
227+ }
228+
229+ if fm .Compatibility != "" && len (fm .Compatibility ) > maxCompatLength {
230+ return false
231+ }
232+
233+ if fm .Name != "" {
234+ if ! isValidName (fm .Name ) {
235+ return false
236+ }
237+ if fm .Name != dirName {
238+ return false
239+ }
240+ }
241+
242+ return true
243+ }
244+
165245func Load () []Skill {
166246 skillMap := make (map [string ]Skill )
167247
@@ -202,21 +282,24 @@ func BuildSkillsPrompt(skills []Skill) string {
202282 }
203283
204284 var sb strings.Builder
205- sb .WriteString ("\n \n <available_skills> \n " )
206- sb .WriteString ("The following skills provide specialized instructions for specific tasks. \n " )
207- sb .WriteString ("Use the read_file tool to load a skill's file when the task matches its description. \n " )
208- sb .WriteString ("Skills may contain {baseDir} placeholders - replace them with the skill's base directory path .\n \n " )
285+ sb .WriteString ("The following skills provide specialized instructions for specific tasks. " )
286+ sb .WriteString ("Each skill's description indicates what it does and when to use it. \n \n " )
287+ sb .WriteString ("When a user's request matches a skill's description, use the read_file tool to load the skill's SKILL.md file from the location path. " )
288+ sb .WriteString ("The file contains detailed instructions to follow for that task .\n \n " )
209289
290+ sb .WriteString ("\n \n <available_skills>\n " )
210291 for _ , skill := range skills {
211- sb .WriteString ("- " )
292+ sb .WriteString (" <skill>\n " )
293+ sb .WriteString (" <name>" )
212294 sb .WriteString (skill .Name )
213- sb .WriteString (": " )
295+ sb .WriteString ("</name>\n " )
296+ sb .WriteString (" <description>" )
214297 sb .WriteString (skill .Description )
215- sb .WriteString ("\n File: " )
298+ sb .WriteString ("</description>\n " )
299+ sb .WriteString (" <location>" )
216300 sb .WriteString (skill .FilePath )
217- sb .WriteString ("\n Base directory: " )
218- sb .WriteString (skill .BaseDir )
219- sb .WriteString ("\n " )
301+ sb .WriteString ("</location>\n " )
302+ sb .WriteString (" </skill>\n " )
220303 }
221304
222305 sb .WriteString ("</available_skills>" )
0 commit comments