package skills import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "strings" "time" "gopkg.in/yaml.v3" ) type SkillDependency struct { Type string `yaml:"type,omitempty" json:"type,omitempty"` Name string `yaml:"name,omitempty" json:"name,omitempty"` Version string `yaml:"version,omitempty" json:"version,omitempty"` Required bool `yaml:"required,omitempty" json:"required,omitempty"` } 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:"-"` Dependencies []SkillDependency `yaml:"dependencies,omitempty" json:"dependencies,omitempty"` Languages []string `yaml:"languages,omitempty" json:"languages,omitempty"` Category string `yaml:"category,omitempty" json:"category,omitempty"` Deployed bool `yaml:"-" json:"deployed,omitempty"` RequiresTools []string `yaml:"requires_tools,omitempty" json:"requires_tools,omitempty"` FallbackForTools []string `yaml:"fallback_for_tools,omitempty" json:"fallback_for_tools,omitempty"` AutoImprove bool `yaml:"auto_improve,omitempty" json:"auto_improve,omitempty"` CreatedFrom string `yaml:"created_from,omitempty" json:"created_from,omitempty"` ImprovementCount int `yaml:"improvement_count,omitempty" json:"improvement_count,omitempty"` LastImprovedAt *time.Time `yaml:"last_improved_at,omitempty" json:"last_improved_at,omitempty"` } type ValidationError struct { Field string `json:"field"` Message string `json:"message"` } func (v ValidationError) Error() string { return fmt.Sprintf("%s: %s", v.Field, v.Message) } type SkillTestResult struct { Name string `json:"name"` Passed bool `json:"passed"` Message string `json:"message"` } 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) } 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 { if errs := Validate(skill); len(errs) > 0 { return fmt.Errorf("validation failed: %v", errs) } 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 IsDeployed(name string) bool { home, err := os.UserHomeDir() if err != nil { return false } crushPath := filepath.Join(home, ".config", "crush", "skills", name, "SKILL.md") claudePath := filepath.Join(home, ".claude", "skills", name, "SKILL.md") _, crushErr := os.Stat(crushPath) _, claudeErr := os.Stat(claudePath) return crushErr == nil || claudeErr == nil } func Undeploy(name string) error { skill, err := Get(name) if err != nil { return err } undeployFromTargets(skill.Name) return nil } func Update(skill *Skill) error { if errs := Validate(skill); len(errs) > 0 { return fmt.Errorf("validation failed: %v", errs) } dir, err := SkillsDir() if err != nil { return err } skillDir := filepath.Join(dir, skill.Name) skillPath := filepath.Join(skillDir, "SKILL.md") skill.UpdatedAt = time.Now() content := renderSkill(skill) if err := os.WriteFile(skillPath, []byte(content), 0644); err != nil { return err } return Deploy(skill) } 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 Validate(skill *Skill) []ValidationError { var errs []ValidationError if skill.Name == "" { errs = append(errs, ValidationError{Field: "name", Message: "name is required"}) } if skill.Name != "" { if matched, _ := regexp.MatchString(`^[a-z0-9][a-z0-9-]*$`, skill.Name); !matched { errs = append(errs, ValidationError{Field: "name", Message: "name must be lowercase alphanumeric with dashes"}) } } if skill.Description == "" { errs = append(errs, ValidationError{Field: "description", Message: "description is required"}) } if skill.Content == "" { errs = append(errs, ValidationError{Field: "content", Message: "content is required"}) } if skill.Target != "" && skill.Target != "crush" && skill.Target != "claude" && skill.Target != "both" { errs = append(errs, ValidationError{Field: "target", Message: "target must be crush, claude, or both"}) } if skill.Version != "" { if matched, _ := regexp.MatchString(`^\d+\.\d+\.\d+$`, skill.Version); !matched { errs = append(errs, ValidationError{Field: "version", Message: "version must be semver (e.g. 1.0.0)"}) } } for i, dep := range skill.Dependencies { if dep.Type != "mcp_server" && dep.Type != "lsp" && dep.Type != "tool" && dep.Type != "runtime" && dep.Type != "" { errs = append(errs, ValidationError{ Field: fmt.Sprintf("dependencies[%d].type", i), Message: "dependency type must be mcp_server, lsp, tool, or runtime", }) } if dep.Name == "" { errs = append(errs, ValidationError{ Field: fmt.Sprintf("dependencies[%d].name", i), Message: "dependency name is required", }) } } return errs } func CheckDependencies(skill *Skill) []SkillDependency { var missing []SkillDependency for _, dep := range skill.Dependencies { switch dep.Type { case "mcp_server": if !isMCPServerAvailable(dep.Name) { missing = append(missing, dep) } case "lsp", "tool", "runtime": if !isToolAvailable(dep.Name) { missing = append(missing, dep) } } } return missing } func isToolAvailable(name string) bool { _, err := lookPath(name) return err == nil } func lookPath(name string) (string, error) { pathEnv := os.Getenv("PATH") home, _ := os.UserHomeDir() if home != "" { pathEnv = home + "/.local/bin:" + home + "/go/bin:" + pathEnv } for _, dir := range filepath.SplitList(pathEnv) { candidate := filepath.Join(dir, name) if info, err := os.Stat(candidate); err == nil && !info.IsDir() { return candidate, nil } } return "", fmt.Errorf("%s not found", name) } func isMCPServerAvailable(name string) bool { home, _ := os.UserHomeDir() if home == "" { return false } configPath := filepath.Join(home, ".config", "crush", "crush.json") data, err := os.ReadFile(configPath) if err != nil { return false } var cfg map[string]interface{} if err := json.Unmarshal(data, &cfg); err != nil { return false } mcps, ok := cfg["mcps"].(map[string]interface{}) if !ok { return false } _, exists := mcps[name] return exists } func Export(name string, exportPath string) error { skill, err := Get(name) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(exportPath), 0755); err != nil { return err } content := renderSkill(skill) return os.WriteFile(exportPath, []byte(content), 0644) } func Import(exportPath string) (*Skill, error) { data, err := os.ReadFile(exportPath) if err != nil { return nil, fmt.Errorf("read export file: %w", err) } skill, err := parseSkill(data) if err != nil { return nil, err } name := filepath.Base(filepath.Dir(exportPath)) if skill.Name == "" { skill.Name = strings.TrimSuffix(filepath.Base(exportPath), ".md") if skill.Name == "SKILL" { skill.Name = filepath.Base(filepath.Dir(exportPath)) } } _ = name if errs := Validate(skill); len(errs) > 0 { return nil, fmt.Errorf("validation failed: %v", errs) } return skill, nil } func DryRun(name string, sampleTask string) SkillTestResult { skill, err := Get(name) if err != nil { return SkillTestResult{Name: name, Passed: false, Message: fmt.Sprintf("skill not found: %s", err)} } if skill.Content == "" { return SkillTestResult{Name: name, Passed: false, Message: "skill has no content"} } if len(skill.Dependencies) > 0 { missing := CheckDependencies(skill) if len(missing) > 0 { var names []string for _, d := range missing { names = append(names, d.Name) } return SkillTestResult{ Name: name, Passed: false, Message: fmt.Sprintf("missing dependencies: %s", strings.Join(names, ", ")), } } } if sampleTask != "" { tags := skill.Tags taskLower := strings.ToLower(sampleTask) matched := false for _, tag := range tags { if strings.Contains(taskLower, strings.ToLower(tag)) { matched = true break } } if len(tags) > 0 && !matched { return SkillTestResult{ Name: name, Passed: true, Message: "skill loaded but sample task does not match skill tags", } } } return SkillTestResult{ Name: name, Passed: true, Message: "skill validated successfully", } } 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 skill.Category != "" { b.WriteString(fmt.Sprintf("category: %s\n", skill.Category)) } if len(skill.Tags) > 0 { b.WriteString(fmt.Sprintf("tags: [%s]\n", strings.Join(skill.Tags, ", "))) } if len(skill.Languages) > 0 { b.WriteString(fmt.Sprintf("languages: [%s]\n", strings.Join(skill.Languages, ", "))) } if len(skill.Dependencies) > 0 { b.WriteString("dependencies:\n") for _, dep := range skill.Dependencies { req := "" if dep.Required { req = ", required: true" } b.WriteString(fmt.Sprintf(" - type: %s, name: %s%s\n", dep.Type, dep.Name, req)) } } if len(skill.RequiresTools) > 0 { b.WriteString(fmt.Sprintf("requires_tools: [%s]\n", strings.Join(skill.RequiresTools, ", "))) } if len(skill.FallbackForTools) > 0 { b.WriteString(fmt.Sprintf("fallback_for_tools: [%s]\n", strings.Join(skill.FallbackForTools, ", "))) } if skill.AutoImprove { b.WriteString("auto_improve: true\n") } if skill.CreatedFrom != "" { b.WriteString(fmt.Sprintf("created_from: %s\n", skill.CreatedFrom)) } if skill.ImprovementCount > 0 { b.WriteString(fmt.Sprintf("improvement_count: %d\n", skill.ImprovementCount)) } if skill.LastImprovedAt != nil { b.WriteString(fmt.Sprintf("last_improved_at: %s\n", skill.LastImprovedAt.Format(time.RFC3339))) } 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, tags, dependencies (if needed) 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 If the skill requires specific tools, MCP servers, or LSP servers, declare them as dependencies: - type: mcp_server, name: - type: lsp, name: - type: tool, name: Output ONLY the skill file content, starting with ---`, name, description, target) }