package knowledge import ( "fmt" "os" "path/filepath" "sort" "strings" ) type Skill struct { Name string Content string Source string } type SkillSummary struct { DirName string Name string Description string Source string } type scannedSkill struct { dirName string name string description string content string source string } func LoadSoul(path string) (string, error) { b, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("read soul file failed: %w", err) } content := strings.TrimSpace(string(b)) if content == "" { return "", fmt.Errorf("soul file is empty") } return content, nil } func LoadSkillSet(dir string) ([]Skill, error) { scanned, err := scanSkills(dir) if err != nil { return nil, err } out := make([]Skill, 0, len(scanned)) for _, s := range scanned { out = append(out, Skill{ Name: s.name, Content: s.content, Source: s.source, }) } return out, nil } func LoadSkillSummaries(dir string) ([]SkillSummary, error) { scanned, err := scanSkills(dir) if err != nil { return nil, err } out := make([]SkillSummary, 0, len(scanned)) for _, s := range scanned { out = append(out, SkillSummary{ DirName: s.dirName, Name: s.name, Description: s.description, Source: s.source, }) } return out, nil } func scanSkills(dir string) ([]scannedSkill, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, fmt.Errorf("read skills dir failed: %w", err) } skillDirs := make([]string, 0) for _, entry := range entries { if entry.IsDir() { skillDirs = append(skillDirs, entry.Name()) } } sort.Strings(skillDirs) out := make([]scannedSkill, 0, len(skillDirs)) for _, skillDir := range skillDirs { file := filepath.Join(dir, skillDir, "skill.md") b, err := os.ReadFile(file) if err != nil { if os.IsNotExist(err) { continue } return nil, fmt.Errorf("read skill file failed: %w", err) } content := strings.TrimSpace(string(b)) if content == "" { continue } name, description := parseSkillNameDescription(skillDir, content) if strings.TrimSpace(name) == "" { continue } out = append(out, scannedSkill{ dirName: skillDir, name: name, description: description, content: content, source: file, }) } if len(out) == 0 { return nil, fmt.Errorf("no valid skills loaded from %s (expected: skills//skill.md)", dir) } return out, nil } func extractSkillName(fileName, markdown string) string { for _, line := range strings.Split(markdown, "\n") { line = strings.TrimSpace(line) if !strings.HasPrefix(line, "#") { continue } title := strings.TrimSpace(strings.TrimLeft(line, "#")) title = strings.TrimSpace(strings.TrimPrefix(title, "Skill:")) title = strings.TrimSpace(strings.TrimPrefix(title, "skill:")) if title != "" { return title } } base := strings.TrimSuffix(fileName, filepath.Ext(fileName)) if base == "" { return fileName } return base } func parseSkillNameDescription(fileName, markdown string) (string, string) { name := "" description := "" fm := parseFrontMatter(markdown) if v, ok := fm["name"]; ok { name = strings.TrimSpace(v) } if v, ok := fm["description"]; ok { description = strings.TrimSpace(v) } if name == "" { name = extractSkillName(fileName, markdown) } if description == "" { description = extractSkillDescription(markdown) } return name, description } func parseFrontMatter(markdown string) map[string]string { lines := strings.Split(markdown, "\n") out := map[string]string{} if len(lines) < 3 { return out } if strings.TrimSpace(lines[0]) != "---" { return out } for i := 1; i < len(lines); i++ { line := strings.TrimSpace(lines[i]) if line == "---" { break } idx := strings.Index(line, ":") if idx <= 0 { continue } k := strings.ToLower(strings.TrimSpace(line[:idx])) v := strings.TrimSpace(line[idx+1:]) v = strings.Trim(v, "\"'") if k != "" && v != "" { out[k] = v } } return out } func extractSkillDescription(markdown string) string { lines := strings.Split(markdown, "\n") for _, raw := range lines { line := strings.TrimSpace(raw) if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "---") { continue } if strings.Contains(line, ":") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { left := strings.ToLower(strings.TrimSpace(parts[0])) if left == "name" || left == "description" || left == "source" || left == "generated_at" { continue } } } if len(line) > 200 { line = line[:200] } return line } return "" }