bots-garden/mini-mepublic Fork 0
d72271127802973540c648bfb372176cdaaa8e4f
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

skills.go · 140 lines · 4.2 KBGo Blame HistoryRaw
💾 Saved. d722711 k33g 7h ago1// Package skills discovers the markdown procedures in ./skills.
2//
3// A skill is a plain markdown file with a small front matter:
4//
5// ---
6// name: go-rename
7// description: rename a Go symbol everywhere it is used
8// ---
9// # Rename a Go symbol
10// ...
11//
12// The point of the package is to turn that directory into ONE STRING: the
13// description of the `read_skill` tool. The model never lists the directory
14// itself — it reads the catalogue in the tool description, the same way it
15// reads the description of `bash`.
16package skills
17
18import (
19 "os"
20 "path/filepath"
21 "sort"
22 "strings"
23)
24
25// Skill is one markdown procedure on disk.
26type Skill struct {
27 Name string // from the front matter, falling back to the file name
28 Description string // the one-liner shown in the catalogue
29 Path string
30}
31
32// skillFile is the file name of a skill stored in its own directory — the
33// Agent Skills convention (`<skillsDir>/<name>/SKILL.md`), the layout the
34// shipped examples use.
35const skillFile = "SKILL.md"
36
37// defaultName is the name a skill gets when its front matter has none: the
38// file name for a flat `<name>.md`, the directory name for `<name>/SKILL.md`.
39func defaultName(path string) string {
40 if filepath.Base(path) == skillFile {
41 return filepath.Base(filepath.Dir(path))
42 }
43 return strings.TrimSuffix(filepath.Base(path), ".md")
44}
45
46// parseHeader reads the `name:` and `description:` lines of the front matter.
47// It stops at the closing `---`: a `description:` in the body is not metadata.
48func parseHeader(content, path string) Skill {
49 s := Skill{Name: defaultName(path), Path: path}
50 lines := strings.Split(content, "\n")
51 if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
52 return s
53 }
54 for _, line := range lines[1:] {
55 if strings.TrimSpace(line) == "---" {
56 break
57 }
58 key, value, found := strings.Cut(line, ":")
59 if !found {
60 continue
61 }
62 value = strings.TrimSpace(value)
63 switch strings.TrimSpace(key) {
64 case "name":
65 if value != "" {
66 s.Name = value
67 }
68 case "description":
69 s.Description = value
70 }
71 }
72 return s
73}
74
75// List returns the skills of dir, sorted by name. Two layouts are accepted,
76// and may be mixed: a flat `<dir>/<name>.md`, and one directory per skill,
77// `<dir>/<name>/SKILL.md`. A missing directory is not an error: it just means
78// this agent has no skills.
79func List(dir string) []Skill {
80 flat, err := filepath.Glob(filepath.Join(dir, "*.md"))
81 if err != nil {
82 return nil
83 }
84 nested, err := filepath.Glob(filepath.Join(dir, "*", skillFile))
85 if err != nil {
86 return nil
87 }
88 var out []Skill
89 for _, p := range append(flat, nested...) {
90 content, err := os.ReadFile(p)
91 if err != nil {
92 continue
93 }
94 out = append(out, parseHeader(string(content), p))
95 }
96 sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
97 return out
98}
99
100// Read returns the full markdown of one skill, whichever layout it uses: the
101// flat `<name>.md` is tried first, then `<name>/SKILL.md`.
102func Read(dir, name string) (string, error) {
103 // Keep the name a plain file name: no "../" escaping out of the directory.
104 name = filepath.Base(name)
105 content, err := os.ReadFile(filepath.Join(dir, name+".md"))
106 if err != nil {
107 if nested, nestedErr := os.ReadFile(filepath.Join(dir, name, skillFile)); nestedErr == nil {
108 return string(nested), nil
109 }
110 }
111 return string(content), err
112}
113
114// Catalogue renders the list as the tool description the model will read.
115// This IS the prompt engineering: what the model knows about the available
116// skills is exactly these lines.
117func Catalogue(list []Skill) string {
118 var b strings.Builder
119 b.WriteString("Load a skill: the step-by-step procedure to follow for this kind of task. " +
120 "Call it BEFORE doing the work, with the name of the matching skill, and then follow " +
121 "what it says. Available skills:\n")
122 for _, s := range list {
123 b.WriteString(" " + s.Name)
124 if s.Description != "" {
125 b.WriteString(" — " + s.Description)
126 }
127 b.WriteString("\n")
128 }
129 return b.String()
130}
131
132// Names lists the skill names, for an error message that helps the model
133// recover from a typo.
134func Names(list []Skill) []string {
135 out := make([]string, 0, len(list))
136 for _, s := range list {
137 out = append(out, s.Name)
138 }
139 return out
140}