package skills import ( "crypto/sha256" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" ) type PatternMatch struct { Pattern string Count int LastSeen time.Time ExampleText string } type AutoCreateProposal struct { Name string Description string SuggestedTags []string Category string Patterns []PatternMatch Confidence float64 CreatedFrom string } type ConversationSnippet struct { Role string `json:"role"` Content string `json:"content"` Timestamp time.Time `json:"timestamp"` } func AnalyzeConversation(snippets []ConversationSnippet) []AutoCreateProposal { patterns := detectPatterns(snippets) var proposals []AutoCreateProposal for _, p := range patterns { if p.Count < 3 { continue } name := generateSkillName(p.Pattern) proposal := AutoCreateProposal{ Name: name, Description: fmt.Sprintf("Auto-detected skill for recurring pattern: %s", p.Pattern), SuggestedTags: extractTags(p.Pattern), Category: categorize(p.Pattern), Patterns: []PatternMatch{p}, Confidence: computeConfidence(p), CreatedFrom: "conversation", } proposals = append(proposals, proposal) } return proposals } func CreateFromProposal(proposal *AutoCreateProposal) (*Skill, error) { skill := &Skill{ Name: proposal.Name, Description: proposal.Description, Author: "muyue-auto", Version: "0.1.0", Tags: proposal.SuggestedTags, Category: proposal.Category, Target: "both", CreatedFrom: proposal.CreatedFrom, AutoImprove: true, Content: buildAutoSkillContent(proposal), } return skill, Create(skill) } func LoadProposals() ([]AutoCreateProposal, error) { dir, err := proposalsDir() if err != nil { return nil, err } entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var proposals []AutoCreateProposal for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { continue } data, err := os.ReadFile(filepath.Join(dir, e.Name())) if err != nil { continue } var p AutoCreateProposal if err := json.Unmarshal(data, &p); err != nil { continue } proposals = append(proposals, p) } return proposals, nil } func SaveProposal(proposal *AutoCreateProposal) error { dir, err := proposalsDir() if err != nil { return err } if err := os.MkdirAll(dir, 0755); err != nil { return err } data, err := json.MarshalIndent(proposal, "", " ") if err != nil { return err } path := filepath.Join(dir, proposal.Name+".json") return os.WriteFile(path, data, 0644) } func DeleteProposal(name string) error { dir, err := proposalsDir() if err != nil { return err } path := filepath.Join(dir, name+".json") return os.Remove(path) } func proposalsDir() (string, error) { dir, err := SkillsDir() if err != nil { return "", err } return filepath.Join(filepath.Dir(dir), ".muyue", "proposals"), nil } func detectPatterns(snippets []ConversationSnippet) []PatternMatch { commandPatterns := make(map[string]*PatternMatch) for _, s := range snippets { if s.Role != "assistant" { continue } lines := strings.Split(s.Content, "\n") for _, line := range lines { line = strings.TrimSpace(line) if isCommandPattern(line) { key := extractPatternKey(line) if key == "" { continue } if existing, ok := commandPatterns[key]; ok { existing.Count++ if s.Timestamp.After(existing.LastSeen) { existing.LastSeen = s.Timestamp existing.ExampleText = truncate(line, 200) } } else { commandPatterns[key] = &PatternMatch{ Pattern: key, Count: 1, LastSeen: s.Timestamp, ExampleText: truncate(line, 200), } } } } } var patterns []PatternMatch for _, p := range commandPatterns { patterns = append(patterns, *p) } return patterns } func isCommandPattern(line string) bool { toolPrefixes := []string{"go test", "go build", "go run", "npm test", "npm run", "docker build", "docker run", "git commit", "git push", "kubectl", "cargo test", "cargo build", "pytest", "make "} for _, prefix := range toolPrefixes { if strings.HasPrefix(line, prefix) { return true } } return false } func extractPatternKey(line string) string { parts := strings.Fields(line) if len(parts) < 2 { return "" } if len(parts) >= 3 && (parts[0] == "go" || parts[0] == "npm" || parts[0] == "cargo" || parts[0] == "git" || parts[0] == "docker") { return parts[0] + " " + parts[1] } return parts[0] } func generateSkillName(pattern string) string { name := strings.ReplaceAll(pattern, " ", "-") name = strings.ToLower(name) if len(name) > 30 { name = name[:30] } h := sha256.Sum256([]byte(pattern)) return fmt.Sprintf("auto-%s-%x", name, h[:4]) } func extractTags(pattern string) []string { var tags []string parts := strings.Fields(pattern) for _, p := range parts { if len(p) > 2 { tags = append(tags, strings.ToLower(p)) } } return tags } func categorize(pattern string) string { categories := map[string]string{ "go test": "testing", "go build": "build", "go run": "build", "npm test": "testing", "npm run": "build", "docker build": "devops", "docker run": "devops", "git commit": "workflow", "git push": "workflow", "kubectl": "devops", "cargo test": "testing", "cargo build": "build", "pytest": "testing", "make": "build", } for prefix, cat := range categories { if strings.HasPrefix(pattern, prefix) { return cat } } return "general" } func computeConfidence(p PatternMatch) float64 { confidence := 0.3 confidence += float64(p.Count) * 0.1 if confidence > 0.95 { confidence = 0.95 } return confidence } func buildAutoSkillContent(proposal *AutoCreateProposal) string { var b strings.Builder b.WriteString(fmt.Sprintf("# %s\n\n", strings.Title(proposal.Name))) b.WriteString("Auto-generated skill based on recurring patterns detected in conversations.\n\n") b.WriteString("## Activation\n\n") b.WriteString("This skill activates when the following patterns are detected:\n\n") for _, p := range proposal.Patterns { b.WriteString(fmt.Sprintf("- `%s` (seen %d times)\n", p.Pattern, p.Count)) } b.WriteString("\n## Instructions\n\n") b.WriteString("1. Detect the pattern context from the user request\n") b.WriteString("2. Apply the standard workflow for this pattern\n") b.WriteString("3. Handle common errors and edge cases\n") b.WriteString("4. Verify the result\n\n") b.WriteString("## Error Handling\n\n") b.WriteString("- If a command fails, check for missing dependencies\n") b.WriteString("- Suggest alternative approaches when the standard pattern doesn't fit\n") return b.String() } func truncate(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] }