// 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, "~")) }