2026-02-21 23:01:39 +08:00
|
|
|
package knowledge
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-21 23:29:27 +08:00
|
|
|
type Skill struct {
|
|
|
|
|
Name string
|
|
|
|
|
Content string
|
|
|
|
|
Source string
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:01:39 +08:00
|
|
|
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 LoadSkills(dir string) (string, error) {
|
2026-02-21 23:29:27 +08:00
|
|
|
skills, err := LoadSkillSet(dir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
builder := strings.Builder{}
|
|
|
|
|
for _, skill := range skills {
|
|
|
|
|
builder.WriteString("## ")
|
|
|
|
|
builder.WriteString(skill.Name)
|
|
|
|
|
builder.WriteString("\n")
|
|
|
|
|
builder.WriteString(skill.Content)
|
|
|
|
|
builder.WriteString("\n\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out := strings.TrimSpace(builder.String())
|
|
|
|
|
if out == "" {
|
|
|
|
|
return "", fmt.Errorf("no non-empty markdown skills loaded from %s", dir)
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LoadSkillSet(dir string) ([]Skill, error) {
|
2026-02-21 23:01:39 +08:00
|
|
|
entries, err := os.ReadDir(dir)
|
|
|
|
|
if err != nil {
|
2026-02-21 23:29:27 +08:00
|
|
|
return nil, fmt.Errorf("read skills dir failed: %w", err)
|
2026-02-21 23:01:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:29:27 +08:00
|
|
|
skillDirs := make([]string, 0)
|
2026-02-21 23:01:39 +08:00
|
|
|
for _, entry := range entries {
|
|
|
|
|
if entry.IsDir() {
|
2026-02-21 23:29:27 +08:00
|
|
|
skillDirs = append(skillDirs, entry.Name())
|
2026-02-21 23:01:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-21 23:29:27 +08:00
|
|
|
sort.Strings(skillDirs)
|
2026-02-21 23:01:39 +08:00
|
|
|
|
2026-02-21 23:29:27 +08:00
|
|
|
out := make([]Skill, 0, len(skillDirs))
|
|
|
|
|
for _, skillDir := range skillDirs {
|
|
|
|
|
file := filepath.Join(dir, skillDir, "skill.md")
|
2026-02-21 23:01:39 +08:00
|
|
|
b, err := os.ReadFile(file)
|
|
|
|
|
if err != nil {
|
2026-02-21 23:29:27 +08:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("read skill file failed: %w", err)
|
2026-02-21 23:01:39 +08:00
|
|
|
}
|
|
|
|
|
content := strings.TrimSpace(string(b))
|
|
|
|
|
if content == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-02-21 23:29:27 +08:00
|
|
|
|
|
|
|
|
name := extractSkillName(skillDir, content)
|
|
|
|
|
out = append(out, Skill{
|
|
|
|
|
Name: name,
|
|
|
|
|
Content: content,
|
|
|
|
|
Source: file,
|
|
|
|
|
})
|
2026-02-21 23:01:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:29:27 +08:00
|
|
|
if len(out) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("no valid skills loaded from %s (expected: skills/<skill_name>/skill.md)", dir)
|
2026-02-21 23:01:39 +08:00
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
2026-02-21 23:29:27 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|