All checks were successful
Stable Release / stable (push) Successful in 1m34s
Major additions: - RAG pipeline (indexing, chunking, search) with sidebar upload button - Memory system with CRUD API - Plugins and lessons modules - MCP discovery and MCP server - Advanced skills (auto-create, conditional, improver) - Agent browser/image support, delegate, sessions - File editor with CodeMirror in split panes - Markdown rendering via react-markdown + KaTeX + highlight.js - Raw markdown toggle - PWA manifest + service worker - Extension UI redesign with new design tokens and studio-style chat - Pipeline API for chat streaming - Mobile responsive layout 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
283 lines
6.7 KiB
Go
283 lines
6.7 KiB
Go
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]
|
|
}
|