Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion pkg/skills/skills.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,34 @@ type Skill struct {
License string `yaml:"license"`
Compatibility string `yaml:"compatibility"`
Metadata map[string]string `yaml:"metadata"`
AllowedTools []string `yaml:"allowed-tools"`
AllowedTools stringOrList `yaml:"allowed-tools"`
}

// stringOrList is a []string that can be unmarshalled from either a YAML list
// or a single comma-separated string (e.g. "Read, Grep").
type stringOrList []string

func (s *stringOrList) UnmarshalYAML(unmarshal func(any) error) error {
var list []string
if err := unmarshal(&list); err == nil {
*s = list
return nil
}

var single string
if err := unmarshal(&single); err != nil {
return err
}

parts := strings.Split(single, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
if t := strings.TrimSpace(p); t != "" {
result = append(result, t)
}
}
*s = result
return nil
}

// Load discovers and loads skills from the given sources.
Expand Down
36 changes: 34 additions & 2 deletions pkg/skills/skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,39 @@ Body`,
License: "Apache-2.0",
Compatibility: "Requires docker and git",
Metadata: map[string]string{"author": "test-org", "version": "1.0"},
AllowedTools: []string{"Bash(git:*)", "Read", "Write"},
AllowedTools: stringOrList{"Bash(git:*)", "Read", "Write"},
},
wantOK: true,
},
{
name: "allowed-tools as comma-separated string",
content: `---
name: csv-skill
description: Skill with comma-separated allowed tools
allowed-tools: Read, Grep, Write
---

Body`,
want: Skill{
Name: "csv-skill",
Description: "Skill with comma-separated allowed tools",
AllowedTools: stringOrList{"Read", "Grep", "Write"},
},
wantOK: true,
},
{
name: "allowed-tools as single string without commas",
content: `---
name: single-tool-skill
description: Skill with a single allowed tool
allowed-tools: Read
---

Body`,
want: Skill{
Name: "single-tool-skill",
Description: "Skill with a single allowed tool",
AllowedTools: stringOrList{"Read"},
},
wantOK: true,
},
Expand Down Expand Up @@ -225,7 +257,7 @@ allowed-tools:
assert.Equal(t, "Apache-2.0", skills[0].License)
assert.Equal(t, "Requires docker", skills[0].Compatibility)
assert.Equal(t, map[string]string{"author": "test-org", "version": "2.0"}, skills[0].Metadata)
assert.Equal(t, []string{"Bash(git:*)", "Read"}, skills[0].AllowedTools)
assert.Equal(t, stringOrList{"Bash(git:*)", "Read"}, skills[0].AllowedTools)
}

func TestLoadSkillsFromDir_NonExistentDir(t *testing.T) {
Expand Down