214 lines
4.6 KiB
Go
214 lines
4.6 KiB
Go
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_name>/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 ""
|
|
}
|