114 lines
3.1 KiB
Go
114 lines
3.1 KiB
Go
// Package skill implementiert Skill-Discovery mit Progressive Disclosure
|
|
// (5.5): Beim Start werden alle Skills gefunden, aber nur Name+Description
|
|
// in den Prompt geschrieben. Der Body kommt erst über das read_skill-Tool.
|
|
package skill
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type Skill struct {
|
|
Name string
|
|
Description string
|
|
Dir string // Verzeichnis des Skills (für weitere Dateien, die das Modell per read findet)
|
|
Path string // absoluter Pfad zu SKILL.md
|
|
Body string // Inhalt nach dem Frontmatter
|
|
}
|
|
|
|
// Discover sammelt Skills aus mehreren Verzeichnissen (4.7: skills.paths).
|
|
// Jedes Unterverzeichnis mit einer SKILL.md ist ein Skill. Spätere Pfade
|
|
// gewinnen bei Namenskollisionen (projekt-lokal überschreibt global) —
|
|
// dasselbe Präzedenzprinzip wie bei der Config (4.7).
|
|
func Discover(paths []string) ([]Skill, error) {
|
|
byName := make(map[string]Skill)
|
|
var order []string
|
|
|
|
for _, root := range paths {
|
|
root = expandHome(root)
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
continue // Pfad fehlt -> überspringen, kein Fehler
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
skillPath := filepath.Join(root, e.Name(), "SKILL.md")
|
|
data, err := os.ReadFile(skillPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
meta, body, err := parseFrontmatter(string(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("skill %s: %w", skillPath, err)
|
|
}
|
|
name := meta["name"]
|
|
if name == "" {
|
|
name = e.Name()
|
|
}
|
|
if _, seen := byName[name]; !seen {
|
|
order = append(order, name)
|
|
}
|
|
byName[name] = Skill{
|
|
Name: name,
|
|
Description: meta["description"],
|
|
Dir: filepath.Join(root, e.Name()),
|
|
Path: skillPath,
|
|
Body: body,
|
|
}
|
|
}
|
|
}
|
|
|
|
out := make([]Skill, 0, len(order))
|
|
for _, name := range order {
|
|
out = append(out, byName[name])
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// parseFrontmatter liest den Bereich zwischen den ersten beiden `---`-Zeilen
|
|
// als flache key: value-Paare. Kein echter YAML-Parser — für das dokumentierte
|
|
// Format (zwei flache String-Felder) reicht das, und es spart eine
|
|
// Dependency, die nicht in der Allowlist steht (2.3).
|
|
func parseFrontmatter(content string) (map[string]string, string, error) {
|
|
lines := strings.Split(content, "\n")
|
|
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
|
return nil, "", fmt.Errorf("missing frontmatter (expected leading ---)")
|
|
}
|
|
|
|
meta := make(map[string]string)
|
|
i := 1
|
|
for ; i < len(lines); i++ {
|
|
line := lines[i]
|
|
if strings.TrimSpace(line) == "---" {
|
|
break
|
|
}
|
|
key, val, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
val = strings.Trim(strings.TrimSpace(val), `"'`)
|
|
meta[key] = val
|
|
}
|
|
if i >= len(lines) {
|
|
return nil, "", fmt.Errorf("unterminated frontmatter (missing closing ---)")
|
|
}
|
|
|
|
body := strings.TrimLeft(strings.Join(lines[i+1:], "\n"), "\n")
|
|
return meta, body, nil
|
|
}
|
|
|
|
func expandHome(path string) string {
|
|
if !strings.HasPrefix(path, "~") {
|
|
return path
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return path
|
|
}
|
|
return filepath.Join(home, strings.TrimPrefix(path, "~"))
|
|
}
|