Files
MuyueWorkspace/internal/skills/skills.go
Augustin fc7981037f
All checks were successful
Beta Release / beta (push) Successful in 34s
chore: remove dead code (packages, functions, types, constants)
Remove 5 unused packages (daemon, preview, proxy, workflow) and dead
symbols across 7 files: orchestrator workflow engine, skills Target type
and Update(), LSP config generation, installer SetupPrompt(), unexported
desktop options, and version License/Prerelease. Total: -1453 lines.
2026-04-21 22:09:42 +02:00

260 lines
6.0 KiB
Go

package skills
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"gopkg.in/yaml.v3"
)
type Skill struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
Content string `yaml:"content" json:"content"`
Author string `yaml:"author" json:"author"`
Version string `yaml:"version" json:"version"`
CreatedAt time.Time `yaml:"created_at" json:"created_at"`
UpdatedAt time.Time `yaml:"updated_at" json:"updated_at"`
Tags []string `yaml:"tags" json:"tags"`
Target string `yaml:"target" json:"target"`
FilePath string `yaml:"-" json:"-"`
}
func SkillsDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
dir := filepath.Join(home, ".muyue", "skills")
return dir, nil
}
func List() ([]Skill, error) {
dir, err := SkillsDir()
if err != nil {
return nil, err
}
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return []Skill{}, nil
}
return nil, err
}
var skills []Skill
for _, e := range entries {
if !e.IsDir() {
continue
}
skillPath := filepath.Join(dir, e.Name(), "SKILL.md")
data, err := os.ReadFile(skillPath)
if err != nil {
continue
}
skill, err := parseSkill(data)
if err != nil {
continue
}
skill.FilePath = skillPath
skill.Name = e.Name()
skills = append(skills, *skill)
}
sort.Slice(skills, func(i, j int) bool {
return skills[i].Name < skills[j].Name
})
return skills, nil
}
func Get(name string) (*Skill, error) {
dir, err := SkillsDir()
if err != nil {
return nil, err
}
skillPath := filepath.Join(dir, name, "SKILL.md")
data, err := os.ReadFile(skillPath)
if err != nil {
return nil, fmt.Errorf("skill '%s' not found", name)
}
skill, err := parseSkill(data)
if err != nil {
return nil, err
}
skill.FilePath = skillPath
skill.Name = name
return skill, nil
}
func Create(skill *Skill) error {
dir, err := SkillsDir()
if err != nil {
return err
}
skillDir := filepath.Join(dir, skill.Name)
if err := os.MkdirAll(skillDir, 0755); err != nil {
return err
}
skillPath := filepath.Join(skillDir, "SKILL.md")
content := renderSkill(skill)
if err := os.WriteFile(skillPath, []byte(content), 0644); err != nil {
return err
}
return Deploy(skill)
}
func Delete(name string) error {
dir, err := SkillsDir()
if err != nil {
return err
}
skillDir := filepath.Join(dir, name)
if err := os.RemoveAll(skillDir); err != nil {
return err
}
undeployFromTargets(name)
return nil
}
func Deploy(skill *Skill) error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("get home dir: %w", err)
}
if skill.Target == "crush" || skill.Target == "both" {
crushSkillsDir := filepath.Join(home, ".config", "crush", "skills")
if err := os.MkdirAll(crushSkillsDir, 0755); err != nil {
return fmt.Errorf("create crush skills dir: %w", err)
}
target := filepath.Join(crushSkillsDir, skill.Name)
if err := os.MkdirAll(target, 0755); err != nil {
return fmt.Errorf("create skill dir: %w", err)
}
content := renderSkill(skill)
if err := os.WriteFile(filepath.Join(target, "SKILL.md"), []byte(content), 0644); err != nil {
return fmt.Errorf("write crush skill: %w", err)
}
}
if skill.Target == "claude" || skill.Target == "both" {
claudeSkillsDir := filepath.Join(home, ".claude", "skills")
if err := os.MkdirAll(claudeSkillsDir, 0755); err != nil {
return fmt.Errorf("create claude skills dir: %w", err)
}
target := filepath.Join(claudeSkillsDir, skill.Name)
if err := os.MkdirAll(target, 0755); err != nil {
return fmt.Errorf("create skill dir: %w", err)
}
content := renderSkill(skill)
if err := os.WriteFile(filepath.Join(target, "SKILL.md"), []byte(content), 0644); err != nil {
return fmt.Errorf("write claude skill: %w", err)
}
}
return nil
}
func DeployAll() error {
skills, err := List()
if err != nil {
return err
}
for _, s := range skills {
if err := Deploy(&s); err != nil {
return fmt.Errorf("deploy %s: %w", s.Name, err)
}
}
return nil
}
func undeployFromTargets(name string) {
home, _ := os.UserHomeDir()
os.RemoveAll(filepath.Join(home, ".config", "crush", "skills", name))
os.RemoveAll(filepath.Join(home, ".claude", "skills", name))
}
func parseSkill(data []byte) (*Skill, error) {
content := string(data)
if !strings.HasPrefix(content, "---") {
return &Skill{Content: content}, nil
}
end := strings.Index(content[3:], "---")
if end == -1 {
return &Skill{Content: content}, nil
}
frontmatter := content[3 : end+3]
body := strings.TrimSpace(content[end+6:])
skill := &Skill{Content: body}
if err := yaml.Unmarshal([]byte(frontmatter), skill); err != nil {
return &Skill{Content: content}, nil
}
return skill, nil
}
func renderSkill(skill *Skill) string {
var b strings.Builder
b.WriteString("---\n")
b.WriteString(fmt.Sprintf("name: %s\n", skill.Name))
b.WriteString(fmt.Sprintf("description: %s\n", skill.Description))
if skill.Author != "" {
b.WriteString(fmt.Sprintf("author: %s\n", skill.Author))
}
if skill.Version != "" {
b.WriteString(fmt.Sprintf("version: %s\n", skill.Version))
}
if skill.Target != "" {
b.WriteString(fmt.Sprintf("target: %s\n", skill.Target))
}
if len(skill.Tags) > 0 {
b.WriteString(fmt.Sprintf("tags: [%s]\n", strings.Join(skill.Tags, ", ")))
}
b.WriteString("---\n\n")
b.WriteString(skill.Content)
b.WriteString("\n")
return b.String()
}
func BuildAIGeneratePrompt(name, description, target string) string {
return fmt.Sprintf(`Generate a skill file for an AI coding assistant.
SKILL NAME: %s
DESCRIPTION: %s
TARGET: %s (crush = Crush with GLM, claude = Claude Code, both = both tools)
The skill must follow this EXACT format:
1. YAML frontmatter with: name, description
2. Markdown body with detailed instructions
The skill should be practical, specific, and actionable.
Include:
- When to activate this skill
- Step-by-step instructions
- Examples where relevant
- Error handling guidance
Output ONLY the skill file content, starting with ---`, name, description, target)
}