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) }