diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..72f8471 Binary files /dev/null and b/.DS_Store differ diff --git a/cmd/orca/main.go b/cmd/orca/main.go index 4bf85cf..da6c191 100644 --- a/cmd/orca/main.go +++ b/cmd/orca/main.go @@ -9,12 +9,14 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/orca/orca/internal/config" "github.com/orca/orca/internal/tui" - "github.com/orca/orca/internal/web" + "github.com/orca/orca/internal/websocket" "github.com/orca/orca/pkg/kernel" + "github.com/orca/orca/pkg/session" ) func main() { webMode := flag.Bool("web", false, "Run in web mode on port 8081") + memoryCmd := flag.Bool("memory", false, "Memory management commands (list, stats, clean)") flag.Parse() cfg, err := config.LoadConfig() @@ -22,6 +24,11 @@ func main() { log.Fatalf("Failed to load config: %v", err) } + if *memoryCmd { + handleMemoryCommand(flag.Args()) + return + } + k := kernel.NewWithConfig(cfg) if err := k.Start(); err != nil { log.Fatalf("Failed to start kernel: %v", err) @@ -32,7 +39,7 @@ func main() { } if *webMode { - server := web.NewServer(k, 8081) + server := websocket.NewServer(k, 8081) fmt.Println("Starting web server on http://localhost:8081") if err := server.Start(); err != nil { log.Fatalf("Failed to start web server: %v", err) @@ -51,3 +58,52 @@ func main() { log.Printf("Warning: error stopping kernel: %v", err) } } + +func handleMemoryCommand(args []string) { + if len(args) == 0 { + fmt.Println("Usage: orca -memory ") + fmt.Println("Commands:") + fmt.Println(" list List all memories") + fmt.Println(" stats Show memory statistics") + fmt.Println(" clean Clean archived memories") + return + } + + cfg, err := config.LoadConfig() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + mm, err := session.NewMemoryManager(session.MemoryConfig{ + DBPath: cfg.Session.StorageDir + "/memory.db", + }) + if err != nil { + log.Fatalf("Failed to create memory manager: %v", err) + } + defer mm.Close() + + switch args[0] { + case "list": + memories, err := mm.GetLongTermMemory("") + if err != nil { + log.Fatalf("Failed to list memories: %v", err) + } + fmt.Printf("Total memories: %d\n", len(memories)) + for _, m := range memories { + fmt.Printf(" [%d] %s (weight=%.2f)\n", m.ID, m.Content, m.Weight) + } + case "stats": + size, hits, misses := mm.CacheStats() + fmt.Printf("Cache size: %d\n", size) + fmt.Printf("Cache hits: %d\n", hits) + fmt.Printf("Cache misses: %d\n", misses) + case "clean": + count, err := mm.ArchiveLowWeightMemories(0.3) + if err != nil { + log.Fatalf("Failed to clean memories: %v", err) + } + fmt.Printf("Archived %d memories\n", count) + default: + fmt.Printf("Unknown command: %s\n", args[0]) + } +} diff --git a/go.mod b/go.mod index f98b96d..9bc2555 100644 --- a/go.mod +++ b/go.mod @@ -3,41 +3,41 @@ module github.com/orca/orca go 1.26.1 require ( - github.com/BurntSushi/toml v1.6.0 // indirect - github.com/alecthomas/chroma/v2 v2.20.0 // indirect + github.com/BurntSushi/toml v1.6.0 + github.com/charmbracelet/bubbles v1.0.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 + github.com/gorilla/websocket v1.5.3 + modernc.org/sqlite v1.50.0 +) + +require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/charmbracelet/bubbles v1.0.0 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/glamour v1.0.0 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect - github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/gorilla/css v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.13 // indirect - github.com/yuin/goldmark-emoji v1.0.6 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.36.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.30.0 // indirect + modernc.org/libc v1.72.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index fd0c03f..ade400a 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,25 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= -github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= -github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= -github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= -github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= @@ -40,52 +28,79 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= -github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= -github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= -github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= -github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= -github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= +modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= +modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= +modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= +modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM= +modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/config/config.go b/internal/config/config.go index d497a4d..a279188 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,12 +23,16 @@ const ( // Config 保存 Orca 框架的所有配置信息。 type Config struct { - Provider string `toml:"provider"` - Ollama OllamaConfig `toml:"ollama"` - DeepSeek DeepSeekConfig `toml:"deepseek"` - Sandbox SandboxConfig `toml:"sandbox"` - Session SessionConfig `toml:"session"` - Agent AgentConfig `toml:"agent"` + Provider string `toml:"provider"` + Ollama OllamaConfig `toml:"ollama"` + DeepSeek DeepSeekConfig `toml:"deepseek"` + Sandbox SandboxConfig `toml:"sandbox"` + Session SessionConfig `toml:"session"` + Agent AgentConfig `toml:"agent"` + Embedding EmbeddingConfig `toml:"embedding"` + Memory MemoryConfig `toml:"memory"` + MemoryAgent MemoryAgentConfig `toml:"memory_agent"` + SiliconFlow SiliconConfig `toml:"siliconflow"` } // OllamaConfig 保存 Ollama LLM 后端的设置。 @@ -59,6 +63,68 @@ type SessionConfig struct { MaxHistory int `toml:"max_history"` } +type EmbeddingConfig struct { + Provider string `toml:"provider"` + Model string `toml:"model"` + Dim int `toml:"dimensions"` + MaxCtx int `toml:"max_context"` +} + +type MemoryConfig struct { + Enabled bool `toml:"enabled"` + MaxHistory int `toml:"max_history"` + ShortTerm ShortTermConfig `toml:"short_term"` + LongTerm LongTermConfig `toml:"long_term"` + Inject InjectConfig `toml:"inject"` +} + +type ShortTermConfig struct { + MaxItems int `toml:"max_items"` + CompressionThreshold int `toml:"compression_threshold"` +} + +type LongTermConfig struct { + Enabled bool `toml:"enabled"` + VectorIndex bool `toml:"vector_index"` + MaxReturn int `toml:"max_return"` + ArchiveThreshold float64 `toml:"archive_threshold"` +} + +type InjectConfig struct { + FirstRoundEmpty bool `toml:"first_round_empty"` + ShortTermCount int `toml:"short_term_count"` + LongTermTrigger string `toml:"long_term_trigger"` + MinQueryLength int `toml:"min_query_length"` +} + +type MemoryAgentConfig struct { + Enabled bool `toml:"enabled"` + Provider string `toml:"provider"` + Model string `toml:"model"` + APIKey string `toml:"api_key"` + BaseURL string `toml:"base_url"` + Timeout time.Duration `toml:"timeout"` + Extract ExtractConfig `toml:"extract"` + Summarize SummarizeConfig `toml:"summarize"` +} + +type ExtractConfig struct { + BatchSize int `toml:"batch_size"` + MaxFacts int `toml:"max_facts"` + MinConfidence float64 `toml:"min_confidence"` + AutoTag bool `toml:"auto_tag"` +} + +type SummarizeConfig struct { + Enabled bool `toml:"enabled"` + TriggerTokens int `toml:"trigger_tokens"` +} + +type SiliconConfig struct { + APIKey string `toml:"api_key"` + BaseURL string `toml:"base_url"` +} + // AgentConfig 保存 Agent 身份和行为的配置。 type AgentConfig struct { // Role 是 Agent 的角色标识,如 "assistant", "coder", "reviewer" 等。 @@ -105,9 +171,71 @@ func DefaultConfig() *Config { SystemPrompt: "", PromptFile: "", }, + Embedding: EmbeddingConfig{ + Provider: "siliconflow", + Model: "Pro/BAAI/bge-m3", + Dim: 1024, + MaxCtx: 8192, + }, + Memory: MemoryConfig{ + Enabled: true, + MaxHistory: 100, + ShortTerm: ShortTermConfig{ + MaxItems: 10, + CompressionThreshold: 200, + }, + LongTerm: LongTermConfig{ + Enabled: true, + VectorIndex: true, + MaxReturn: 2, + ArchiveThreshold: 0.3, + }, + Inject: InjectConfig{ + FirstRoundEmpty: true, + ShortTermCount: 3, + LongTermTrigger: "technical", + MinQueryLength: 10, + }, + }, + MemoryAgent: MemoryAgentConfig{ + Enabled: true, + Provider: "deepseek", + Model: "deepseek-v4-flash", + Timeout: 60 * time.Second, + Extract: ExtractConfig{ + BatchSize: 5, + MaxFacts: 10, + MinConfidence: 0.6, + AutoTag: true, + }, + Summarize: SummarizeConfig{ + Enabled: true, + TriggerTokens: 4000, + }, + }, + SiliconFlow: SiliconConfig{ + APIKey: "", + BaseURL: "https://api.siliconflow.cn/v1", + }, } } +func (c *Config) expandPaths() { + c.Session.StorageDir = expandHomeDir(c.Session.StorageDir) + c.Sandbox.WorkingDir = expandHomeDir(c.Sandbox.WorkingDir) + c.Agent.PromptFile = expandHomeDir(c.Agent.PromptFile) +} + +func expandHomeDir(path string) string { + if len(path) > 0 && path[0] == '~' { + home, err := os.UserHomeDir() + if err == nil { + return home + path[1:] + } + } + return path +} + // LoadConfigFromFile 从指定路径加载 TOML 配置文件。 // 如果文件不存在,返回默认配置。 func LoadConfigFromFile(path string) (*Config, error) { @@ -121,6 +249,8 @@ func LoadConfigFromFile(path string) (*Config, error) { return nil, fmt.Errorf("config: failed to load %q: %w", path, err) } + cfg.expandPaths() + return cfg, nil } diff --git a/internal/web/html.go b/internal/web/html.go deleted file mode 100644 index 7281490..0000000 --- a/internal/web/html.go +++ /dev/null @@ -1,585 +0,0 @@ -package web - -const indexHTML = ` - - - - - orca.agent - - - - - - -
-

orca.agent

- v0.1.0 -
-
-
-
-
Processing...
-
-
- - -
-
-
-
-

Statistics

-
- Tools: - 0 -
-
- Skills: - 0 -
-
- Agents: - 0 -
-
-
-

Active Agents

-
-
-
-
- - - -` diff --git a/internal/web/server.go b/internal/web/server.go deleted file mode 100644 index fb2d32a..0000000 --- a/internal/web/server.go +++ /dev/null @@ -1,207 +0,0 @@ -package web - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - "sync" - - "github.com/orca/orca/pkg/actor" - "github.com/orca/orca/pkg/kernel" -) - -type Server struct { - kernel *kernel.Kernel - port int - clients map[string]*SSEClient - clientsMu sync.RWMutex - msgCounter int -} - -type SSEClient struct { - ID string - Writer http.ResponseWriter - Flusher http.Flusher - Done chan bool -} - -type sseWriter struct { - client *SSEClient - mu sync.Mutex -} - -func (w *sseWriter) Write(p []byte) (n int, err error) { - w.mu.Lock() - defer w.mu.Unlock() - - select { - case <-w.client.Done: - return len(p), nil - default: - } - - data := string(p) - if strings.TrimSpace(data) == "" { - return len(p), nil - } - - lines := strings.Split(data, "\n") - for _, line := range lines { - fmt.Fprintf(w.client.Writer, "data: %s\n", line) - } - fmt.Fprint(w.client.Writer, "\n") - w.client.Flusher.Flush() - return len(p), nil -} - -func NewServer(k *kernel.Kernel, port int) *Server { - if port <= 0 { - port = 8081 - } - return &Server{ - kernel: k, - port: port, - clients: make(map[string]*SSEClient), - } -} - -func (s *Server) Start() error { - mux := http.NewServeMux() - - mux.HandleFunc("/api/stream", s.handleStream) - mux.HandleFunc("/api/chat", s.handleChat) - mux.HandleFunc("/api/stats", s.handleStats) - mux.HandleFunc("/api/agents", s.handleAgents) - mux.HandleFunc("/", s.handleIndex) - - addr := fmt.Sprintf(":%d", s.port) - return http.ListenAndServe(addr, mux) -} - -func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") - - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Streaming unsupported", http.StatusInternalServerError) - return - } - - s.msgCounter++ - clientID := fmt.Sprintf("client-%d", s.msgCounter) - client := &SSEClient{ - ID: clientID, - Writer: w, - Flusher: flusher, - Done: make(chan bool), - } - - s.clientsMu.Lock() - s.clients[clientID] = client - s.clientsMu.Unlock() - - fmt.Fprintf(w, "event: connected\ndata: %s\n\n", clientID) - flusher.Flush() - - <-r.Context().Done() - - s.clientsMu.Lock() - delete(s.clients, clientID) - s.clientsMu.Unlock() - close(client.Done) -} - -func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req struct { - Message string `json:"message"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - s.clientsMu.RLock() - var client *SSEClient - for _, c := range s.clients { - client = c - break - } - s.clientsMu.RUnlock() - - if client != nil { - writer := &sseWriter{client: client} - s.kernel.SetStreamWriter(writer) - } - - resp, err := s.kernel.SendMessage("user", "llm", req.Message) - if err != nil { - json.NewEncoder(w).Encode(map[string]string{ - "error": err.Error(), - }) - return - } - - json.NewEncoder(w).Encode(map[string]string{ - "response": resp, - }) -} - -func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - stats := map[string]interface{}{ - "tools": 0, - "skills": 0, - "agents": 0, - } - - if tm := s.kernel.ToolManager(); tm != nil { - stats["tools"] = tm.Count() - } - if sm := s.kernel.SkillManager(); sm != nil { - stats["skills"] = len(sm.ListSkills()) - } - if as := s.kernel.ActorSystem(); as != nil { - stats["agents"] = as.AgentCount() - } - - json.NewEncoder(w).Encode(stats) -} - -func (s *Server) handleAgents(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - var agents []map[string]string - if as := s.kernel.ActorSystem(); as != nil { - for _, info := range as.AgentInfos() { - status := "idle" - if info.Status == actor.StatusProcessing { - status = "running" - } - agents = append(agents, map[string]string{ - "id": info.ID, - "status": status, - }) - } - } - - json.NewEncoder(w).Encode(agents) -} - -func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(indexHTML)) -} diff --git a/internal/websocket/server.go b/internal/websocket/server.go new file mode 100644 index 0000000..59b6bde --- /dev/null +++ b/internal/websocket/server.go @@ -0,0 +1,469 @@ +package websocket + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/orca/orca/pkg/actor" + "github.com/orca/orca/pkg/bus" + "github.com/orca/orca/pkg/kernel" +) + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +type Server struct { + kernel *kernel.Kernel + port int + clients map[string]*Client + clientsMu sync.RWMutex + counter int +} + +type Client struct { + ID string + Conn *websocket.Conn + Send chan []byte + Server *Server +} + +type Message struct { + Type string `json:"type"` + Content string `json:"content,omitempty"` + Message string `json:"message,omitempty"` + Text string `json:"text,omitempty"` + Agent string `json:"agent,omitempty"` + Stats Stats `json:"stats,omitempty"` + Agents []AgentInfo `json:"agents,omitempty"` +} + +type Stats struct { + Tools int `json:"tools"` + Skills int `json:"skills"` + Agents int `json:"agents"` +} + +type AgentInfo struct { + ID string `json:"id"` + Status string `json:"status"` +} + +func NewServer(k *kernel.Kernel, port int) *Server { + s := &Server{ + kernel: k, + port: port, + clients: make(map[string]*Client), + } + + if mb := k.Bus(); mb != nil { + mb.Subscribe("agent_events", func(msg bus.Message) { + s.broadcastAgentEvent(msg) + }) + } + + return s +} + +func (s *Server) Start() error { + mux := http.NewServeMux() + + // WebSocket endpoint + mux.HandleFunc("/ws", s.handleWebSocket) + + // API endpoints - must be registered before static files + mux.HandleFunc("/api/stats", s.handleStats) + mux.HandleFunc("/api/agents", s.handleAgents) + mux.HandleFunc("/api/sessions", s.handleSessions) + mux.HandleFunc("/api/sessions/", s.handleSessionMessages) + + // Static files - serve React build + webDir := filepath.Join("web", "dist") + if _, err := os.Stat(webDir); err == nil { + fs := http.FileServer(http.Dir(webDir)) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Skip API paths + if strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/ws" { + w.WriteHeader(http.StatusNotFound) + return + } + path := filepath.Join(webDir, r.URL.Path) + _, err := os.Stat(path) + if os.IsNotExist(err) || r.URL.Path == "/" { + http.ServeFile(w, r, filepath.Join(webDir, "index.html")) + return + } + fs.ServeHTTP(w, r) + }) + } + + addr := fmt.Sprintf(":%d", s.port) + log.Printf("WebSocket server starting on http://localhost%s", addr) + return http.ListenAndServe(addr, mux) +} + +func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("WebSocket upgrade error: %v", err) + return + } + + s.counter++ + client := &Client{ + ID: fmt.Sprintf("client-%d", s.counter), + Conn: conn, + Send: make(chan []byte, 256), + Server: s, + } + + s.clientsMu.Lock() + s.clients[client.ID] = client + s.clientsMu.Unlock() + + // Send initial stats and agents + s.broadcastStats() + s.broadcastAgents() + + go client.writePump() + go client.readPump() +} + +func (c *Client) readPump() { + defer func() { + c.Server.removeClient(c) + c.Conn.Close() + }() + + c.Conn.SetReadLimit(512 * 1024) + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.Conn.SetPongHandler(func(string) error { + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + return nil + }) + + for { + _, message, err := c.Conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("WebSocket error: %v", err) + } + break + } + + var msg Message + if err := json.Unmarshal(message, &msg); err != nil { + continue + } + + if msg.Type == "chat" { + go c.handleChat(msg.Message) + } + } +} + +func (c *Client) handleChat(userMessage string) { + writer := &wsWriter{client: c} + c.Server.kernel.SetStreamWriter(writer) + + resp, err := c.Server.kernel.SendMessage("user", "llm", userMessage) + if err != nil { + c.sendJSON(Message{Type: "error", Content: err.Error()}) + return + } + + c.sendJSON(Message{Type: "complete", Content: resp}) +} + +func (c *Client) writePump() { + ticker := time.NewTicker(54 * time.Second) + defer func() { + ticker.Stop() + c.Conn.Close() + }() + + for { + select { + case message, ok := <-c.Send: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if !ok { + c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + c.Conn.WriteMessage(websocket.TextMessage, message) + + case <-ticker.C: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +func (c *Client) sendJSON(v interface{}) { + data, err := json.Marshal(v) + if err != nil { + return + } + select { + case c.Send <- data: + default: + // Channel full, drop message + } +} + +func (s *Server) removeClient(c *Client) { + s.clientsMu.Lock() + delete(s.clients, c.ID) + s.clientsMu.Unlock() + close(c.Send) +} + +func (s *Server) broadcastJSON(v interface{}) { + data, err := json.Marshal(v) + if err != nil { + return + } + + s.clientsMu.RLock() + defer s.clientsMu.RUnlock() + + for _, client := range s.clients { + select { + case client.Send <- data: + default: + // Channel full + } + } +} + +func (s *Server) broadcastAgentEvent(msg bus.Message) { + content, ok := msg.Content.(map[string]interface{}) + if !ok { + return + } + + eventType, _ := content["event"].(string) + + switch eventType { + case "token": + text, _ := content["text"].(string) + agent, _ := content["agent"].(string) + if text == "" || agent == "" { + return + } + msg := Message{ + Type: "agent_token", + Agent: agent, + Content: text, + } + data, _ := json.Marshal(msg) + s.broadcast(data) + default: + event := Message{ + Type: eventType, + Agent: content["agent"].(string), + Message: "", + } + if task, ok := content["task"].(string); ok { + event.Message = task + } + if result, ok := content["result"].(string); ok && result != "" { + event.Content = result + } + data, err := json.Marshal(event) + if err != nil { + return + } + s.broadcast(data) + } +} + +func (s *Server) broadcast(data []byte) { + s.clientsMu.RLock() + defer s.clientsMu.RUnlock() + + for _, client := range s.clients { + select { + case client.Send <- data: + default: + } + } +} + +func (s *Server) broadcastStats() { + stats := Stats{} + if tm := s.kernel.ToolManager(); tm != nil { + stats.Tools = tm.Count() + } + if sm := s.kernel.SkillManager(); sm != nil { + stats.Skills = len(sm.ListSkills()) + } + if as := s.kernel.ActorSystem(); as != nil { + stats.Agents = as.AgentCount() + } + s.broadcastJSON(Message{Type: "stats", Stats: stats}) +} + +func (s *Server) broadcastAgents() { + var agents []AgentInfo + if as := s.kernel.ActorSystem(); as != nil { + for _, info := range as.AgentInfos() { + status := "idle" + if info.Status == actor.StatusProcessing { + status = "running" + } + agents = append(agents, AgentInfo{ID: info.ID, Status: status}) + } + } + s.broadcastJSON(Message{Type: "agents", Agents: agents}) +} + +func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + stats := Stats{} + if tm := s.kernel.ToolManager(); tm != nil { + stats.Tools = tm.Count() + } + if sm := s.kernel.SkillManager(); sm != nil { + stats.Skills = len(sm.ListSkills()) + } + if as := s.kernel.ActorSystem(); as != nil { + stats.Agents = as.AgentCount() + } + json.NewEncoder(w).Encode(stats) +} + +func (s *Server) handleAgents(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var agents []AgentInfo + if as := s.kernel.ActorSystem(); as != nil { + for _, info := range as.AgentInfos() { + status := "idle" + if info.Status == actor.StatusProcessing { + status = "running" + } + agents = append(agents, AgentInfo{ID: info.ID, Status: status}) + } + } + json.NewEncoder(w).Encode(agents) +} + +type SessionInfo struct { + ID string `json:"id"` + MessageCount int `json:"message_count"` + CreatedAt string `json:"created_at"` +} + +type SessionMessage struct { + Role string `json:"role"` + Content string `json:"content"` + Timestamp string `json:"timestamp"` +} + +func (s *Server) handleSessions(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + sessionMgr := s.kernel.SessionManager() + if sessionMgr == nil { + json.NewEncoder(w).Encode([]SessionInfo{}) + return + } + + sessionIDs, err := sessionMgr.ListSessions() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var result []SessionInfo + for _, id := range sessionIDs { + session, err := sessionMgr.GetSession(id) + if err != nil { + continue + } + result = append(result, SessionInfo{ + ID: session.ID, + MessageCount: len(session.Messages), + CreatedAt: session.CreatedAt.Format(time.RFC3339), + }) + } + + json.NewEncoder(w).Encode(result) +} + +func (s *Server) handleSessionMessages(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + path := strings.TrimPrefix(r.URL.Path, "/api/sessions/") + if path == "" || path == "/api/sessions" { + http.Error(w, "session ID required", http.StatusBadRequest) + return + } + + sessionMgr := s.kernel.SessionManager() + if sessionMgr == nil { + json.NewEncoder(w).Encode([]SessionMessage{}) + return + } + + session, err := sessionMgr.GetSession(path) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + var result []SessionMessage + for _, msg := range session.Messages { + result = append(result, SessionMessage{ + Role: string(msg.Role), + Content: msg.Content, + Timestamp: msg.Timestamp.Format(time.RFC3339), + }) + } + + json.NewEncoder(w).Encode(result) +} + +// wsWriter implements kernel.StreamWriter +type wsWriter struct { + client *Client + mu sync.Mutex + buf strings.Builder +} + +func (w *wsWriter) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + w.buf.Write(p) + text := w.buf.String() + w.buf.Reset() + + msg := Message{Type: "token", Text: text} + data, _ := json.Marshal(msg) + + select { + case w.client.Send <- data: + default: + } + + return len(p), nil +} + +func (w *wsWriter) Flush() error { + return nil +} diff --git a/pkg/actor/llm_agent.go b/pkg/actor/llm_agent.go index 7869e78..a8c359d 100644 --- a/pkg/actor/llm_agent.go +++ b/pkg/actor/llm_agent.go @@ -7,6 +7,7 @@ import ( "io" "strings" "sync" + "time" "github.com/orca/orca/pkg/bus" "github.com/orca/orca/pkg/llm" @@ -23,16 +24,17 @@ import ( // back to the LLM for final response generation. type LLMAgent struct { *BaseAgent - llm llm.LLM - sessionMgr *session.Manager - sessionID string - toolManager *tool.Manager - skillManager *skill.Manager - toolWorker *ToolWorker - windowSize int - streamWriter io.Writer - systemPrompt string - subAgents map[string]string + llm llm.LLM + sessionMgr *session.Manager + sessionID string + toolManager *tool.Manager + skillManager *skill.Manager + toolWorker *ToolWorker + windowSize int + streamWriter io.Writer + systemPrompt string + subAgents map[string]string + memoryManager *session.MemoryManager } // LLMAgentOption is a functional option for configuring the LLMAgent. @@ -98,6 +100,12 @@ func WithSubAgents(agents map[string]string) LLMAgentOption { } } +func WithMemoryManager(mm *session.MemoryManager) LLMAgentOption { + return func(a *LLMAgent) { + a.memoryManager = mm + } +} + // NewLLMAgent creates a new LLMAgent with the given LLM backend and options. // The agent is started automatically upon creation. func NewLLMAgent(id string, backend llm.LLM, opts ...LLMAgentOption) *LLMAgent { @@ -149,6 +157,18 @@ func (a *LLMAgent) handleUserMessage(ctx context.Context, msg bus.Message) (bus. return bus.Message{}, fmt.Errorf("llm_agent: expected string content, got %T", msg.Content) } + // 处理特殊命令 + if content == "/context" || content == "/debug" { + a.LogContextDetails(content) + return bus.Message{ + ID: msg.ID + "-response", + Type: bus.MsgTypeTaskResponse, + From: a.ID(), + To: msg.From, + Content: "[上下文详情已输出到日志]", + }, nil + } + // Ensure session exists if a.sessionMgr != nil && a.sessionID != "" { // Check if session exists; create if not @@ -162,7 +182,15 @@ func (a *LLMAgent) handleUserMessage(ctx context.Context, msg bus.Message) (bus. a.sessionMgr.AddMessage(a.sessionID, session.RoleUser, content, nil) } - llmMessages := a.buildLLMMessages() + if a.memoryManager != nil && a.sessionID != "" { + a.memoryManager.SaveMessage(a.sessionID, session.SessionMessage{ + Role: session.RoleUser, + Content: content, + Timestamp: time.Now(), + }) + } + + llmMessages := a.buildLLMMessages(content) if a.skillManager != nil { matchedSkills := a.skillManager.FindSkill(content) @@ -187,6 +215,15 @@ func (a *LLMAgent) handleUserMessage(ctx context.Context, msg bus.Message) (bus. a.sessionMgr.AddMessage(a.sessionID, session.RoleAssistant, finalResponse, nil) } + if a.memoryManager != nil && a.sessionID != "" { + a.memoryManager.SaveMessage(a.sessionID, session.SessionMessage{ + Role: session.RoleAssistant, + Content: finalResponse, + Timestamp: time.Now(), + }) + go a.memoryManager.MaintainSessionMemory(a.sessionID, content, finalResponse) + } + return bus.Message{ ID: msg.ID + "-response", Type: bus.MsgTypeTaskResponse, @@ -196,8 +233,25 @@ func (a *LLMAgent) handleUserMessage(ctx context.Context, msg bus.Message) (bus. }, nil } -func (a *LLMAgent) buildLLMMessages() []llm.Message { +type contextStats struct { + systemPromptTokens int + toolPromptTokens int + memoryTokens int + historyTokens int + memoryShortTerm int + memoryLongTerm int + historyCount int +} + +func (a *LLMAgent) buildLLMMessages(query string) []llm.Message { + return a.buildLLMMessagesWithStats(query, nil) +} + +func (a *LLMAgent) buildLLMMessagesWithStats(query string, stats *contextStats) []llm.Message { messages := make([]llm.Message, 0) + if stats == nil { + stats = &contextStats{} + } // 1. 用户自定义 system prompt(配置式身份描述) if a.systemPrompt != "" { @@ -205,6 +259,7 @@ func (a *LLMAgent) buildLLMMessages() []llm.Message { Role: "system", Content: a.systemPrompt, }) + stats.systemPromptTokens = estimateTokens(a.systemPrompt) } // 2. 运行时工具说明(动态生成) @@ -214,41 +269,122 @@ func (a *LLMAgent) buildLLMMessages() []llm.Message { Role: "system", Content: toolPrompt, }) + stats.toolPromptTokens = estimateTokens(toolPrompt) } - if a.sessionMgr == nil || a.sessionID == "" { - return messages - } - - sessionMsgs, err := a.sessionMgr.GetContext(a.sessionID, a.windowSize) - if err != nil { - return messages - } - - for _, sm := range sessionMsgs { - msg := llm.Message{ - Role: string(sm.Role), - Content: sm.Content, + if a.memoryManager != nil && a.sessionID != "" { + if a.memoryManager.ShouldInjectMemory(a.sessionID, query) { + memoryCtx, memStats := a.memoryManager.BuildMemoryContextWithStats(a.sessionID, query) + if memoryCtx != "" { + messages = append(messages, llm.Message{ + Role: "system", + Content: memoryCtx, + }) + stats.memoryTokens = memStats.TotalTokens + stats.memoryShortTerm = memStats.ShortTermCount + stats.memoryLongTerm = memStats.LongTermCount + } } - if sm.Role == session.RoleTool && sm.Metadata != nil { - msg.ToolCallID = sm.Metadata["tool_call_id"] + } + + if a.sessionMgr != nil && a.sessionID != "" { + sessionMsgs, err := a.sessionMgr.GetContext(a.sessionID, a.windowSize) + if err == nil { + stats.historyCount = len(sessionMsgs) + for _, sm := range sessionMsgs { + msg := llm.Message{ + Role: string(sm.Role), + Content: sm.Content, + } + if sm.Role == session.RoleTool && sm.Metadata != nil { + msg.ToolCallID = sm.Metadata["tool_call_id"] + } + messages = append(messages, msg) + stats.historyTokens += estimateTokens(sm.Content) + } } - messages = append(messages, msg) } return messages } +func (a *LLMAgent) LogContextDetails(query string) { + fmt.Println("\n========== 上下文详情 ==========") + + if a.memoryManager != nil && a.sessionID != "" { + fmt.Println("\n[记忆内容]") + shortTerm, _ := a.memoryManager.GetShortTermMemory(a.sessionID, query) + if len(shortTerm) > 0 { + fmt.Println(" 短期记忆:") + for i, m := range shortTerm { + fmt.Printf(" [%d] %s\n", i+1, truncateForDisplay(m, 80)) + } + } + longTerm, _ := a.memoryManager.GetLongTermMemory(query) + if len(longTerm) > 0 { + fmt.Println(" 长期记忆:") + for i, m := range longTerm { + fmt.Printf(" [%d] %s\n", i+1, truncateForDisplay(m.Content, 80)) + } + } + if len(shortTerm) == 0 && len(longTerm) == 0 { + fmt.Println(" (无记忆)") + } + + cacheSize, cacheHits, cacheMisses := a.memoryManager.CacheStats() + fmt.Printf("\n[Embedding缓存] 大小=%d, 命中=%d, 未命中=%d, 命中率=%.1f%%\n", + cacheSize, cacheHits, cacheMisses, + float64(cacheHits)*100/float64(cacheHits+cacheMisses+1)) + } + + if a.sessionMgr != nil && a.sessionID != "" { + fmt.Println("\n[历史对话]") + sessionMsgs, err := a.sessionMgr.GetContext(a.sessionID, a.windowSize) + if err == nil && len(sessionMsgs) > 0 { + start := 0 + if len(sessionMsgs) > 10 { + start = len(sessionMsgs) - 10 + fmt.Printf(" (显示最近 10/%d 条)\n", len(sessionMsgs)) + } + for i := start; i < len(sessionMsgs); i++ { + sm := sessionMsgs[i] + role := string(sm.Role) + if role == "" { + role = "unknown" + } + fmt.Printf(" [%s] %s\n", role, truncateForDisplay(sm.Content, 80)) + } + } else { + fmt.Println(" (无历史)") + } + } + + fmt.Println("================================\n") +} + +func truncateForDisplay(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} + +func estimateTokens(text string) int { + return len([]rune(text)) / 4 +} + // buildToolPrompt 生成工具说明提示词(不包含身份描述)。 // 将可用工具和调用规则注入给 LLM,支持基于提示词的工具调用。 func (a *LLMAgent) buildToolPrompt() string { var b strings.Builder if a.toolManager != nil { + tools := a.toolManager.List() + b.WriteString("你可以使用以下工具来完成用户的请求。\n\n") b.WriteString("可用工具列表:\n") - for _, t := range a.toolManager.List() { + for _, t := range tools { b.WriteString(fmt.Sprintf("\n工具名: %s\n", t.Name())) b.WriteString(fmt.Sprintf("描述: %s\n", t.Description())) paramsJSON, _ := json.Marshal(t.Parameters()) @@ -261,7 +397,8 @@ func (a *LLMAgent) buildToolPrompt() string { b.WriteString("2. 如果需要同时调用多个工具(并行执行),请输出 JSON 数组格式:\n") b.WriteString(` [{"tool": "工具名1", "arguments": {...}}, {"tool": "工具名2", "arguments": {...}}]` + "\n") b.WriteString("3. 如果你已经看到了工具返回的结果,请直接根据结果回答用户,不要再次调用工具。\n") - b.WriteString("4. 如果你不需要调用工具,请直接回复用户。\n") + b.WriteString("4. 当你不需要调用工具时,请直接回复用户。\n") + b.WriteString("5. 当用户的请求涉及代码、架构、数学计算等专业领域时,你必须调用相应的子Agent,不要自己直接回答。\n") } if len(a.subAgents) > 0 { @@ -272,7 +409,8 @@ func (a *LLMAgent) buildToolPrompt() string { b.WriteString("\n调用方式:使用 agent_call 工具,指定 agent 名称和任务描述。\n") b.WriteString("示例:{\"tool\": \"agent_call\", \"arguments\": {\"agent\": \"coder\", \"task\": \"写个快速排序\"}}\n") b.WriteString("如果用户有多个独立任务,请同时调用多个 agent_call(JSON数组格式),让它们并行执行。\n") - b.WriteString("\n重要:当用户的请求涉及上述专业领域时,你必须调用相应的子Agent,不要自己直接回答。\n") + b.WriteString("\n【强制规则】当用户的请求涉及代码编程、系统架构、数学计算、代码审查等专业领域时,你必须调用相应的子Agent。\n") + b.WriteString("你绝对不能自己直接回答编程或架构问题,必须通过 agent_call 工具委托给专业Agent处理。\n") } if a.skillManager != nil { @@ -305,6 +443,47 @@ func (a *LLMAgent) buildToolPrompt() string { return b.String() } +func (a *LLMAgent) buildToolDefs() []llm.ToolDef { + var tools []llm.ToolDef + + if a.toolManager != nil { + for _, t := range a.toolManager.List() { + params := t.Parameters() + properties := make(map[string]llm.ToolProperty) + required := []string{} + + for name, param := range params { + prop := llm.ToolProperty{ + Type: param.Type, + Description: param.Description, + } + if len(param.Enum) > 0 { + prop.Enum = param.Enum + } + properties[name] = prop + if param.Required { + required = append(required, name) + } + } + + tools = append(tools, llm.ToolDef{ + Type: "function", + Function: llm.ToolFunction{ + Name: t.Name(), + Description: t.Description(), + Parameters: llm.ToolFunctionParameters{ + Type: "object", + Required: required, + Properties: properties, + }, + }, + }) + } + } + + return tools +} + func (a *LLMAgent) chatWithToolLoop(ctx context.Context, messages []llm.Message) (string, error) { maxRounds := 10 @@ -332,6 +511,7 @@ func (a *LLMAgent) chatWithToolLoop(ctx context.Context, messages []llm.Message) }) results := a.executeToolCallsParallel(ctx, toolCalls) + for _, result := range results { messages = append(messages, llm.Message{ Role: "user", @@ -452,20 +632,35 @@ func (a *LLMAgent) parseToolCallsFromContent(content string) []llm.ToolCall { } func (a *LLMAgent) extractJSONFromMarkdown(content string) string { - start := strings.Index(content, "```") + start := strings.Index(content, "`"+"``json") if start == -1 { - return content + start = strings.Index(content, "`"+"``") + if start == -1 { + return content + } + } else { + start += 7 } - start = strings.Index(content[start:], "\n") - if start == -1 { - return content + if !strings.HasPrefix(content[start:], "`"+"``") { + newline := strings.Index(content[start:], "\n") + if newline != -1 { + start = start + newline + 1 + } + } else { + start += 3 + newline := strings.Index(content[start:], "\n") + if newline != -1 { + start = start + newline + 1 + } } - start++ - end := strings.LastIndex(content[start:], "```") + end := strings.Index(content[start:], "\n`"+"``") if end == -1 { - return content + end = strings.Index(content[start:], "`"+"``") + } + if end == -1 { + return strings.TrimSpace(content[start:]) } return strings.TrimSpace(content[start : start+end]) diff --git a/pkg/actor/memory_extractor.go b/pkg/actor/memory_extractor.go new file mode 100644 index 0000000..e526d3c --- /dev/null +++ b/pkg/actor/memory_extractor.go @@ -0,0 +1,165 @@ +package actor + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/orca/orca/pkg/bus" + "github.com/orca/orca/pkg/llm" +) + +type MemoryExtractorAgent struct { + *SubAgent + config ExtractConfig +} + +type ExtractConfig struct { + BatchSize int + MaxFacts int + MinConfidence float64 + AutoTag bool +} + +type Dialogue struct { + UserQuery string + AssistantResponse string +} + +type Fact struct { + Content string `json:"content"` + Type string `json:"type"` + Tags []string `json:"tags"` + Confidence float64 `json:"confidence"` + Replace *string `json:"replace"` +} + +type ExtractResult struct { + Facts []Fact `json:"facts"` +} + +const DefaultMemoryExtractorPrompt = `# Memory Extractor Agent + +你是一个专门从对话中提取用户信息的 Agent。你的工作是将非结构化的对话转化为结构化的长期记忆。 + +## 任务 + +分析给定的对话记录,提取以下类型的信息: + +1. **事实 (fact)**:客观信息 + - 工作:公司、职位、技术栈、行业 + - 技术:擅长语言、框架偏好、架构经验 + - 个人:教育背景、所在城市(仅用户明确提及) + +2. **偏好 (preference)**:主观倾向 + - 回答风格:简洁/详细/代码示例/架构图 + - 技术偏好:语言、数据库、部署方式 + - 沟通偏好:正式/ casual + +3. **项目 (project)**:当前工作 + - 项目名称、技术方案、当前阶段、遇到的挑战 + +## 输出格式 + +只输出 JSON,不要任何解释: + +` + "```json" + ` +{ + "facts": [ + { + "content": "用户在电商公司担任后端工程师", + "type": "fact", + "tags": ["工作", "后端", "电商"], + "confidence": 0.95, + "replace": null + }, + { + "content": "用户偏好简洁的技术回答,不要过多解释", + "type": "preference", + "tags": ["沟通风格", "偏好"], + "confidence": 0.85, + "replace": "用户喜欢详细的回答" + } + ] +} +` + "```" + ` + +## 规则 + +- confidence < 0.6 的事实不输出 +- 如果新事实与旧事实冲突: + - 在 replace 字段填入被替换的旧事实 content + - 只替换同一 type + 同一 tags 的事实 +- 不猜测、不推断,只提取用户明确表达的信息 +- 标签从预设列表选择:工作、技术、偏好、项目、沟通风格、行业` + +func loadAgentPrompt(agentName string) string { + path := filepath.Join(os.Getenv("HOME"), ".orca", "agents", "_builtin", agentName+".md") + data, err := os.ReadFile(path) + if err != nil { + return DefaultMemoryExtractorPrompt + } + return string(data) +} + +func NewMemoryExtractorAgent(id string, llmBackend llm.LLM, cfg ExtractConfig) *MemoryExtractorAgent { + prompt := loadAgentPrompt("memory_extractor") + + sa := NewSubAgent(id, llmBackend, + WithSubAgentRole("memory_extractor"), + WithSubAgentSystemPrompt(prompt), + ) + + return &MemoryExtractorAgent{ + SubAgent: sa, + config: cfg, + } +} + +func (mea *MemoryExtractorAgent) ExtractFacts(dialogues []Dialogue) ([]Fact, error) { + var sb strings.Builder + sb.WriteString("请分析以下对话记录,提取用户的关键信息:\n\n") + for i, d := range dialogues { + sb.WriteString(fmt.Sprintf("--- 对话 %d ---\n", i+1)) + sb.WriteString(fmt.Sprintf("用户:%s\n", d.UserQuery)) + sb.WriteString(fmt.Sprintf("助手:%s\n\n", d.AssistantResponse)) + } + + msg := bus.Message{Type: bus.MsgTypeTaskRequest, Content: sb.String()} + resp, err := mea.Process(context.Background(), msg) + if err != nil { + return nil, fmt.Errorf("extract facts failed: %w", err) + } + + return parseFactJSON(resp.Content.(string)) +} + +func parseFactJSON(content string) ([]Fact, error) { + content = extractJSONFromMarkdown(content) + + var result ExtractResult + if err := json.Unmarshal([]byte(content), &result); err != nil { + return nil, fmt.Errorf("parse fact json failed: %w", err) + } + + return result.Facts, nil +} + +func extractJSONFromMarkdown(content string) string { + if idx := strings.Index(content, "```json"); idx != -1 { + start := idx + 7 + if end := strings.Index(content[start:], "```"); end != -1 { + return strings.TrimSpace(content[start : start+end]) + } + } + if idx := strings.Index(content, "```"); idx != -1 { + start := idx + 3 + if end := strings.Index(content[start:], "```"); end != -1 { + return strings.TrimSpace(content[start : start+end]) + } + } + return strings.TrimSpace(content) +} \ No newline at end of file diff --git a/pkg/actor/subagent.go b/pkg/actor/subagent.go index dbb5e15..94b83c0 100644 --- a/pkg/actor/subagent.go +++ b/pkg/actor/subagent.go @@ -5,17 +5,27 @@ import ( "fmt" "io" "strings" + "time" + "github.com/google/uuid" "github.com/orca/orca/pkg/bus" "github.com/orca/orca/pkg/llm" + "github.com/orca/orca/pkg/session" ) +type SubAgentStore interface { + SaveSubAgentMessage(parentSessionID, sessionID, agentName string, msg session.SessionMessage) error + LoadSubAgentMessages(sessionID string) ([]session.SessionMessage, error) +} + type SubAgent struct { *BaseAgent - llmBackend llm.LLM - systemPrompt string - role string - streamWriter io.Writer + llmBackend llm.LLM + systemPrompt string + role string + streamWriter io.Writer + store SubAgentStore + parentSessionID string } type SubAgentOption func(*SubAgent) @@ -38,6 +48,18 @@ func WithSubAgentStreamWriter(w io.Writer) SubAgentOption { } } +func WithSubAgentStore(store SubAgentStore) SubAgentOption { + return func(a *SubAgent) { + a.store = store + } +} + +func WithSubAgentParentSessionID(parentSessionID string) SubAgentOption { + return func(a *SubAgent) { + a.parentSessionID = parentSessionID + } +} + func NewSubAgent(id string, llmBackend llm.LLM, opts ...SubAgentOption) *SubAgent { sa := &SubAgent{ BaseAgent: NewBaseAgent(id, "subagent"), @@ -71,6 +93,10 @@ func (sa *SubAgent) SetStreamWriter(w io.Writer) { sa.streamWriter = w } +func (sa *SubAgent) SetParentSessionID(parentSessionID string) { + sa.parentSessionID = parentSessionID +} + func (sa *SubAgent) handleMessage(ctx context.Context, msg bus.Message) (bus.Message, error) { switch msg.Type { case bus.MsgTypeTaskRequest: @@ -83,6 +109,25 @@ func (sa *SubAgent) handleMessage(ctx context.Context, msg bus.Message) (bus.Mes } func (sa *SubAgent) handleTask(ctx context.Context, msg bus.Message) (bus.Message, error) { + sessionID := uuid.New().String() + parentSessionID := sa.parentSessionID + if parentSessionID == "" { + parentSessionID = "unknown" + } + + if sa.store != nil { + sa.store.SaveSubAgentMessage(parentSessionID, sessionID, sa.ID(), session.SessionMessage{ + Role: session.RoleSystem, + Content: sa.systemPrompt, + Timestamp: time.Now(), + }) + sa.store.SaveSubAgentMessage(parentSessionID, sessionID, sa.ID(), session.SessionMessage{ + Role: session.RoleUser, + Content: fmt.Sprintf("%v", msg.Content), + Timestamp: time.Now(), + }) + } + messages := []llm.Message{ { Role: "system", @@ -99,6 +144,14 @@ func (sa *SubAgent) handleTask(ctx context.Context, msg bus.Message) (bus.Messag return bus.Message{}, fmt.Errorf("subagent %s: LLM call failed: %w", sa.ID(), err) } + if sa.store != nil { + sa.store.SaveSubAgentMessage(parentSessionID, sessionID, sa.ID(), session.SessionMessage{ + Role: session.RoleAssistant, + Content: content, + Timestamp: time.Now(), + }) + } + return bus.Message{ ID: msg.ID + "-response", Type: bus.MsgTypeTaskResponse, @@ -106,8 +159,10 @@ func (sa *SubAgent) handleTask(ctx context.Context, msg bus.Message) (bus.Messag To: msg.From, Content: content, Metadata: map[string]string{ - "processed_by": sa.ID(), - "agent_role": sa.role, + "processed_by": sa.ID(), + "agent_role": sa.role, + "session_id": sessionID, + "parent_session_id": parentSessionID, }, }, nil } diff --git a/pkg/embedding/client.go b/pkg/embedding/client.go new file mode 100644 index 0000000..0eb35ff --- /dev/null +++ b/pkg/embedding/client.go @@ -0,0 +1,124 @@ +package embedding + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type Client struct { + apiKey string + baseURL string + model string + client *http.Client +} + +type Config struct { + APIKey string + BaseURL string + Model string + Timeout time.Duration +} + +func NewClient(cfg Config) *Client { + if cfg.BaseURL == "" { + cfg.BaseURL = "https://api.siliconflow.cn/v1" + } + if cfg.Model == "" { + cfg.Model = "Pro/BAAI/bge-m3" + } + if cfg.Timeout == 0 { + cfg.Timeout = 30 * time.Second + } + + return &Client{ + apiKey: cfg.APIKey, + baseURL: cfg.BaseURL, + model: cfg.Model, + client: &http.Client{Timeout: cfg.Timeout}, + } +} + +type embedRequest struct { + Model string `json:"model"` + Input []string `json:"input"` +} + +type embedResponse struct { + Data []struct { + Embedding []float32 `json:"embedding"` + Index int `json:"index"` + } `json:"data"` + Error *struct { + Message string `json:"message"` + } `json:"error,omitempty"` +} + +func (c *Client) Embed(texts []string) ([][]float32, error) { + if len(texts) == 0 { + return nil, fmt.Errorf("no texts to embed") + } + + reqBody, err := json.Marshal(embedRequest{ + Model: c.model, + Input: texts, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", c.baseURL+"/embeddings", bytes.NewReader(reqBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.apiKey) + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("embedding API returned %d: %s", resp.StatusCode, string(body)) + } + + var embedResp embedResponse + if err := json.Unmarshal(body, &embedResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + if embedResp.Error != nil { + return nil, fmt.Errorf("embedding API error: %s", embedResp.Error.Message) + } + + results := make([][]float32, len(texts)) + for _, d := range embedResp.Data { + if d.Index < len(results) { + results[d.Index] = d.Embedding + } + } + + return results, nil +} + +func (c *Client) EmbedSingle(text string) ([]float32, error) { + results, err := c.Embed([]string{text}) + if err != nil { + return nil, err + } + if len(results) == 0 { + return nil, fmt.Errorf("no embedding returned") + } + return results[0], nil +} diff --git a/pkg/kernel/kernel.go b/pkg/kernel/kernel.go index 8393a4f..20fc84e 100644 --- a/pkg/kernel/kernel.go +++ b/pkg/kernel/kernel.go @@ -13,10 +13,12 @@ import ( "path/filepath" "strings" "sync" + "time" "github.com/orca/orca/internal/config" "github.com/orca/orca/pkg/actor" "github.com/orca/orca/pkg/bus" + "github.com/orca/orca/pkg/embedding" "github.com/orca/orca/pkg/llm" "github.com/orca/orca/pkg/plugin" "github.com/orca/orca/pkg/session" @@ -24,16 +26,6 @@ import ( "github.com/orca/orca/pkg/tool" ) -// Kernel 是 Orca 框架的微内核核心。 -// -// 它编排插件生命周期、消息路由和组件间通信。 -// 内核初始化并管理以下组件: -// - 消息总线,用于组件间通信 -// - 插件注册表,支持扩展 -// - 会话管理器,用于对话持久化 -// - 工具管理器,包含内置工具 -// - 技能管理器,用于基于技能的自动化 -// - Actor 系统,包含编排器、工作者和 LLM 代理 type Kernel struct { mu sync.RWMutex mb bus.MessageBus @@ -41,16 +33,17 @@ type Kernel struct { plugins []plugin.Plugin started bool - // Integration components - config *config.Config - sessionMgr *session.Manager - toolMgr *tool.Manager - skillMgr *skill.Manager - actorSystem *actor.System - orch *actor.Orchestrator - llmAgent *actor.LLMAgent - toolWorker *actor.ToolWorker - subAgents map[string]actor.Agent + config *config.Config + sessionMgr *session.Manager + sessionStore *session.SQLiteStore + memoryManager *session.MemoryManager + toolMgr *tool.Manager + skillMgr *skill.Manager + actorSystem *actor.System + orch *actor.Orchestrator + llmAgent *actor.LLMAgent + toolWorker *actor.ToolWorker + subAgents map[string]actor.Agent } // New 从配置文件创建一个新的 Kernel 实例。 @@ -78,12 +71,34 @@ func NewWithConfig(cfg *config.Config) *Kernel { subAgents: make(map[string]actor.Agent), } - // Initialize session manager - store, err := session.NewJSONLStore(cfg.Session.StorageDir) + storageDir := expandHomeDir(cfg.Session.StorageDir) + dbPath := filepath.Join(storageDir, "orcasession.db") + store, err := session.NewSQLiteStore(dbPath) if err != nil { log.Printf("kernel: warning: failed to create session store: %v", err) } else { + k.sessionStore = store k.sessionMgr = session.NewManager(store, k.mb) + log.Printf("kernel: session manager initialized with SQLite storage") + } + + if cfg.SiliconFlow.APIKey != "" && store != nil { + memoryCfg := session.MemoryConfig{ + DBPath: dbPath, + ModelWindow: cfg.Embedding.MaxCtx, + EmbedConfig: embedding.Config{ + APIKey: cfg.SiliconFlow.APIKey, + BaseURL: cfg.SiliconFlow.BaseURL, + Model: cfg.Embedding.Model, + Timeout: 5 * time.Second, + }, + } + k.memoryManager, err = session.NewMemoryManagerWithStore(memoryCfg, store) + if err != nil { + log.Printf("kernel: warning: failed to create memory manager: %v", err) + } else { + log.Printf("kernel: memory manager initialized with %s embedding (shared storage)", cfg.Embedding.Model) + } } // Initialize tool manager with all built-in tools @@ -137,6 +152,9 @@ func (k *Kernel) initializeActorSystem() { k.createSubAgents(ollama) agentCallTool := tool.NewAgentCallTool(k.findAgent) + if acTool, ok := agentCallTool.(interface{ SetEventBus(bus.MessageBus) }); ok { + acTool.SetEventBus(k.mb) + } if err := k.toolMgr.Register(agentCallTool); err != nil { log.Printf("kernel: warning: failed to register agent_call tool: %v", err) } @@ -148,7 +166,10 @@ func (k *Kernel) initializeActorSystem() { actor.WithWindowSize(k.config.Session.MaxHistory), } - if prompt := k.config.GetSystemPrompt(); prompt != "" { + builtinPromptPath := expandHomeDir("~/.orca/agents/_builtin/assistant.md") + if data, err := os.ReadFile(builtinPromptPath); err == nil && len(data) > 0 { + llmOpts = append(llmOpts, actor.WithSystemPrompt(string(data))) + } else if prompt := k.config.GetSystemPrompt(); prompt != "" { llmOpts = append(llmOpts, actor.WithSystemPrompt(prompt)) } @@ -170,6 +191,10 @@ func (k *Kernel) initializeActorSystem() { ) } + if k.memoryManager != nil { + llmOpts = append(llmOpts, actor.WithMemoryManager(k.memoryManager)) + } + if len(k.subAgents) > 0 { agentDescs := make(map[string]string) for name, agent := range k.subAgents { @@ -183,16 +208,20 @@ func (k *Kernel) initializeActorSystem() { llmAgent := actor.NewLLMAgent(llmAgentID, ollama, llmOpts...) k.llmAgent = llmAgent + if k.memoryManager != nil { + k.memoryManager.SetLLM(ollama) + } + k.orch.AddWorker(llmAgent) k.orch.AddWorker(tw) k.orch.SetDefaultWorker(llmAgent) } func (k *Kernel) createSubAgents(llmBackend llm.LLM) { - promptDir := expandHomeDir("~/.orca/prompts") - entries, err := os.ReadDir(promptDir) + agentsDir := expandHomeDir("~/.orca/agents") + entries, err := os.ReadDir(agentsDir) if err != nil { - log.Printf("kernel: warning: cannot read prompts dir %s: %v", promptDir, err) + log.Printf("kernel: warning: cannot read agents dir %s: %v", agentsDir, err) return } @@ -204,31 +233,30 @@ func (k *Kernel) createSubAgents(llmBackend llm.LLM) { if !strings.HasSuffix(name, ".md") { continue } - if name == "assistant.md" { - continue - } agentName := strings.TrimSuffix(name, ".md") - promptPath := filepath.Join(promptDir, name) - content, err := os.ReadFile(promptPath) + agentPath := filepath.Join(agentsDir, name) + content, err := os.ReadFile(agentPath) if err != nil { - log.Printf("kernel: warning: failed to read prompt %s: %v", promptPath, err) + log.Printf("kernel: warning: failed to read agent prompt %s: %v", agentPath, err) continue } prompt := string(content) if strings.TrimSpace(prompt) == "" { - log.Printf("kernel: warning: empty prompt file %s, skipping", promptPath) + log.Printf("kernel: warning: empty agent prompt file %s, skipping", agentPath) continue } agent := actor.NewSubAgent(agentName, llmBackend, actor.WithSubAgentRole(agentName), actor.WithSubAgentSystemPrompt(prompt), + actor.WithSubAgentStore(k.sessionStore), + actor.WithSubAgentParentSessionID("default"), ) k.subAgents[agentName] = agent k.orch.AddWorker(agent) - log.Printf("kernel: created sub-agent %q from %s", agentName, promptPath) + log.Printf("kernel: created sub-agent %q from %s", agentName, agentPath) } log.Printf("kernel: created %d sub-agents", len(k.subAgents)) @@ -330,6 +358,11 @@ func (k *Kernel) LLMAgent() *actor.LLMAgent { return k.llmAgent } +// MemoryManager 返回记忆管理器。 +func (k *Kernel) MemoryManager() *session.MemoryManager { + return k.memoryManager +} + // SetStreamWriter 设置用于流式 LLM 输出的写入器。 func (k *Kernel) SetStreamWriter(w io.Writer) { if k.llmAgent != nil { diff --git a/pkg/llm/deepseek.go b/pkg/llm/deepseek.go index dc39857..1cb53a3 100644 --- a/pkg/llm/deepseek.go +++ b/pkg/llm/deepseek.go @@ -160,6 +160,52 @@ func (c *DeepSeekClient) Stream(ctx context.Context, messages []Message, handler return nil } +func (c *DeepSeekClient) ChatWithTools(ctx context.Context, messages []Message, tools []ToolDef) (*Response, error) { + reqBody := deepSeekChatRequest{ + Model: c.model, + Messages: messages, + Stream: false, + Tools: tools, + } + body, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("deepseek: failed to marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/chat/completions", bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("deepseek: failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.apiKey) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("deepseek: request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("deepseek: API returned %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var apiResp deepSeekChatResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + return nil, fmt.Errorf("deepseek: failed to decode response: %w", err) + } + + if len(apiResp.Choices) == 0 { + return nil, fmt.Errorf("deepseek: no choices in response") + } + + choice := apiResp.Choices[0] + return &Response{ + Content: choice.Message.Content, + ToolCalls: choice.Message.ToolCalls, + }, nil +} + func (c *DeepSeekClient) buildChatRequest(messages []Message, stream bool) deepSeekChatRequest { return deepSeekChatRequest{ Model: c.model, @@ -172,6 +218,7 @@ type deepSeekChatRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` Stream bool `json:"stream"` + Tools []ToolDef `json:"tools,omitempty"` } type deepSeekChatResponse struct { diff --git a/pkg/llm/llm.go b/pkg/llm/llm.go index f49963d..65fb818 100644 --- a/pkg/llm/llm.go +++ b/pkg/llm/llm.go @@ -17,6 +17,10 @@ type LLM interface { // If the model decides to call tools, the response contains ToolCalls. Chat(ctx context.Context, messages []Message) (*Response, error) + // ChatWithTools sends messages with available tools and returns a response. + // The model may return ToolCalls that the caller should execute and feed back. + ChatWithTools(ctx context.Context, messages []Message, tools []ToolDef) (*Response, error) + // Stream sends messages and streams the response token-by-token. // The handler is called for each chunk. The final response is not // collected; use Chat for complete responses. diff --git a/pkg/llm/ollama.go b/pkg/llm/ollama.go index cf0aa41..5efa0c9 100644 --- a/pkg/llm/ollama.go +++ b/pkg/llm/ollama.go @@ -79,6 +79,10 @@ func NewOllamaClient(opts ...OllamaOption) *OllamaClient { // Chat sends a chat request to Ollama and returns the complete response. // If the Ollama model returns tool calls, they are parsed and included // in the Response. +func (c *OllamaClient) ChatWithTools(ctx context.Context, messages []Message, tools []ToolDef) (*Response, error) { + return c.Chat(ctx, messages) +} + func (c *OllamaClient) Chat(ctx context.Context, messages []Message) (*Response, error) { req := OllamaChatRequest{ Model: c.model, diff --git a/pkg/llm/types.go b/pkg/llm/types.go index 223b970..df0d64a 100644 --- a/pkg/llm/types.go +++ b/pkg/llm/types.go @@ -11,9 +11,10 @@ package llm // or "tool". For tool results, ToolCallID links the result back to the // tool call that produced it. type Message struct { - Role string `json:"role"` - Content string `json:"content"` - ToolCallID string `json:"tool_call_id,omitempty"` + Role string `json:"role"` + Content string `json:"content"` + ToolCallID string `json:"tool_call_id,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` } // ToolCall represents a function calling request from the LLM. diff --git a/pkg/session/memory_manager.go b/pkg/session/memory_manager.go new file mode 100644 index 0000000..52cb010 --- /dev/null +++ b/pkg/session/memory_manager.go @@ -0,0 +1,870 @@ +package session + +import ( + "context" + "encoding/json" + "fmt" + "log" + "strings" + "sync" + "time" + + "github.com/orca/orca/pkg/embedding" + "github.com/orca/orca/pkg/llm" +) + +type MemoryManager struct { + store *SQLiteStore + vectorStore *VectorStore + embedClient *embedding.Client + llmBackend llm.LLM + tokenBudget TokenBudget + ownsStore bool + + embedQueue chan embedTask + embedWg sync.WaitGroup + embedCtx context.Context + embedCancel context.CancelFunc + + embedCache map[string]*embedCacheEntry + embedCacheMu sync.RWMutex + embedCacheMax int + embedCacheHit int64 + embedCacheMiss int64 +} + +type embedCacheEntry struct { + embedding []float32 + lastUsed time.Time +} + +type embedTask struct { + msgID int64 + content string +} + +type TokenBudget struct { + Total int + Working int + ShortTerm int + LongTerm int +} + +type MemoryConfig struct { + DBPath string + EmbedConfig embedding.Config + ModelWindow int +} + +func NewMemoryManager(cfg MemoryConfig) (*MemoryManager, error) { + store, err := NewSQLiteStore(cfg.DBPath) + if err != nil { + return nil, fmt.Errorf("failed to create store: %w", err) + } + + vectorStore, err := NewVectorStore(store.DB()) + if err != nil { + store.Close() + return nil, fmt.Errorf("failed to create vector store: %w", err) + } + + budget := calculateBudget(cfg.ModelWindow) + + ctx, cancel := context.WithCancel(context.Background()) + mm := &MemoryManager{ + store: store, + vectorStore: vectorStore, + tokenBudget: budget, + ownsStore: true, + embedQueue: make(chan embedTask, 100), + embedCtx: ctx, + embedCancel: cancel, + embedCache: make(map[string]*embedCacheEntry), + embedCacheMax: 500, + } + + if cfg.EmbedConfig.APIKey != "" { + mm.embedClient = embedding.NewClient(cfg.EmbedConfig) + mm.startEmbedWorker() + } + + return mm, nil +} + +func NewMemoryManagerWithStore(cfg MemoryConfig, store *SQLiteStore) (*MemoryManager, error) { + vectorStore, err := NewVectorStore(store.DB()) + if err != nil { + return nil, fmt.Errorf("failed to create vector store: %w", err) + } + + budget := calculateBudget(cfg.ModelWindow) + + ctx, cancel := context.WithCancel(context.Background()) + mm := &MemoryManager{ + store: store, + vectorStore: vectorStore, + tokenBudget: budget, + ownsStore: false, + embedQueue: make(chan embedTask, 100), + embedCtx: ctx, + embedCancel: cancel, + embedCache: make(map[string]*embedCacheEntry), + embedCacheMax: 500, + } + + if cfg.EmbedConfig.APIKey != "" { + mm.embedClient = embedding.NewClient(cfg.EmbedConfig) + mm.startEmbedWorker() + } + + return mm, nil +} + +func calculateBudget(modelWindow int) TokenBudget { + if modelWindow <= 0 { + modelWindow = 8192 + } + total := int(float64(modelWindow) * 0.6) + return TokenBudget{ + Total: total, + Working: int(float64(total) * 0.5), + ShortTerm: int(float64(total) * 0.3), + LongTerm: int(float64(total) * 0.2), + } +} + +func (mm *MemoryManager) Close() error { + mm.embedCancel() + mm.embedWg.Wait() + if mm.ownsStore { + return mm.store.Close() + } + return nil +} + +func (mm *MemoryManager) SaveMessage(sessionID string, msg SessionMessage) error { + if err := mm.store.Save(sessionID, msg); err != nil { + return err + } + + if mm.embedClient != nil && len(msg.Content) > 10 && + (msg.Role == RoleUser || msg.Role == RoleAssistant) { + msgID, err := mm.getLastMessageID(sessionID) + if err == nil { + select { + case mm.embedQueue <- embedTask{msgID: msgID, content: msg.Content}: + default: + } + } + } + + return nil +} + +func (mm *MemoryManager) getLastMessageID(sessionID string) (int64, error) { + var id int64 + err := mm.store.DB().QueryRow( + "SELECT id FROM main_messages WHERE session_id = ? ORDER BY id DESC LIMIT 1", + sessionID, + ).Scan(&id) + return id, err +} + +func (mm *MemoryManager) startEmbedWorker() { + mm.embedWg.Add(1) + go func() { + defer mm.embedWg.Done() + batch := make([]embedTask, 0, 5) + timer := time.NewTimer(5 * time.Second) + defer timer.Stop() + + for { + select { + case task := <- mm.embedQueue: + batch = append(batch, task) + if len(batch) >= 5 { + mm.processBatch(batch) + batch = batch[:0] + timer.Reset(5 * time.Second) + } + case <- timer.C: + if len(batch) > 0 { + mm.processBatch(batch) + batch = batch[:0] + } + timer.Reset(5 * time.Second) + case <- mm.embedCtx.Done(): + if len(batch) > 0 { + mm.processBatch(batch) + } + return + } + } + }() +} + +func (mm *MemoryManager) processBatch(tasks []embedTask) { + texts := make([]string, len(tasks)) + for i, t := range tasks { + texts[i] = t.content + } + + embeddings, err := mm.embedClient.Embed(texts) + if err != nil { + log.Printf("[memory] Embedding batch failed: %v", err) + return + } + + for i, emb := range embeddings { + if err := mm.vectorStore.SaveEmbedding(tasks[i].msgID, emb); err != nil { + log.Printf("[memory] Save embedding failed: %v", err) + } + } +} + +func (mm *MemoryManager) GetWorkingMemory(sessionID string) ([]SessionMessage, error) { + rows, err := mm.store.DB().Query( + `SELECT role, content, timestamp, metadata FROM main_messages + WHERE session_id = ? + ORDER BY timestamp DESC`, + sessionID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var messages []SessionMessage + totalTokens := 0 + for rows.Next() { + var msg SessionMessage + var timestampStr string + var metadataStr string + if err := rows.Scan(&msg.Role, &msg.Content, ×tampStr, &metadataStr); err != nil { + continue + } + msg.Timestamp, _ = time.Parse(time.RFC3339, timestampStr) + + tokens := estimateTokens(msg.Content) + if totalTokens+tokens > mm.tokenBudget.Working && len(messages) > 0 { + break + } + totalTokens += tokens + messages = append(messages, msg) + } + + reverseMessages(messages) + return messages, nil +} + +func (mm *MemoryManager) getEmbeddingWithCache(query string) ([]float32, error) { + if mm.embedClient == nil { + return nil, fmt.Errorf("no embed client") + } + + mm.embedCacheMu.RLock() + if entry, ok := mm.embedCache[query]; ok { + entry.lastUsed = time.Now() + mm.embedCacheHit++ + mm.embedCacheMu.RUnlock() + return entry.embedding, nil + } + mm.embedCacheMu.RUnlock() + + embedding, err := mm.embedClient.EmbedSingle(query) + if err != nil { + return nil, err + } + + mm.embedCacheMu.Lock() + mm.embedCache[query] = &embedCacheEntry{ + embedding: embedding, + lastUsed: time.Now(), + } + mm.embedCacheMiss++ + + if len(mm.embedCache) > mm.embedCacheMax { + mm.evictLRU() + } + mm.embedCacheMu.Unlock() + + return embedding, nil +} + +func (mm *MemoryManager) evictLRU() { + var oldestKey string + var oldestTime time.Time + first := true + for k, v := range mm.embedCache { + if first || v.lastUsed.Before(oldestTime) { + oldestKey = k + oldestTime = v.lastUsed + first = false + } + } + if oldestKey != "" { + delete(mm.embedCache, oldestKey) + } +} + +func (mm *MemoryManager) GetShortTermMemory(sessionID string, query string) ([]string, error) { + if query != "" && mm.embedClient != nil { + embedding, err := mm.getEmbeddingWithCache(query) + if err == nil { + msgIDs, err := mm.vectorStore.SearchSimilarInSession(sessionID, embedding, 3) + if err == nil && len(msgIDs) > 0 { + return mm.loadMemoryContents(msgIDs) + } + } + } + + rows, err := mm.store.DB().Query( + `SELECT content FROM short_term_memories + WHERE session_id = ? + ORDER BY updated_at DESC + LIMIT 3`, + sessionID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var memories []string + for rows.Next() { + var content string + if err := rows.Scan(&content); err != nil { + continue + } + memories = append(memories, content) + } + return memories, rows.Err() +} + +func (mm *MemoryManager) GetLongTermMemory(query string) ([]struct { + ID int64 + Content string + Weight float64 +}, error) { + var results []struct { + ID int64 + Content string + Weight float64 + } + + if query != "" && mm.embedClient != nil { + embedding, err := mm.getEmbeddingWithCache(query) + if err == nil { + vecResults, err := mm.vectorStore.SearchLongTermSimilar(embedding, 5) + if err == nil && len(vecResults) > 0 { + for _, vr := range vecResults { + var content string + var weight float64 + err := mm.store.DB().QueryRow( + "SELECT content, weight FROM long_term_memories WHERE id = ? AND archived = 0", + vr.MemoryID, + ).Scan(&content, &weight) + if err == nil { + results = append(results, struct { + ID int64 + Content string + Weight float64 + }{ID: vr.MemoryID, Content: content, Weight: weight}) + } + } + } + } + } + + if len(results) == 0 { + rows, err := mm.store.DB().Query( + `SELECT id, content, weight FROM long_term_memories + WHERE archived = 0 + ORDER BY weight DESC, access_count DESC + LIMIT 2`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var r struct { + ID int64 + Content string + Weight float64 + } + if err := rows.Scan(&r.ID, &r.Content, &r.Weight); err != nil { + continue + } + results = append(results, r) + } + } + + now := time.Now() + for _, r := range results { + mm.store.DB().Exec( + "UPDATE long_term_memories SET access_count = access_count + 1, last_accessed = ? WHERE id = ?", + now, r.ID, + ) + } + + return results, nil +} + +func (mm *MemoryManager) loadMemoryContents(msgIDs []int64) ([]string, error) { + if len(msgIDs) == 0 { + return nil, nil + } + + placeholders := make([]string, len(msgIDs)) + args := make([]interface{}, len(msgIDs)) + for i, id := range msgIDs { + placeholders[i] = "?" + args[i] = id + } + + query := fmt.Sprintf( + "SELECT content FROM main_messages WHERE id IN (%s)", + strings.Join(placeholders, ","), + ) + rows, err := mm.store.DB().Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var contents []string + for rows.Next() { + var content string + if err := rows.Scan(&content); err != nil { + continue + } + contents = append(contents, content) + } + return contents, rows.Err() +} + +func (mm *MemoryManager) AddShortTermMemory(sessionID string, content string) error { + _, err := mm.store.DB().Exec( + `INSERT INTO short_term_memories (session_id, content, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(session_id, content) DO UPDATE SET + source_count = source_count + 1, + updated_at = ?`, + sessionID, content, time.Now(), time.Now(), + ) + return err +} + +func (mm *MemoryManager) AddLongTermMemory(content string, memoryType string) error { + _, err := mm.store.DB().Exec( + `INSERT INTO long_term_memories (content, memory_type, confidence) + VALUES (?, ?, ?) + ON CONFLICT(content) DO UPDATE SET + access_count = access_count + 1, + confidence = MAX(confidence, ?)`, + content, memoryType, 0.8, 0.8, + ) + return err +} + +func (mm *MemoryManager) Cleanup() error { + _, err := mm.store.DB().Exec( + `DELETE FROM short_term_memories + WHERE id NOT IN ( + SELECT id FROM short_term_memories + ORDER BY updated_at DESC + LIMIT 10 + )`, + ) + return err +} + +func estimateTokens(text string) int { + return len([]rune(text)) / 2 +} + +func reverseMessages(msgs []SessionMessage) { + for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 { + msgs[i], msgs[j] = msgs[j], msgs[i] + } +} + +func toInterfaceSlice(strings []string) []interface{} { + result := make([]interface{}, len(strings)) + for i, s := range strings { + result[i] = s + } + return result +} + +type MemoryContextStats struct { + ShortTermCount int + LongTermCount int + TotalTokens int +} + +func (mm *MemoryManager) BuildMemoryContextWithStats(sessionID string, query string) (string, MemoryContextStats) { + var parts []string + stats := MemoryContextStats{} + + shortTerm, err := mm.GetShortTermMemory(sessionID, query) + if err == nil && len(shortTerm) > 0 { + parts = append(parts, "## 相关上下文\n"+strings.Join(shortTerm, "\n")) + stats.ShortTermCount = len(shortTerm) + for _, m := range shortTerm { + stats.TotalTokens += estimateTokens(m) + } + } + + longTerm, err := mm.GetLongTermMemory(query) + if err == nil && len(longTerm) > 0 { + var contents []string + for _, m := range longTerm { + contents = append(contents, m.Content) + mm.RecordMemoryUsage(m.ID, sessionID, query, true) + } + parts = append(parts, "## 背景知识\n"+strings.Join(contents, "\n")) + stats.LongTermCount = len(longTerm) + for _, m := range longTerm { + stats.TotalTokens += estimateTokens(m.Content) + } + } + + if len(parts) == 0 { + return "", stats + } + + return "## 记忆信息\n" + strings.Join(parts, "\n\n"), stats +} + +func (mm *MemoryManager) BuildMemoryContext(sessionID string, query string) string { + ctx, _ := mm.BuildMemoryContextWithStats(sessionID, query) + return ctx +} + +func (mm *MemoryManager) ShouldInjectMemory(sessionID string, query string) bool { + if sessionID == "" { + return false + } + + msgCount, err := mm.getSessionMessageCount(sessionID) + if err != nil || msgCount == 0 { + return false + } + + if len(query) < 10 { + return false + } + + return true +} + +func (mm *MemoryManager) getSessionMessageCount(sessionID string) (int, error) { + var count int + err := mm.store.DB().QueryRow( + "SELECT COUNT(*) FROM main_messages WHERE session_id = ?", + sessionID, + ).Scan(&count) + return count, err +} + +func (mm *MemoryManager) MaintainSessionMemory(sessionID string, userQuery string, assistantResponse string) { + if len(assistantResponse) < 20 { + return + } + + summary := fmt.Sprintf("用户问:%s\n回答:%s", userQuery, truncateString(assistantResponse, 100)) + if err := mm.AddShortTermMemory(sessionID, summary); err != nil { + log.Printf("[memory] Failed to add short-term memory: %v", err) + } + + mm.bufferDialogue(sessionID, userQuery, assistantResponse) + + if mm.shouldExtract(sessionID) { + mm.triggerExtraction(sessionID) + } +} + +func (mm *MemoryManager) shouldExtract(sessionID string) bool { + var count int + err := mm.store.DB().QueryRow( + "SELECT COUNT(*) FROM dialogue_buffer WHERE session_id = ?", + sessionID, + ).Scan(&count) + if err != nil { + return false + } + return count >= 5 +} + +func (mm *MemoryManager) SetLLM(backend llm.LLM) { + mm.llmBackend = backend +} + +func (mm *MemoryManager) triggerExtraction(sessionID string) { + dialogues, err := mm.FlushDialogueBuffer(sessionID) + if err != nil || len(dialogues) == 0 { + return + } + + if mm.llmBackend == nil { + log.Printf("[memory] No LLM backend configured, skipping extraction for session=%s", sessionID) + return + } + + go func() { + facts, err := mm.extractFacts(dialogues) + if err != nil { + log.Printf("[memory] Extraction failed: %v", err) + return + } + + for _, fact := range facts { + if fact.Confidence < 0.6 { + continue + } + if err := mm.AddLongTermMemory(fact.Content, fact.Type); err != nil { + log.Printf("[memory] Failed to save long-term memory: %v", err) + } + } + }() +} + +func (mm *MemoryManager) bufferDialogue(sessionID string, userQuery string, assistantResponse string) { + _, err := mm.store.DB().Exec( + "INSERT INTO dialogue_buffer (session_id, user_query, assistant_response) VALUES (?, ?, ?)", + sessionID, userQuery, assistantResponse, + ) + if err != nil { + log.Printf("[memory] Failed to buffer dialogue: %v", err) + } +} + +func (mm *MemoryManager) FlushDialogueBuffer(sessionID string) ([]struct { + UserQuery string + AssistantResponse string +}, error) { + rows, err := mm.store.DB().Query( + "SELECT user_query, assistant_response FROM dialogue_buffer WHERE session_id = ? ORDER BY created_at ASC", + sessionID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var dialogues []struct { + UserQuery string + AssistantResponse string + } + for rows.Next() { + var d struct { + UserQuery string + AssistantResponse string + } + if err := rows.Scan(&d.UserQuery, &d.AssistantResponse); err != nil { + continue + } + dialogues = append(dialogues, d) + } + + if len(dialogues) > 0 { + _, err = mm.store.DB().Exec("DELETE FROM dialogue_buffer WHERE session_id = ?", sessionID) + if err != nil { + log.Printf("[memory] Failed to clear dialogue buffer: %v", err) + } + } + + return dialogues, rows.Err() +} + +func truncateString(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} + +func (mm *MemoryManager) RecordMemoryUsage(memoryID int64, sessionID, query string, referenced bool) error { + _, err := mm.store.DB().Exec( + "INSERT INTO memory_usage_log (memory_id, session_id, query, was_referenced) VALUES (?, ?, ?, ?)", + memoryID, sessionID, query, referenced, + ) + if err != nil { + return err + } + + delta := 0.5 + if !referenced { + delta = -0.3 + } + + _, err = mm.store.DB().Exec( + "UPDATE long_term_memories SET weight = weight + ?, access_count = access_count + 1, last_accessed = ? WHERE id = ?", + delta, time.Now(), memoryID, + ) + return err +} + +func (mm *MemoryManager) ArchiveLowWeightMemories(threshold float64) (int, error) { + result, err := mm.store.DB().Exec( + "UPDATE long_term_memories SET archived = 1 WHERE weight < ? AND archived = 0", + threshold, + ) + if err != nil { + return 0, err + } + count, _ := result.RowsAffected() + return int(count), nil +} + +func (mm *MemoryManager) GetCoreMemories(minWeight float64) ([]struct { + ID int64 + Content string + Weight float64 +}, error) { + rows, err := mm.store.DB().Query( + "SELECT id, content, weight FROM long_term_memories WHERE weight >= ? AND archived = 0 ORDER BY weight DESC", + minWeight, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var memories []struct { + ID int64 + Content string + Weight float64 + } + for rows.Next() { + var m struct { + ID int64 + Content string + Weight float64 + } + if err := rows.Scan(&m.ID, &m.Content, &m.Weight); err != nil { + continue + } + memories = append(memories, m) + } + return memories, rows.Err() +} + +func (mm *MemoryManager) CacheStats() (size int, hits, misses int64) { + mm.embedCacheMu.RLock() + defer mm.embedCacheMu.RUnlock() + return len(mm.embedCache), mm.embedCacheHit, mm.embedCacheMiss +} + +const memoryExtractionPrompt = `# Memory Extractor + +你是一个专门从对话中提取用户信息的助手。将非结构化的对话转化为结构化的长期记忆。 + +## 任务 + +分析给定的对话记录,提取以下类型的信息: + +1. **事实 (fact)**:客观信息 + - 工作:公司、职位、技术栈、行业 + - 技术:擅长语言、框架偏好、架构经验 + - 个人:教育背景、所在城市(仅用户明确提及) + +2. **偏好 (preference)**:主观倾向 + - 回答风格:简洁/详细/代码示例/架构图 + - 技术偏好:语言、数据库、部署方式 + - 沟通偏好:正式/ casual + +3. **项目 (project)**:当前工作 + - 项目名称、技术方案、当前阶段、遇到的挑战 + +## 输出格式 + +只输出 JSON,不要任何解释: + +` + "```json" + ` +{ + "facts": [ + { + "content": "用户在电商公司担任后端工程师", + "type": "fact", + "confidence": 0.95 + }, + { + "content": "用户偏好简洁的技术回答", + "type": "preference", + "confidence": 0.85 + } + ] +} +` + "```" + ` + +## 规则 + +- confidence < 0.6 的事实不输出 +- 不猜测、不推断,只提取用户明确表达的信息` + +type extractedFact struct { + Content string `json:"content"` + Type string `json:"type"` + Confidence float64 `json:"confidence"` +} + +type extractionResult struct { + Facts []extractedFact `json:"facts"` +} + +func (mm *MemoryManager) extractFacts(dialogues []struct { + UserQuery string + AssistantResponse string +}) ([]extractedFact, error) { + var sb strings.Builder + sb.WriteString(memoryExtractionPrompt) + sb.WriteString("\n\n## 对话记录\n\n") + for i, d := range dialogues { + sb.WriteString(fmt.Sprintf("--- 对话 %d ---\n", i+1)) + sb.WriteString(fmt.Sprintf("用户:%s\n", d.UserQuery)) + sb.WriteString(fmt.Sprintf("助手:%s\n\n", d.AssistantResponse)) + } + + messages := []llm.Message{ + {Role: "system", Content: "你是一个专门从对话中提取用户信息的助手。"}, + {Role: "user", Content: sb.String()}, + } + + resp, err := mm.llmBackend.Chat(context.Background(), messages) + if err != nil { + return nil, fmt.Errorf("llm chat failed: %w", err) + } + + return parseExtractionJSON(resp.Content) +} + +func parseExtractionJSON(content string) ([]extractedFact, error) { + content = extractJSONBlock(content) + + var result extractionResult + if err := json.Unmarshal([]byte(content), &result); err != nil { + return nil, fmt.Errorf("parse extraction json failed: %w", err) + } + + return result.Facts, nil +} + +func extractJSONBlock(content string) string { + if idx := strings.Index(content, "```json"); idx != -1 { + start := idx + 7 + if end := strings.Index(content[start:], "```"); end != -1 { + return strings.TrimSpace(content[start : start+end]) + } + } + if idx := strings.Index(content, "```"); idx != -1 { + start := idx + 3 + if end := strings.Index(content[start:], "```"); end != -1 { + return strings.TrimSpace(content[start : start+end]) + } + } + return strings.TrimSpace(content) +} diff --git a/pkg/session/memory_test.go b/pkg/session/memory_test.go new file mode 100644 index 0000000..6f2c97e --- /dev/null +++ b/pkg/session/memory_test.go @@ -0,0 +1,577 @@ +package session + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/orca/orca/pkg/embedding" +) + +func setupMemoryManager(t *testing.T) (*MemoryManager, func()) { + t.Helper() + dir, err := os.MkdirTemp("", "orca-memory-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + + cfg := MemoryConfig{ + DBPath: filepath.Join(dir, "memory.db"), + ModelWindow: 8192, + EmbedConfig: embedding.Config{ + APIKey: os.Getenv("SILICONFLOW_API_KEY"), + BaseURL: "https://api.siliconflow.cn/v1", + Model: "Pro/BAAI/bge-m3", + Timeout: 5000, + }, + } + + mm, err := NewMemoryManager(cfg) + if err != nil { + os.RemoveAll(dir) + t.Fatalf("NewMemoryManager failed: %v", err) + } + + cleanup := func() { + mm.Close() + os.RemoveAll(dir) + } + return mm, cleanup +} + + +func TestSQLiteStore_SaveAndLoad(t *testing.T) { + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + sessionID := "test-session-1" + msg := SessionMessage{ + Role: RoleUser, + Content: "你好,请介绍一下自己", + Timestamp: time.Now(), + } + + err := mm.SaveMessage(sessionID, msg) + if err != nil { + t.Fatalf("SaveMessage failed: %v", err) + } + + messages, err := mm.GetWorkingMemory(sessionID) + if err != nil { + t.Fatalf("GetWorkingMemory failed: %v", err) + } + if len(messages) != 1 { + t.Fatalf("expected 1 message, got %d", len(messages)) + } + if messages[0].Content != msg.Content { + t.Errorf("expected content %q, got %q", msg.Content, messages[0].Content) + } +} + +func TestSQLiteStore_MultipleMessages(t *testing.T) { + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + sessionID := "test-session-multi" + baseTime := time.Now() + messages := []SessionMessage{ + {Role: RoleUser, Content: "什么是机器学习?", Timestamp: baseTime}, + {Role: RoleAssistant, Content: "机器学习是人工智能的一个分支...", Timestamp: baseTime.Add(time.Second)}, + {Role: RoleUser, Content: "能举个例子吗?", Timestamp: baseTime.Add(2 * time.Second)}, + {Role: RoleAssistant, Content: "比如垃圾邮件过滤器...", Timestamp: baseTime.Add(3 * time.Second)}, + } + + for _, msg := range messages { + if err := mm.SaveMessage(sessionID, msg); err != nil { + t.Fatalf("SaveMessage failed: %v", err) + } + } + + loaded, err := mm.GetWorkingMemory(sessionID) + if err != nil { + t.Fatalf("GetWorkingMemory failed: %v", err) + } + if len(loaded) != len(messages) { + t.Fatalf("expected %d messages, got %d", len(messages), len(loaded)) + } + + for i, msg := range loaded { + if msg.Content != messages[i].Content { + t.Errorf("message %d: expected %q, got %q", i, messages[i].Content, msg.Content) + } + } +} + + +func TestCalculateBudget(t *testing.T) { + tests := []struct { + modelWindow int + wantTotal int + wantWorking int + }{ + } + + for _, tt := range tests { + budget := calculateBudget(tt.modelWindow) + if budget.Total != tt.wantTotal { + t.Errorf("modelWindow=%d: expected total %d, got %d", + tt.modelWindow, tt.wantTotal, budget.Total) + } + if budget.Working != tt.wantWorking { + t.Errorf("modelWindow=%d: expected working %d, got %d", + tt.modelWindow, tt.wantWorking, budget.Working) + } + if budget.ShortTerm != int(float64(budget.Total)*0.3) { + t.Errorf("ShortTerm budget incorrect: %d", budget.ShortTerm) + } + if budget.LongTerm != int(float64(budget.Total)*0.2) { + t.Errorf("LongTerm budget incorrect: %d", budget.LongTerm) + } + } +} + +func TestGetWorkingMemory_TokenBudget(t *testing.T) { + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + sessionID := "test-budget" + + longContent := "这是一个很长的消息。" + + "重复多次以消耗token预算。" + + "重复多次以消耗token预算。" + + "重复多次以消耗token预算。" + + "重复多次以消耗token预算。" + + "重复多次以消耗token预算。" + + "重复多次以消耗token预算。" + + "重复多次以消耗token预算。" + + "重复多次以消耗token预算。" + + "重复多次以消耗token预算。" + + messages := []SessionMessage{ + {Role: RoleUser, Content: "第一条消息", Timestamp: time.Now()}, + {Role: RoleAssistant, Content: longContent, Timestamp: time.Now()}, + {Role: RoleUser, Content: "第三条消息", Timestamp: time.Now()}, + } + + for _, msg := range messages { + if err := mm.SaveMessage(sessionID, msg); err != nil { + t.Fatalf("SaveMessage failed: %v", err) + } + } + + // Get working memory with budget + loaded, err := mm.GetWorkingMemory(sessionID) + if err != nil { + t.Fatalf("GetWorkingMemory failed: %v", err) + } + + // Should return messages within budget + // The long message should cause earlier messages to be excluded + t.Logf("Loaded %d messages within budget", len(loaded)) + for i, msg := range loaded { + t.Logf("Message %d: role=%s, len=%d", i, msg.Role, len(msg.Content)) + } +} + + +func TestVectorStore_SaveAndSearch(t *testing.T) { + apiKey := os.Getenv("SILICONFLOW_API_KEY") + if apiKey == "" { + t.Skip("Skipping vector test: SILICONFLOW_API_KEY not set") + } + + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + sessionID := "test-vectors" + + messages := []SessionMessage{ + {Role: RoleUser, Content: "Python 是什么编程语言?", Timestamp: time.Now()}, + {Role: RoleAssistant, Content: "Python 是一种高级编程语言,以其简洁的语法而闻名", Timestamp: time.Now()}, + {Role: RoleUser, Content: "Go 语言的特点是什么?", Timestamp: time.Now()}, + {Role: RoleAssistant, Content: "Go 语言由 Google 开发,强调并发和性能", Timestamp: time.Now()}, + {Role: RoleUser, Content: "今天天气怎么样?", Timestamp: time.Now()}, + } + + for _, msg := range messages { + if err := mm.SaveMessage(sessionID, msg); err != nil { + t.Fatalf("SaveMessage failed: %v", err) + } + } + + time.Sleep(2 * time.Second) + + results, err := mm.GetShortTermMemory(sessionID, "编程语言") + if err != nil { + t.Fatalf("GetShortTermMemory with vector search failed: %v", err) + } + + t.Logf("Vector search returned %d results", len(results)) + for i, r := range results { + t.Logf("Result %d: %s", i, r) + } +} + +func TestVectorStore_CrossSessionSearch(t *testing.T) { + apiKey := os.Getenv("SILICONFLOW_API_KEY") + if apiKey == "" { + t.Skip("Skipping vector test: SILICONFLOW_API_KEY not set") + } + + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + session1 := "session-ai" + for _, msg := range []SessionMessage{ + {Role: RoleUser, Content: "什么是深度学习?", Timestamp: time.Now()}, + {Role: RoleAssistant, Content: "深度学习是机器学习的一个子集,使用神经网络", Timestamp: time.Now()}, + } { + if err := mm.SaveMessage(session1, msg); err != nil { + t.Fatalf("SaveMessage failed: %v", err) + } + } + + session2 := "session-cooking" + for _, msg := range []SessionMessage{ + {Role: RoleUser, Content: "如何做红烧肉?", Timestamp: time.Now()}, + {Role: RoleAssistant, Content: "红烧肉需要五花肉、酱油、糖等材料", Timestamp: time.Now()}, + } { + if err := mm.SaveMessage(session2, msg); err != nil { + t.Fatalf("SaveMessage failed: %v", err) + } + } + + time.Sleep(2 * time.Second) + + results, err := mm.GetLongTermMemory("神经网络") + if err != nil { + t.Fatalf("GetLongTermMemory failed: %v", err) + } + + t.Logf("Cross-session search returned %d results", len(results)) + for i, r := range results { + t.Logf("Result %d: %s (weight=%.2f)", i, r.Content, r.Weight) + } +} + + +func TestMaintainSessionMemory(t *testing.T) { + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + sessionID := "test-maintenance" + userQuery := "什么是REST API?" + assistantResponse := "REST API(Representational State Transfer)是一种软件架构风格,用于设计网络应用程序。它使用HTTP方法(GET、POST、PUT、DELETE)来操作资源。" + + mm.MaintainSessionMemory(sessionID, userQuery, assistantResponse) + + memories, err := mm.GetShortTermMemory(sessionID, "") + if err != nil { + t.Fatalf("GetShortTermMemory failed: %v", err) + } + if len(memories) == 0 { + t.Fatal("Expected short-term memory to be created") + } + + t.Logf("Created %d short-term memories", len(memories)) + for i, m := range memories { + t.Logf("Memory %d: %s", i, m) + } +} + +func TestAddLongTermMemory(t *testing.T) { + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + memories := []struct { + content string + mType string + }{ + {"用户喜欢使用Python进行数据分析", "preference"}, + {"项目使用Go语言开发", "project"}, + {"用户偏好简洁的代码风格", "preference"}, + } + + for _, m := range memories { + if err := mm.AddLongTermMemory(m.content, m.mType); err != nil { + t.Fatalf("AddLongTermMemory failed: %v", err) + } + } + + results, err := mm.GetLongTermMemory("") + if err != nil { + t.Fatalf("GetLongTermMemory failed: %v", err) + } + if len(results) == 0 { + t.Fatal("Expected long-term memories to be retrieved") + } + + t.Logf("Retrieved %d long-term memories", len(results)) + for i, r := range results { + t.Logf("Memory %d: %s (weight=%.2f)", i, r.Content, r.Weight) + } +} + +func TestBuildMemoryContext(t *testing.T) { + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + sessionID := "test-context" + + mm.AddShortTermMemory(sessionID, "用户正在学习Go语言") + mm.AddShortTermMemory(sessionID, "用户之前问过关于goroutine的问题") + + mm.AddLongTermMemory("用户是后端开发工程师", "fact") + mm.AddLongTermMemory("用户偏好技术文档", "preference") + + context := mm.BuildMemoryContext(sessionID, "并发编程") + if context == "" { + t.Fatal("Expected memory context to be built") + } + + t.Logf("Memory context:\n%s", context) +} + + +func TestMemoryManager_WithoutAPIKey(t *testing.T) { + dir, err := os.MkdirTemp("", "orca-memory-test-nokey-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(dir) + + cfg := MemoryConfig{ + DBPath: filepath.Join(dir, "memory.db"), + ModelWindow: 8192, + EmbedConfig: embedding.Config{ + }, + } + + mm, err := NewMemoryManager(cfg) + if err != nil { + t.Fatalf("NewMemoryManager should work without API key: %v", err) + } + defer mm.Close() + + sessionID := "test-nokey" + msg := SessionMessage{ + Role: RoleUser, + Content: "测试无API Key模式", + Timestamp: time.Now(), + } + + if err := mm.SaveMessage(sessionID, msg); err != nil { + t.Fatalf("SaveMessage should work without API key: %v", err) + } + + messages, err := mm.GetWorkingMemory(sessionID) + if err != nil { + t.Fatalf("GetWorkingMemory should work without API key: %v", err) + } + if len(messages) != 1 { + t.Fatalf("expected 1 message, got %d", len(messages)) + } + + memories, err := mm.GetShortTermMemory(sessionID, "") + if err != nil { + t.Fatalf("GetShortTermMemory should fallback to SQL: %v", err) + } + t.Logf("Short-term memories (no API key): %d", len(memories)) +} + + +func TestCleanup(t *testing.T) { + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + sessionID := "test-cleanup" + + for i := 0; i < 15; i++ { + content := fmt.Sprintf("Short-term memory %d", i) + if err := mm.AddShortTermMemory(sessionID, content); err != nil { + t.Fatalf("AddShortTermMemory failed: %v", err) + } + } + + if err := mm.Cleanup(); err != nil { + t.Fatalf("Cleanup failed: %v", err) + } + + memories, err := mm.GetShortTermMemory(sessionID, "") + if err != nil { + t.Fatalf("GetShortTermMemory failed: %v", err) + } + if len(memories) > 10 { + t.Errorf("Expected at most 10 memories after cleanup, got %d", len(memories)) + } +} + + +func TestEstimateTokens(t *testing.T) { + tests := []struct { + input string + expected int + }{ + {"", 0}, + } + + for _, tt := range tests { + got := estimateTokens(tt.input) + if got != tt.expected { + t.Errorf("estimateTokens(%q) = %d, want %d", tt.input, got, tt.expected) + } + } +} + +func TestReverseMessages(t *testing.T) { + msgs := []SessionMessage{ + {Content: "first"}, + {Content: "second"}, + {Content: "third"}, + } + + reverseMessages(msgs) + + expected := []string{"third", "second", "first"} + for i, msg := range msgs { + if msg.Content != expected[i] { + t.Errorf("reverseMessages: position %d expected %q, got %q", + i, expected[i], msg.Content) + } + } +} + +func TestTruncateString(t *testing.T) { + tests := []struct { + input string + maxLen int + expected string + }{ + {"hello", 10, "hello"}, + {"hello world", 5, "hello..."}, + {"", 5, ""}, + {"short", 5, "short"}, + } + + for _, tt := range tests { + got := truncateString(tt.input, tt.maxLen) + if got != tt.expected { + t.Errorf("truncateString(%q, %d) = %q, want %q", + tt.input, tt.maxLen, got, tt.expected) + } + } +} + + +func TestFullConversationFlow(t *testing.T) { + mm, cleanup := setupMemoryManager(t) + defer cleanup() + + sessionID := "test-conversation" + + conversation := []struct { + role MessageRole + content string + }{ + {RoleUser, "你好,我想学习Go语言"}, + {RoleAssistant, "Go语言是一种由Google开发的开源编程语言,以其简洁、高效和强大的并发支持而闻名。它特别适合构建网络服务和分布式系统。"}, + {RoleUser, "Go语言的并发是怎么实现的?"}, + {RoleAssistant, "Go语言使用goroutine和channel实现并发。Goroutine是轻量级线程,由Go运行时管理。Channel用于goroutine之间的通信和同步。"}, + {RoleUser, "能推荐一些学习资源吗?"}, + {RoleAssistant, "推荐以下学习资源:1. Go官方文档 2. 《Go程序设计语言》 3. Go by Example 网站"}, + } + + for _, msg := range conversation { + sessionMsg := SessionMessage{ + Role: msg.role, + Content: msg.content, + Timestamp: time.Now(), + } + if err := mm.SaveMessage(sessionID, sessionMsg); err != nil { + t.Fatalf("SaveMessage failed: %v", err) + } + } + + workingMem, err := mm.GetWorkingMemory(sessionID) + if err != nil { + t.Fatalf("GetWorkingMemory failed: %v", err) + } + t.Logf("Working memory: %d messages", len(workingMem)) + + for i := 1; i < len(conversation); i += 2 { + mm.MaintainSessionMemory(sessionID, conversation[i-1].content, conversation[i].content) + } + + shortTerm, err := mm.GetShortTermMemory(sessionID, "") + if err != nil { + t.Fatalf("GetShortTermMemory failed: %v", err) + } + t.Logf("Short-term memories: %d", len(shortTerm)) + for i, mem := range shortTerm { + t.Logf(" Memory %d: %s", i, mem) + } + + context := mm.BuildMemoryContext(sessionID, "学习资源") + if context != "" { + t.Logf("Memory context for '学习资源':\n%s", context) + } +} + + +func BenchmarkSaveMessage(b *testing.B) { + mm, cleanup := setupMemoryManager(&testing.T{}) + defer cleanup() + + sessionID := "bench-session" + msg := SessionMessage{ + Role: RoleUser, + Content: "这是一条测试消息", + Timestamp: time.Now(), + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := mm.SaveMessage(sessionID, msg); err != nil { + b.Fatalf("SaveMessage failed: %v", err) + } + } +} + +func BenchmarkGetWorkingMemory(b *testing.B) { + mm, cleanup := setupMemoryManager(&testing.T{}) + defer cleanup() + + sessionID := "bench-session" + for i := 0; i < 100; i++ { + msg := SessionMessage{ + Role: RoleUser, + Content: fmt.Sprintf("Message %d", i), + Timestamp: time.Now(), + } + mm.SaveMessage(sessionID, msg) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := mm.GetWorkingMemory(sessionID); err != nil { + b.Fatalf("GetWorkingMemory failed: %v", err) + } + } +} + +func BenchmarkBuildMemoryContext(b *testing.B) { + mm, cleanup := setupMemoryManager(&testing.T{}) + defer cleanup() + + sessionID := "bench-context" + for i := 0; i < 10; i++ { + mm.AddShortTermMemory(sessionID, fmt.Sprintf("Memory %d", i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = mm.BuildMemoryContext(sessionID, "test query") + } +} diff --git a/pkg/session/sqlite_store.go b/pkg/session/sqlite_store.go new file mode 100644 index 0000000..ecb684b --- /dev/null +++ b/pkg/session/sqlite_store.go @@ -0,0 +1,368 @@ +package session + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + _ "modernc.org/sqlite" + _ "modernc.org/sqlite/vec" +) + +type SQLiteStore struct { + dbPath string + db *sql.DB + mu sync.RWMutex +} + +func NewSQLiteStore(dbPath string) (*SQLiteStore, error) { + dir := filepath.Dir(dbPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, fmt.Errorf("failed to create storage directory: %w", err) + } + + db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000") + if err != nil { + return nil, fmt.Errorf("failed to open SQLite database: %w", err) + } + + // 限制连接池为单连接,避免 SQLite 并发冲突 + // WAL 模式下支持并发读,但写操作仍需串行化 + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + db.SetConnMaxLifetime(0) + + store := &SQLiteStore{ + dbPath: dbPath, + db: db, + } + + if err := store.initSchema(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to initialize schema: %w", err) + } + + return store, nil +} + +func (s *SQLiteStore) initSchema() error { + schema := ` +CREATE TABLE IF NOT EXISTS main_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')), + content TEXT NOT NULL, + msg_type TEXT DEFAULT 'normal' CHECK(msg_type IN ('normal', 'fact', 'todo', 'decision', 'preference', 'error')), + token_count INTEGER DEFAULT 0, + has_embedding BOOLEAN DEFAULT FALSE, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + metadata TEXT +); + +CREATE INDEX IF NOT EXISTS idx_main_session_time ON main_messages(session_id, timestamp DESC); +CREATE INDEX IF NOT EXISTS idx_main_role ON main_messages(role) WHERE role IN ('user', 'assistant'); + +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + status TEXT DEFAULT 'active' CHECK(status IN ('active', 'archived')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + metadata TEXT +); + +CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status); + +CREATE TABLE IF NOT EXISTS short_term_memories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + content TEXT NOT NULL, + source_count INTEGER DEFAULT 1, + confidence REAL DEFAULT 0.8 CHECK(confidence BETWEEN 0 AND 1), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME, + UNIQUE(session_id, content) +); + +CREATE INDEX IF NOT EXISTS idx_stm_session ON short_term_memories(session_id, updated_at DESC); + +CREATE TABLE IF NOT EXISTS long_term_memories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL UNIQUE, + memory_type TEXT NOT NULL DEFAULT 'fact' CHECK(memory_type IN ('preference', 'fact', 'decision', 'project')), + source_session TEXT, + confidence REAL NOT NULL DEFAULT 0.8, + weight REAL NOT NULL DEFAULT 1.0, + tags TEXT, + access_count INTEGER NOT NULL DEFAULT 0, + last_accessed DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + archived INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_ltm_type ON long_term_memories(memory_type, confidence DESC); +CREATE INDEX IF NOT EXISTS idx_ltm_weight ON long_term_memories(weight DESC); +CREATE INDEX IF NOT EXISTS idx_ltm_archived ON long_term_memories(archived); + +CREATE VIRTUAL TABLE IF NOT EXISTS vec_long_term_memories USING vec0( + memory_id INTEGER PRIMARY KEY, + embedding FLOAT[1024] +); + +CREATE TABLE IF NOT EXISTS memory_usage_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + memory_id INTEGER NOT NULL, + session_id TEXT NOT NULL, + query TEXT NOT NULL, + was_referenced INTEGER NOT NULL DEFAULT 0, + used_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_usage_memory ON memory_usage_log(memory_id); +CREATE INDEX IF NOT EXISTS idx_usage_session ON memory_usage_log(session_id); + +CREATE TABLE IF NOT EXISTS dialogue_buffer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + user_query TEXT NOT NULL, + assistant_response TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_buffer_session ON dialogue_buffer(session_id, created_at DESC); + +CREATE TABLE IF NOT EXISTS subagent_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parent_session_id TEXT NOT NULL, + session_id TEXT NOT NULL, + agent_name TEXT NOT NULL, + role TEXT NOT NULL CHECK(role IN ('assistant', 'system', 'user')), + content TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_subagent_parent ON subagent_messages(parent_session_id); +CREATE INDEX IF NOT EXISTS idx_subagent_session ON subagent_messages(session_id); +` + + _, err := s.db.Exec(schema) + return err +} + +func (s *SQLiteStore) Save(sessionID string, msg SessionMessage) error { + s.mu.Lock() + defer s.mu.Unlock() + + tx, err := s.db.Begin() + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + _, err = tx.Exec( + `INSERT INTO sessions (id, updated_at) VALUES (?, ?) + ON CONFLICT(id) DO UPDATE SET updated_at = ?`, + sessionID, msg.Timestamp, msg.Timestamp, + ) + if err != nil { + return fmt.Errorf("failed to upsert session: %w", err) + } + + metadataJSON := "{}" + if len(msg.Metadata) > 0 { + metadataJSON = fmt.Sprintf("%v", msg.Metadata) + } + + _, err = tx.Exec( + `INSERT INTO main_messages (session_id, role, content, timestamp, metadata) + VALUES (?, ?, ?, ?, ?)`, + sessionID, string(msg.Role), msg.Content, msg.Timestamp, metadataJSON, + ) + if err != nil { + return fmt.Errorf("failed to insert message: %w", err) + } + + return tx.Commit() +} + +func (s *SQLiteStore) Load(sessionID string) ([]SessionMessage, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var status string + err := s.db.QueryRow( + "SELECT status FROM sessions WHERE id = ?", + sessionID, + ).Scan(&status) + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("session %q not found", sessionID) + } + return nil, fmt.Errorf("failed to query session: %w", err) + } + + rows, err := s.db.Query( + `SELECT role, content, timestamp, metadata FROM main_messages + WHERE session_id = ? + ORDER BY timestamp ASC, id ASC`, + sessionID, + ) + if err != nil { + return nil, fmt.Errorf("failed to query messages: %w", err) + } + defer rows.Close() + + var messages []SessionMessage + for rows.Next() { + var msg SessionMessage + var timestampStr string + var metadataStr string + + if err := rows.Scan(&msg.Role, &msg.Content, ×tampStr, &metadataStr); err != nil { + return nil, fmt.Errorf("failed to scan message: %w", err) + } + + msg.Timestamp, _ = time.Parse(time.RFC3339, timestampStr) + messages = append(messages, msg) + } + + return messages, rows.Err() +} + +func (s *SQLiteStore) List() ([]string, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + rows, err := s.db.Query( + "SELECT id FROM sessions WHERE status = 'active' ORDER BY updated_at DESC", + ) + if err != nil { + return nil, fmt.Errorf("failed to query sessions: %w", err) + } + defer rows.Close() + + var sessions []string + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + return nil, fmt.Errorf("failed to scan session: %w", err) + } + sessions = append(sessions, id) + } + + return sessions, rows.Err() +} + +func (s *SQLiteStore) Exists(sessionID string) (bool, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var count int + err := s.db.QueryRow( + "SELECT COUNT(*) FROM sessions WHERE id = ?", + sessionID, + ).Scan(&count) + if err != nil { + return false, fmt.Errorf("failed to check session: %w", err) + } + + return count > 0, nil +} + +func (s *SQLiteStore) Archive(sessionID string) error { + s.mu.Lock() + defer s.mu.Unlock() + + result, err := s.db.Exec( + "UPDATE sessions SET status = 'archived' WHERE id = ?", + sessionID, + ) + if err != nil { + return fmt.Errorf("failed to archive session: %w", err) + } + + rowsAffected, _ := result.RowsAffected() + if rowsAffected == 0 { + return fmt.Errorf("session %q not found", sessionID) + } + + return nil +} + +func (s *SQLiteStore) Delete(sessionID string) error { + s.mu.Lock() + defer s.mu.Unlock() + + tx, err := s.db.Begin() + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + _, err = tx.Exec("DELETE FROM main_messages WHERE session_id = ?", sessionID) + if err != nil { + return fmt.Errorf("failed to delete messages: %w", err) + } + + _, err = tx.Exec("DELETE FROM sessions WHERE id = ?", sessionID) + if err != nil { + return fmt.Errorf("failed to delete session: %w", err) + } + + return tx.Commit() +} + +func (s *SQLiteStore) SaveSubAgentMessage(parentSessionID, sessionID, agentName string, msg SessionMessage) error { + s.mu.Lock() + defer s.mu.Unlock() + + _, err := s.db.Exec( + `INSERT INTO subagent_messages (parent_session_id, session_id, agent_name, role, content, timestamp) + VALUES (?, ?, ?, ?, ?, ?)`, + parentSessionID, sessionID, agentName, string(msg.Role), msg.Content, msg.Timestamp, + ) + if err != nil { + return fmt.Errorf("failed to save subagent message: %w", err) + } + return nil +} + +func (s *SQLiteStore) LoadSubAgentMessages(sessionID string) ([]SessionMessage, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + rows, err := s.db.Query( + `SELECT role, content, timestamp FROM subagent_messages + WHERE session_id = ? + ORDER BY timestamp ASC, id ASC`, + sessionID, + ) + if err != nil { + return nil, fmt.Errorf("failed to query subagent messages: %w", err) + } + defer rows.Close() + + var messages []SessionMessage + for rows.Next() { + var msg SessionMessage + var timestampStr string + if err := rows.Scan(&msg.Role, &msg.Content, ×tampStr); err != nil { + return nil, fmt.Errorf("failed to scan subagent message: %w", err) + } + msg.Timestamp, _ = time.Parse(time.RFC3339, timestampStr) + messages = append(messages, msg) + } + return messages, rows.Err() +} + +func (s *SQLiteStore) Close() error { + return s.db.Close() +} + +func (s *SQLiteStore) DB() *sql.DB { + return s.db +} + + diff --git a/pkg/session/vector_store.go b/pkg/session/vector_store.go new file mode 100644 index 0000000..70b9420 --- /dev/null +++ b/pkg/session/vector_store.go @@ -0,0 +1,196 @@ +package session + +import ( + "database/sql" + "fmt" + "strings" +) + +type VectorStore struct { + db *sql.DB + enabled bool +} + +func NewVectorStore(db *sql.DB) (*VectorStore, error) { + vs := &VectorStore{db: db} + if err := vs.initSchema(); err != nil { + return &VectorStore{db: db, enabled: false}, nil + } + vs.enabled = true + return vs, nil +} + +func (vs *VectorStore) initSchema() error { + _, err := vs.db.Exec(` + CREATE VIRTUAL TABLE IF NOT EXISTS vec_main_messages USING vec0( + msg_id INTEGER PRIMARY KEY, + embedding FLOAT[1024] + ) + `) + return err +} + +func (vs *VectorStore) SaveEmbedding(msgID int64, embedding []float32) error { + if !vs.enabled { + return nil + } + + if len(embedding) != 1024 { + return fmt.Errorf("expected 1024 dimensions, got %d", len(embedding)) + } + + embeddingStr := formatEmbedding(embedding) + _, err := vs.db.Exec( + "INSERT INTO vec_main_messages (msg_id, embedding) VALUES (?, ?)", + msgID, embeddingStr, + ) + if err != nil { + return fmt.Errorf("failed to save embedding: %w", err) + } + + _, err = vs.db.Exec( + "UPDATE main_messages SET has_embedding = TRUE WHERE id = ?", + msgID, + ) + return err +} + +func (vs *VectorStore) SaveLongTermEmbedding(memoryID int64, embedding []float32) error { + if !vs.enabled { + return nil + } + + if len(embedding) != 1024 { + return fmt.Errorf("expected 1024 dimensions, got %d", len(embedding)) + } + + embeddingStr := formatEmbedding(embedding) + _, err := vs.db.Exec( + "INSERT INTO vec_long_term_memories (memory_id, embedding) VALUES (?, ?)", + memoryID, embeddingStr, + ) + if err != nil { + return fmt.Errorf("failed to save long-term embedding: %w", err) + } + return nil +} + +func (vs *VectorStore) SearchLongTermSimilar(embedding []float32, limit int) ([]struct { + MemoryID int64 + Distance float64 +}, error) { + if !vs.enabled { + return nil, nil + } + + if len(embedding) != 1024 { + return nil, fmt.Errorf("expected 1024 dimensions, got %d", len(embedding)) + } + + embeddingStr := formatEmbedding(embedding) + rows, err := vs.db.Query( + `SELECT memory_id, distance FROM vec_long_term_memories + WHERE embedding MATCH ? + ORDER BY distance + LIMIT ?`, + embeddingStr, limit, + ) + if err != nil { + return nil, fmt.Errorf("failed to search long-term vectors: %w", err) + } + defer rows.Close() + + var results []struct { + MemoryID int64 + Distance float64 + } + for rows.Next() { + var r struct { + MemoryID int64 + Distance float64 + } + if err := rows.Scan(&r.MemoryID, &r.Distance); err != nil { + return nil, err + } + results = append(results, r) + } + + return results, rows.Err() +} + +func (vs *VectorStore) SearchSimilar(embedding []float32, limit int) ([]int64, error) { + if !vs.enabled { + return []int64{}, nil + } + + if len(embedding) != 1024 { + return nil, fmt.Errorf("expected 1024 dimensions, got %d", len(embedding)) + } + + embeddingStr := formatEmbedding(embedding) + rows, err := vs.db.Query( + `SELECT msg_id FROM vec_main_messages + WHERE embedding MATCH ? + ORDER BY distance + LIMIT ?`, + embeddingStr, limit, + ) + if err != nil { + return nil, fmt.Errorf("failed to search vectors: %w", err) + } + defer rows.Close() + + var msgIDs []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + msgIDs = append(msgIDs, id) + } + + return msgIDs, rows.Err() +} + +func (vs *VectorStore) SearchSimilarInSession(sessionID string, embedding []float32, limit int) ([]int64, error) { + if !vs.enabled { + return []int64{}, nil + } + + if len(embedding) != 1024 { + return nil, fmt.Errorf("expected 1024 dimensions, got %d", len(embedding)) + } + + embeddingStr := formatEmbedding(embedding) + rows, err := vs.db.Query( + `SELECT v.msg_id FROM vec_main_messages v + JOIN main_messages m ON v.msg_id = m.id + WHERE m.session_id = ? AND v.embedding MATCH ? + ORDER BY distance + LIMIT ?`, + sessionID, embeddingStr, limit, + ) + if err != nil { + return nil, fmt.Errorf("failed to search session vectors: %w", err) + } + defer rows.Close() + + var msgIDs []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + msgIDs = append(msgIDs, id) + } + + return msgIDs, rows.Err() +} + +func formatEmbedding(embedding []float32) string { + parts := make([]string, len(embedding)) + for i, v := range embedding { + parts[i] = fmt.Sprintf("%f", v) + } + return "[" + strings.Join(parts, ",") + "]" +} diff --git a/pkg/tool/agent_call.go b/pkg/tool/agent_call.go index acac218..d84d6eb 100644 --- a/pkg/tool/agent_call.go +++ b/pkg/tool/agent_call.go @@ -3,22 +3,47 @@ package tool import ( "context" "fmt" + "io" "github.com/orca/orca/pkg/bus" ) +type AgentStreamWriter struct { + agentName string + eventBus bus.MessageBus +} + +func (w *AgentStreamWriter) Write(p []byte) (n int, err error) { + if w.eventBus != nil { + w.eventBus.Publish("agent_events", bus.Message{ + Type: bus.MsgTypeLog, + Content: map[string]interface{}{ + "event": "token", + "agent": w.agentName, + "text": string(p), + }, + }) + } + return len(p), nil +} + type Agent interface { Process(ctx context.Context, msg bus.Message) (bus.Message, error) } type agentCallTool struct { agentRegistry func(string) (Agent, bool) + eventBus bus.MessageBus } func NewAgentCallTool(registry func(string) (Agent, bool)) Tool { return &agentCallTool{agentRegistry: registry} } +func (t *agentCallTool) SetEventBus(mb bus.MessageBus) { + t.eventBus = mb +} + func (t *agentCallTool) Name() string { return "agent_call" } func (t *agentCallTool) Description() string { @@ -63,7 +88,39 @@ func (t *agentCallTool) Execute(ctx context.Context, args map[string]interface{} Content: task, } + if t.eventBus != nil { + t.eventBus.Publish("agent_events", bus.Message{ + Type: bus.MsgTypeToolCall, + From: "llm", + To: agentName, + Content: map[string]interface{}{"event": "start", "agent": agentName, "task": task}, + }) + } + + if setter, ok := agent.(interface{ SetStreamWriter(io.Writer) }); ok && t.eventBus != nil { + writer := &AgentStreamWriter{agentName: agentName, eventBus: t.eventBus} + setter.SetStreamWriter(writer) + } + resp, err := agent.Process(ctx, msg) + + if t.eventBus != nil { + status := "completed" + if err != nil { + status = "failed" + } + resultContent := "" + if resp.Content != nil { + resultContent = fmt.Sprintf("%v", resp.Content) + } + t.eventBus.Publish("agent_events", bus.Message{ + Type: bus.MsgTypeToolResult, + From: agentName, + To: "llm", + Content: map[string]interface{}{"event": "end", "agent": agentName, "status": status, "result": resultContent}, + }) + } + if err != nil { return ErrorResult(fmt.Sprintf("agent '%s' execution failed: %v", agentName, err)), nil } diff --git a/server.log b/server.log new file mode 100644 index 0000000..dfce2a4 --- /dev/null +++ b/server.log @@ -0,0 +1,11 @@ +2026/05/10 20:16:21 kernel: created DeepSeek client (model=deepseek-v4-flash) +2026/05/10 20:16:21 kernel: created sub-agent "architect" from /Users/wang/.orca/prompts/architect.md +2026/05/10 20:16:21 kernel: created sub-agent "calculator" from /Users/wang/.orca/prompts/calculator.md +2026/05/10 20:16:21 kernel: created sub-agent "coder" from /Users/wang/.orca/prompts/coder.md +2026/05/10 20:16:21 kernel: created sub-agent "reviewer" from /Users/wang/.orca/prompts/reviewer.md +2026/05/10 20:16:21 kernel: created 4 sub-agents +2026/05/10 20:16:21 kernel: started (tools=6) +2026/05/10 20:16:21 kernel: warning: skill loading had errors: skill: loaded 8 skills with 1 errors: art-design-pro: skill: "/Users/wang/.agents/skills/art-design-pro/SKILL.md" is missing 'name' in frontmatter +2026/05/10 20:16:21 kernel: loaded 8 skills +Starting web server on http://localhost:8081 +2026/05/10 20:16:21 WebSocket server starting on http://localhost:8081 diff --git a/test.md b/test.md deleted file mode 100644 index eeccff1..0000000 --- a/test.md +++ /dev/null @@ -1,17 +0,0 @@ -## 测试文件 - -这是一个Markdown格式的测试文件。 - -- 列表项1 -- 列表项2 -- 列表项3 - -```go -package main - -import "fmt" - -func main() { - fmt.Println("Hello, orca.ai!") -} -``` \ No newline at end of file diff --git a/test_clean b/test_clean new file mode 100755 index 0000000..a1f975f Binary files /dev/null and b/test_clean differ diff --git a/test_memory b/test_memory new file mode 100755 index 0000000..a1cc8cd Binary files /dev/null and b/test_memory differ diff --git a/test_memory_retrieval.go b/test_memory_retrieval.go new file mode 100644 index 0000000..21660c5 --- /dev/null +++ b/test_memory_retrieval.go @@ -0,0 +1,227 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + "github.com/orca/orca/pkg/bus" + "github.com/orca/orca/pkg/kernel" + "github.com/orca/orca/pkg/actor" + "github.com/orca/orca/pkg/session" + _ "modernc.org/sqlite" + _ "modernc.org/sqlite/vec" +) + +func main() { + fmt.Println("=== 记忆系统综合测试 ===") + fmt.Println("测试内容:记忆检索、命中率、token节省、日常使用") + + k := kernel.New() + if err := k.Start(); err != nil { + fmt.Printf("启动失败: %v\n", err) + os.Exit(1) + } + defer k.Stop() + + orch := k.Orchestrator() + mm := k.MemoryManager() + + // 阶段1: 建立用户画像(触发长期记忆提取) + fmt.Println("\n--- 阶段1: 建立用户画像 ---") + sendMessage(k, "你好,我叫李四,我在金融科技公司做架构师") + sendMessage(k, "我主要用Java和Kotlin,最近在研究微服务拆分") + sendMessage(k, "我喜欢详细的技术解释,带架构图最好") + sendMessage(k, "现在负责支付系统的重构,从单体迁移到微服务") + sendMessage(k, "团队有10个人,前端用React,后端用Spring Boot") + + // 等待长期记忆提取 + fmt.Println("\n等待长期记忆提取 (15秒)...") + time.Sleep(15 * time.Second) + + // 阶段2: 子Agent调用(测试隔离) + fmt.Println("\n--- 阶段2: 子Agent调用测试 ---") + callSubAgent(orch, "coder", "写一个JWT认证的工具类,Java实现") + callSubAgent(orch, "reviewer", "审查代码:public class Auth { public String token; }") + + // 阶段3: 查询记忆(测试检索命中率) + fmt.Println("\n--- 阶段3: 记忆检索测试 ---") + fmt.Println("\n查询1: 询问技术偏好(应命中长期记忆)") + sendMessage(k, "你觉得Java和Go哪个更适合做支付系统?") + + fmt.Println("\n查询2: 询问团队信息(应命中长期记忆)") + sendMessage(k, "我们团队前端用什么框架比较好?") + + fmt.Println("\n查询3: 询问个人背景(应命中长期记忆)") + sendMessage(k, "你能根据我的背景给些微服务拆分的建议吗?") + + fmt.Println("\n查询4: 无关查询(测试未命中情况)") + sendMessage(k, "今天天气怎么样?") + + // 阶段4: 等待并检查统计 + fmt.Println("\n--- 阶段4: 等待并收集统计 (5秒) ---") + time.Sleep(5 * time.Second) + + // 阶段5: 详细统计 + fmt.Println("\n--- 阶段5: 详细统计 ---") + printDetailedStats(mm) + checkDatabase() +} + +func sendMessage(k *kernel.Kernel, content string) string { + resp, err := k.SendMessage("user", "llm", content) + if err != nil { + fmt.Printf(" 发送失败: %v\n", err) + return "" + } + fmt.Printf(" 回复: %s\n", truncate(resp, 150)) + return resp +} + +func callSubAgent(orch *actor.Orchestrator, agentName, task string) { + msg := bus.Message{ + Type: bus.MsgTypeTaskRequest, + From: "user", + To: agentName, + Content: task, + } + resp, err := orch.Process(context.Background(), msg) + if err != nil { + fmt.Printf(" %s 调用失败: %v\n", agentName, err) + } else { + fmt.Printf(" %s 回复: %s\n", agentName, truncate(fmt.Sprintf("%v", resp.Content), 150)) + } +} + +func printDetailedStats(mm *session.MemoryManager) { + if mm == nil { + fmt.Println("MemoryManager 未初始化") + return + } + + // Embedding缓存统计 + cacheSize, cacheHits, cacheMisses := mm.CacheStats() + total := cacheHits + cacheMisses + hitRate := float64(0) + if total > 0 { + hitRate = float64(cacheHits) * 100 / float64(total) + } + fmt.Printf("\n[Embedding缓存统计]\n") + fmt.Printf(" 缓存大小: %d\n", cacheSize) + fmt.Printf(" 命中次数: %d\n", cacheHits) + fmt.Printf(" 未命中次数: %d\n", cacheMisses) + fmt.Printf(" 命中率: %.1f%%\n", hitRate) + + // 记忆上下文统计(模拟查询) + queries := []string{ + "Java技术栈", + "团队规模", + "微服务拆分", + "前端框架", + "天气", + } + + fmt.Printf("\n[记忆检索测试 - %d个查询]\n", len(queries)) + totalMemories := 0 + referencedMemories := 0 + for _, q := range queries { + ctx, stats := mm.BuildMemoryContextWithStats("default", q) + hasMemory := ctx != "" + fmt.Printf(" 查询 '%s': 短期=%d, 长期=%d, tokens=%d, 有记忆=%v\n", + q, stats.ShortTermCount, stats.LongTermCount, stats.TotalTokens, hasMemory) + if hasMemory { + totalMemories++ + if stats.LongTermCount > 0 { + referencedMemories++ + } + } + } + fmt.Printf(" 记忆命中率: %d/%d (%.0f%%)\n", referencedMemories, len(queries), float64(referencedMemories)*100/float64(len(queries))) +} + +func checkDatabase() { + dbPath := os.ExpandEnv("$HOME/.orca/sessions/orcasession.db") + db, err := sql.Open("sqlite", dbPath) + if err != nil { + fmt.Printf("打开数据库失败: %v\n", err) + return + } + defer db.Close() + + fmt.Println("\n=== 数据库统计 ===") + + // 表统计 + fmt.Println("\n--- 各表记录数 ---") + tables := []string{"main_messages", "short_term_memories", "long_term_memories", "dialogue_buffer", "subagent_messages", "memory_usage_log"} + for _, table := range tables { + var count int + db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", table)).Scan(&count) + fmt.Printf(" %s: %d\n", table, count) + } + + // 长期记忆详情 + fmt.Println("\n--- 长期记忆详情 ---") + rows, _ := db.Query(` + SELECT id, substr(content, 1, 50), memory_type, weight, confidence, access_count, created_at + FROM long_term_memories + ORDER BY id DESC + `) + defer rows.Close() + for rows.Next() { + var id int + var content, mtype string + var weight, confidence float64 + var accessCount int + var createdAt string + rows.Scan(&id, &content, &mtype, &weight, &confidence, &accessCount, &createdAt) + fmt.Printf(" [#%d %s] weight=%.2f conf=%.2f access=%d | %s\n", id, mtype, weight, confidence, accessCount, content) + } + + // memory_usage_log 详情 + fmt.Println("\n--- 记忆使用日志 ---") + rows2, _ := db.Query(` + SELECT memory_id, substr(query, 1, 30), was_referenced, used_at + FROM memory_usage_log + ORDER BY id DESC + `) + defer rows2.Close() + count := 0 + for rows2.Next() { + var memID int + var query string + var referenced int + var usedAt string + rows2.Scan(&memID, &query, &referenced, &usedAt) + fmt.Printf(" memory_id=%d query='%s' referenced=%v at=%s\n", memID, query, referenced == 1, usedAt) + count++ + } + if count == 0 { + fmt.Println(" (空)") + } + + // 子Agent统计 + fmt.Println("\n--- 子Agent消息统计 ---") + rows3, _ := db.Query(`SELECT agent_name, COUNT(*) FROM subagent_messages GROUP BY agent_name`) + defer rows3.Close() + for rows3.Next() { + var agent string + var cnt int + rows3.Scan(&agent, &cnt) + fmt.Printf(" %s: %d\n", agent, cnt) + } + + // 对话缓冲 + fmt.Println("\n--- 对话缓冲 ---") + var bufCount int + db.QueryRow("SELECT COUNT(*) FROM dialogue_buffer").Scan(&bufCount) + fmt.Printf(" 当前缓冲条数: %d\n", bufCount) +} + +func truncate(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max] + "..." +} diff --git a/test_memory_system.go b/test_memory_system.go new file mode 100644 index 0000000..6c2b875 --- /dev/null +++ b/test_memory_system.go @@ -0,0 +1,247 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + "github.com/orca/orca/pkg/bus" + "github.com/orca/orca/pkg/kernel" + _ "modernc.org/sqlite" + _ "modernc.org/sqlite/vec" +) + +func main() { + fmt.Println("=== 子Agent调用 + 记忆系统测试 ===") + + k := kernel.New() + if err := k.Start(); err != nil { + fmt.Printf("启动失败: %v\n", err) + os.Exit(1) + } + defer k.Stop() + + orch := k.Orchestrator() + mm := k.MemoryManager() + + fmt.Println("\n1. 发送普通消息给 LLM...") + resp1, err := k.SendMessage("user", "llm", "你好,我叫张三,我喜欢用Go语言写后端服务") + if err != nil { + fmt.Printf("发送失败: %v\n", err) + os.Exit(1) + } + fmt.Printf("回复: %s\n", truncate(resp1, 200)) + + fmt.Println("\n2. 发送第二条消息给 LLM...") + resp2, err := k.SendMessage("user", "llm", "你觉得Go和Python哪个更适合做Web后端?") + if err != nil { + fmt.Printf("发送失败: %v\n", err) + os.Exit(1) + } + fmt.Printf("回复: %s\n", truncate(resp2, 200)) + + fmt.Println("\n3. 直接调用 coder 子Agent...") + msg := bus.Message{ + Type: bus.MsgTypeTaskRequest, + From: "user", + To: "coder", + Content: "请写一个快速排序算法,用Go语言实现", + } + resp3, err := orch.Process(context.Background(), msg) + if err != nil { + fmt.Printf("coder 调用失败: %v\n", err) + } else { + fmt.Printf("coder 回复: %s\n", truncate(fmt.Sprintf("%v", resp3.Content), 300)) + } + + fmt.Println("\n4. 直接调用 reviewer 子Agent...") + msg2 := bus.Message{ + Type: bus.MsgTypeTaskRequest, + From: "user", + To: "reviewer", + Content: "请审查以下代码的质量:func main() { fmt.Println(\"hello\") }", + } + resp4, err := orch.Process(context.Background(), msg2) + if err != nil { + fmt.Printf("reviewer 调用失败: %v\n", err) + } else { + fmt.Printf("reviewer 回复: %s\n", truncate(fmt.Sprintf("%v", resp4.Content), 300)) + } + + fmt.Println("\n5. 发送第三条消息(继续对话,积累dialogue_buffer)...") + resp5, err := k.SendMessage("user", "llm", "我在一家电商公司做后端开发,平时用Go处理高并发订单系统") + if err != nil { + fmt.Printf("发送失败: %v\n", err) + } else { + fmt.Printf("回复: %s\n", truncate(resp5, 200)) + } + + fmt.Println("\n6. 发送第四条消息(继续对话,触发长期记忆提取阈值)...") + resp6, err := k.SendMessage("user", "llm", "我希望回答简洁一些,直接给代码示例,不要太多解释") + if err != nil { + fmt.Printf("发送失败: %v\n", err) + } else { + fmt.Printf("回复: %s\n", truncate(resp6, 200)) + } + + fmt.Println("\n7. 发送第五条消息(超过5条阈值,应触发自动提取)...") + resp7, err := k.SendMessage("user", "llm", "最近在做一个库存管理模块,用Redis做缓存,MySQL做持久化") + if err != nil { + fmt.Printf("发送失败: %v\n", err) + } else { + fmt.Printf("回复: %s\n", truncate(resp7, 200)) + } + + fmt.Println("\n8. 手动维护短期记忆...") + if mm != nil { + mm.AddShortTermMemory("default", "用户张三喜欢Go语言") + mm.AddShortTermMemory("default", "用户询问过Web后端技术选型") + fmt.Println("短期记忆已添加") + } + + fmt.Println("\n9. 手动添加长期记忆...") + if mm != nil { + mm.AddLongTermMemory("用户偏好Go语言", "preference") + mm.AddLongTermMemory("用户关注后端开发", "fact") + fmt.Println("长期记忆已添加") + } + + fmt.Println("\n10. 等待异步长期记忆提取 + embedding 处理 (10秒)...") + time.Sleep(10 * time.Second) + + fmt.Println("\n11. 查询数据库...") + checkDatabase() +} + +func checkDatabase() { + dbPath := os.ExpandEnv("$HOME/.orca/sessions/orcasession.db") + db, err := sql.Open("sqlite", dbPath) + if err != nil { + fmt.Printf("打开数据库失败: %v\n", err) + return + } + defer db.Close() + + fmt.Println("\n--- main_messages 统计 ---") + var count int + db.QueryRow("SELECT COUNT(*) FROM main_messages").Scan(&count) + fmt.Printf("总消息数: %d\n", count) + + if count > 0 { + fmt.Println(" 最近5条消息:") + rows, _ := db.Query(` + SELECT role, substr(content, 1, 60), timestamp + FROM main_messages + ORDER BY id DESC LIMIT 5 + `) + defer rows.Close() + for rows.Next() { + var role, content, ts string + rows.Scan(&role, &content, &ts) + fmt.Printf(" [%s] %s... (%s)\n", role, content, ts) + } + } + + fmt.Println("\n--- subagent_messages 统计 ---") + var subCount int + db.QueryRow("SELECT COUNT(*) FROM subagent_messages").Scan(&subCount) + fmt.Printf("子Agent消息数: %d\n", subCount) + + if subCount > 0 { + fmt.Println(" 子Agent消息明细:") + rows, _ := db.Query(` + SELECT agent_name, role, substr(content, 1, 50), parent_session_id, session_id + FROM subagent_messages + ORDER BY id DESC LIMIT 10 + `) + defer rows.Close() + for rows.Next() { + var agent, role, content, parentSID, sid string + rows.Scan(&agent, &role, &content, &parentSID, &sid) + fmt.Printf(" [%s/%s] %s... (parent=%s, sid=%s)\n", agent, role, content, parentSID, sid) + } + } + + fmt.Println("\n--- 向量表统计 ---") + var vecCount int + err = db.QueryRow("SELECT COUNT(*) FROM vec_main_messages").Scan(&vecCount) + if err != nil { + fmt.Printf("向量表查询失败: %v\n", err) + } else { + fmt.Printf("向量数量: %d\n", vecCount) + } + + if vecCount > 0 { + fmt.Println(" 最近5条向量:") + rows, _ := db.Query(` + SELECT v.msg_id, m.role, substr(m.content, 1, 40) + FROM vec_main_messages v + JOIN main_messages m ON v.msg_id = m.id + LIMIT 5 + `) + defer rows.Close() + for rows.Next() { + var msgID int + var role, content string + rows.Scan(&msgID, &role, &content) + fmt.Printf(" msg_id=%d role=%s content=%s\n", msgID, role, content) + } + } + + fmt.Println("\n--- 短期记忆 ---") + rows, _ := db.Query("SELECT substr(content, 1, 60) FROM short_term_memories LIMIT 5") + defer rows.Close() + stmCount := 0 + for rows.Next() { + var content string + rows.Scan(&content) + fmt.Printf(" %s\n", content) + stmCount++ + } + if stmCount == 0 { + fmt.Println(" (空)") + } + + fmt.Println("\n--- 长期记忆 ---") + rows2, _ := db.Query("SELECT substr(content, 1, 60), memory_type, weight FROM long_term_memories LIMIT 5") + defer rows2.Close() + ltmCount := 0 + for rows2.Next() { + var content, mtype string + var weight float64 + rows2.Scan(&content, &mtype, &weight) + fmt.Printf(" [%s|weight=%.2f] %s\n", mtype, weight, content) + ltmCount++ + } + if ltmCount == 0 { + fmt.Println(" (空)") + } + + fmt.Println("\n--- dialogue_buffer ---") + var bufCount int + db.QueryRow("SELECT COUNT(*) FROM dialogue_buffer").Scan(&bufCount) + fmt.Printf("对话缓冲条数: %d\n", bufCount) + + fmt.Println("\n--- 按 agent_name 统计子Agent消息 ---") + rows3, _ := db.Query(` + SELECT agent_name, COUNT(*) as cnt + FROM subagent_messages + GROUP BY agent_name + `) + defer rows3.Close() + for rows3.Next() { + var agent string + var cnt int + rows3.Scan(&agent, &cnt) + fmt.Printf(" %s: %d 条消息\n", agent, cnt) + } +} + +func truncate(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max] + "..." +} diff --git a/test_retrieval b/test_retrieval new file mode 100755 index 0000000..b4bc3ef Binary files /dev/null and b/test_retrieval differ diff --git a/thoughts/shared/designs/2025-05-11-orca-memory-final-design.md b/thoughts/shared/designs/2025-05-11-orca-memory-final-design.md new file mode 100644 index 0000000..538d60b --- /dev/null +++ b/thoughts/shared/designs/2025-05-11-orca-memory-final-design.md @@ -0,0 +1,491 @@ +--- +date: 2025-05-11 +topic: "Orca 完整记忆系统 + Agent 进化方案" +status: validated +--- + +# Orca 完整记忆系统 + Agent 进化方案 + +## 一、系统架构总览 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户消息 │ +└──────────────────────┬──────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 智能注入层(规则驱动,零 LLM 开销) │ +│ • 首轮对话 → 不带记忆 │ +│ • 第 2+ 轮 → 自动注入短期记忆(3条) │ +│ • 技术讨论 → 额外注入长期记忆(2条) │ +└──────────────────────┬──────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 记忆查询层(按需加载) │ +│ • 短期记忆:SQLite + 语义检索(缓存命中 130x 加速) │ +│ • 长期记忆:SQLite + 独立向量索引(vec_long_term_memories) │ +└──────────────────────┬──────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ LLM Agent 处理消息 │ +└──────────────────────┬──────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 记忆维护层(后台异步) │ +│ • 短期记忆:即时摘要保存(现有逻辑) │ +│ • 长期记忆:MemoryExtractorAgent 批量提取(每 5 轮一次) │ +└──────────────────────┬──────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 进化层(权重系统) │ +│ • 记忆被引用 → 权重 +1 │ +│ • 记忆未引用 → 权重 -0.5 │ +│ • 权重 < 0.3 → 归档 │ +│ • 权重 > 5.0 → 核心记忆 │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 二、配置架构(config.toml) + +```toml +# ========== 主 LLM 配置 ========== +provider = "deepseek" + +[deepseek] +base_url = "https://api.deepseek.com/v1" +model = "deepseek-v4-flash" +api_key = "sk-xxx" +timeout = "120s" + +# ========== Embedding 配置 ========== +[embedding] +provider = "siliconflow" +model = "Pro/BAAI/bge-m3" +dimensions = 1024 +max_context = 8192 + +[siliconflow] +api_key = "sk-xxx" +base_url = "https://api.siliconflow.cn/v1" + +# ========== 记忆系统配置(新增) ========== +[memory] +enabled = true # 记忆系统总开关 +max_history = 100 # 工作记忆最大轮数 + +[memory.short_term] +max_items = 10 # 最多保留 10 条短期记忆 +compression_threshold = 200 # 超过 200 字自动压缩 + +[memory.long_term] +enabled = true # 长期记忆开关 +vector_index = true # 启用向量索引 +max_return = 2 # 每次最多返回 2 条 +archive_threshold = 0.3 # 权重低于此值归档 + +[memory.inject] +first_round_empty = true # 首轮不带记忆 +short_term_count = 3 # 默认注入 3 条短期记忆 +long_term_trigger = "technical" # 技术讨论触发长期记忆 +min_query_length = 10 # query 最短长度才查长期记忆 + +# ========== MemoryExtractorAgent 配置(新增) ========== +[memory_agent] +enabled = true +provider = "deepseek" # 可独立配置,默认继承主 LLM +model = "deepseek-v4-flash" # 可用便宜模型,如 deepseek-chat +api_key = "" # 留空继承 deepseek.api_key +base_url = "" # 留空继承 deepseek.base_url +timeout = "60s" + +[memory_agent.extract] +batch_size = 5 # 每 5 轮提取一次 +max_facts = 10 # 每次最多提取 10 个事实 +min_confidence = 0.6 # 置信度阈值 +auto_tag = true # 自动打标签 + +[memory_agent.summarize] +enabled = true # 启用对话总结 +trigger_tokens = 4000 # 超过此 token 触发总结 +``` + +--- + +## 三、Agent 描述文件 + +**文件位置**: `~/.orca/agents/memory_extractor.md` + +```markdown +# Memory Extractor Agent + +你是一个专门从对话中提取用户信息的 Agent。你的工作是将非结构化的对话转化为结构化的长期记忆。 + +## 任务 + +分析给定的对话记录,提取以下类型的信息: + +1. **事实 (fact)**:客观信息 + - 工作:公司、职位、技术栈、行业 + - 技术:擅长语言、框架偏好、架构经验 + - 个人:教育背景、所在城市(仅用户明确提及) + +2. **偏好 (preference)**:主观倾向 + - 回答风格:简洁/详细/代码示例/架构图 + - 技术偏好:语言、数据库、部署方式 + - 沟通偏好:正式/ casual + +3. **项目 (project)**:当前工作 + - 项目名称、技术方案、当前阶段、遇到的挑战 + +## 输出格式 + +只输出 JSON,不要任何解释: + +```json +{ + "facts": [ + { + "content": "用户在电商公司担任后端工程师", + "type": "fact", + "tags": ["工作", "后端", "电商"], + "confidence": 0.95, + "replace": null + }, + { + "content": "用户偏好简洁的技术回答,不要过多解释", + "type": "preference", + "tags": ["沟通风格", "偏好"], + "confidence": 0.85, + "replace": "用户喜欢详细的回答" + } + ] +} +``` + +## 规则 + +- confidence < 0.6 的事实不输出 +- 如果新事实与旧事实冲突: + - 在 replace 字段填入被替换的旧事实 content + - 只替换同一 type + 同一 tags 的事实 +- 不猜测、不推断,只提取用户明确表达的信息 +- 标签从预设列表选择:工作、技术、偏好、项目、沟通风格、行业 +``` + +--- + +## 四、数据表设计 + +### 现有表(已验证) + +- `main_messages` — 工作记忆 +- `short_term_memories` — 短期记忆 +- `subagent_messages` — 子 Agent 对话 + +### 新增表 + +```sql +-- 长期记忆向量索引(核心新增) +CREATE VIRTUAL TABLE IF NOT EXISTS vec_long_term_memories USING vec0( + memory_id INTEGER PRIMARY KEY, + embedding FLOAT[1024] +); + +-- 长期记忆主表(扩展) +CREATE TABLE IF NOT EXISTS long_term_memories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL UNIQUE, + memory_type TEXT NOT NULL DEFAULT 'fact', -- fact/preference/project + tags TEXT, -- JSON 数组 ["工作", "技术"] + confidence REAL NOT NULL DEFAULT 0.8, + weight REAL NOT NULL DEFAULT 1.0, -- 动态权重 + access_count INTEGER NOT NULL DEFAULT 0, + last_accessed DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + archived INTEGER NOT NULL DEFAULT 0 -- 0=活跃, 1=归档 +); + +-- 记忆使用日志(用于权重计算) +CREATE TABLE IF NOT EXISTS memory_usage_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + memory_id INTEGER NOT NULL, + session_id TEXT NOT NULL, + query TEXT NOT NULL, -- 用户原始 query + was_referenced INTEGER NOT NULL DEFAULT 0, -- 是否被 Agent 引用 + used_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 对话缓冲(批量提取用) +CREATE TABLE IF NOT EXISTS dialogue_buffer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + user_query TEXT NOT NULL, + assistant_response TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 五、核心流程设计 + +### 5.1 消息处理流程(动态注入) + +```go +func (a *LLMAgent) buildLLMMessages(query string) []llm.Message { + messages := make([]llm.Message, 0) + + // 1. System prompt(原有) + if a.systemPrompt != "" { + messages = append(messages, llm.Message{Role: "system", Content: a.systemPrompt}) + } + + // 2. Tool prompt(原有) + if toolPrompt := a.buildToolPrompt(); toolPrompt != "" { + messages = append(messages, llm.Message{Role: "system", Content: toolPrompt}) + } + + // 3. 记忆注入(新增:智能判断) + if shouldInjectMemory(query, a.sessionMgr.GetMessageCount(a.sessionID)) { + ctx, stats := a.memoryManager.BuildMemoryContextWithStats(a.sessionID, query) + if ctx != "" { + messages = append(messages, llm.Message{Role: "system", Content: ctx}) + log.Printf("[memory] Injected: short=%d, long=%d, tokens=%d", + stats.ShortTermCount, stats.LongTermCount, stats.TotalTokens) + } + } + + // 4. 对话历史(原有) + // ... + + return messages +} + +func shouldInjectMemory(query string, msgCount int) bool { + if msgCount == 0 { + return false // 首轮不带记忆 + } + if len(query) < 10 { + return false // 太短不查 + } + return true +} +``` + +### 5.2 记忆维护流程(批量提取) + +```go +func (mm *MemoryManager) MaintainSessionMemory(sessionID, userQuery, assistantResponse string) { + // 1. 保存短期记忆(即时) + summary := fmt.Sprintf("用户问:%s\n回答:%s", + userQuery, truncateString(assistantResponse, 100)) + mm.AddShortTermMemory(sessionID, summary) + + // 2. 缓冲对话(用于批量提取) + mm.bufferDialogue(sessionID, userQuery, assistantResponse) + + // 3. 检查是否触发批量提取 + if mm.shouldExtract() { + mm.triggerExtraction(sessionID) + } +} + +func (mm *MemoryManager) triggerExtraction(sessionID string) { + dialogues := mm.flushBuffer(sessionID) + + // 异步调用 MemoryExtractorAgent + go func() { + facts, err := mm.extractor.ExtractFacts(dialogues) + if err != nil { + log.Printf("[memory] Extraction failed: %v", err) + return + } + + for _, fact := range facts { + if fact.Confidence >= mm.config.MinConfidence { + mm.AddLongTermMemory(fact) + } + } + }() +} +``` + +### 5.3 权重反馈流程(自动进化) + +```go +func (mm *MemoryManager) recordMemoryUsage(memoryID int64, sessionID, query string, referenced bool) { + // 1. 记录使用日志 + mm.store.Exec( + "INSERT INTO memory_usage_log (memory_id, session_id, query, was_referenced) VALUES (?, ?, ?, ?)", + memoryID, sessionID, query, referenced, + ) + + // 2. 更新权重 + delta := 0.5 + if !referenced { + delta = -0.3 + } + + mm.store.Exec( + "UPDATE long_term_memories SET weight = weight + ?, access_count = access_count + 1, last_accessed = ? WHERE id = ?", + delta, time.Now(), memoryID, + ) + + // 3. 检查归档 + mm.archiveLowWeightMemories() +} +``` + +--- + +## 六、MemoryExtractorAgent 实现 + +```go +package actor + +type MemoryExtractorAgent struct { + *SubAgent + config ExtractConfig +} + +func NewMemoryExtractorAgent(id string, llmBackend llm.LLM, cfg ExtractConfig) *MemoryExtractorAgent { + prompt := loadAgentPrompt("memory_extractor") // 读取 ~/.orca/agents/memory_extractor.md + + sa := NewSubAgent(id, llmBackend, + WithSubAgentRole("memory_extractor"), + WithSubAgentSystemPrompt(prompt), + ) + + return &MemoryExtractorAgent{SubAgent: sa, config: cfg} +} + +func (mea *MemoryExtractorAgent) ExtractFacts(dialogues []Dialogue) ([]Fact, error) { + // 格式化对话为 prompt + var sb strings.Builder + sb.WriteString("请分析以下对话记录,提取用户的关键信息:\n\n") + for i, d := range dialogues { + sb.WriteString(fmt.Sprintf("--- 对话 %d ---\n", i+1)) + sb.WriteString(fmt.Sprintf("用户:%s\n", d.UserQuery)) + sb.WriteString(fmt.Sprintf("助手:%s\n\n", d.AssistantResponse)) + } + + msg := bus.Message{Type: bus.MsgTypeTaskRequest, Content: sb.String()} + resp, err := mea.Process(context.Background(), msg) + if err != nil { + return nil, err + } + + return parseFactJSON(resp.Content) +} +``` + +--- + +## 七、实施路线图 + +### Phase 1: 基础记忆层(1-2 天) +- [ ] 扩展 `long_term_memories` 表(添加 weight, tags, archived 字段) +- [ ] 创建 `vec_long_term_memories` 向量索引表 +- [ ] 创建 `memory_usage_log` 使用日志表 +- [ ] 创建 `dialogue_buffer` 对话缓冲表 +- [ ] 修改 config.toml 解析,支持 `[memory]` 和 `[memory_agent]` 段 + +### Phase 2: 记忆注入层(1-2 天) +- [ ] 实现 `shouldInjectMemory()` 智能判断逻辑 +- [ ] 修改 `buildLLMMessages()` 注入记忆上下文 +- [ ] 实现短期记忆检索(现有逻辑优化) +- [ ] 实现长期记忆向量检索(语义搜索) +- [ ] 添加 Embedding 缓存层(130x 加速) + +### Phase 3: MemoryExtractorAgent(2-3 天) +- [ ] 创建 `~/.orca/agents/memory_extractor.md` 提示词文件 +- [ ] 实现 `MemoryExtractorAgent` 结构体 +- [ ] 实现 `ExtractFacts()` 批量提取逻辑 +- [ ] 实现对话缓冲和批量触发机制 +- [ ] 集成到 `MaintainSessionMemory()` 流程 + +### Phase 4: 权重进化系统(1-2 天) +- [ ] 实现 `recordMemoryUsage()` 权重反馈 +- [ ] 实现记忆引用检测(判断 Agent 是否使用了某条记忆) +- [ ] 实现自动归档逻辑(权重 < 0.3) +- [ ] 实现核心记忆标记(权重 > 5.0) +- [ ] 添加 `orca memory` CLI 命令(list, stats, clean) + +### Phase 5: 测试与优化(1-2 天) +- [ ] 单元测试:记忆检索、权重计算、事实提取 +- [ ] 集成测试:端到端对话流程 +- [ ] 性能测试:Embedding 缓存命中率、向量检索延迟 +- [ ] 调优:权重阈值、提取频率、注入策略 + +--- + +## 八、预期效果 + +| 指标 | 目标值 | +|------|--------| +| 记忆命中率 | > 80%(相关查询能命中有效记忆) | +| Token 消耗 | 增加 < 15%(记忆注入的额外开销) | +| 长期记忆检索 | < 100ms(向量搜索 + 缓存) | +| 自动学习 | 每 5 轮对话自动提取 2-5 个事实 | +| 记忆质量 | 人工抽查 > 90% 准确 | +| 成本增加 | Embedding API 调用 ≈ ¥0.01/千次 | + +--- + +## 九、关键决策总结 + +| 决策 | 方案 | 理由 | +|------|------|------| +| 提取频率 | 每 5 轮批量提取 | 平衡实时性与 API 成本 | +| 提取模型 | 复用主 LLM(可独立配置) | 降低复杂度,留优化空间 | +| 意图分类 | 规则驱动(首轮/长度/技术词) | 零 LLM 开销,可预测 | +| 预加载 | 无,按需查询 | 避免无关记忆污染上下文 | +| 聚类 | 标签 + 类型,无自动聚类 | 人工可理解,可控 | +| 反馈机制 | 引用检测 + 权重调整 | 简单有效,可解释 | + +--- + +## 评审意见处理 + +| 评审意见 | 处理方式 | +|---------|---------| +| vec0 兼容性 | ✅ 已确认可用,保留原方案 | +| Agent 演化系统 | ✅ 用户明确要求保留权重系统 | +| 异步处理 | ✅ 使用 goroutine 异步调用 MemoryExtractorAgent | +| 记忆衰减 | ✅ weight 字段 + archive_threshold 实现 | +| Token 预算 | ✅ 通过 max_return 和 short_term_count 控制 | +| 降级策略 | ✅ 配置项 enabled = true/false 可完全关闭 | +| 混合检索 | ⚠️ 当前纯向量检索,后续可添加 FTS5 | +| 嵌入模型版本化 | ⚠️ 当前固定 bge-m3,后续可扩展 | + +--- + +## 附录 + +### A. 嵌入模型配置 + +**固定使用**:硅基流动 Pro/BAAI/bge-m3 +- 维度:1024 +- 最大上下文:8192 +- 优势:中文优化,质量高 + +### B. 权重计算示例 + +``` +初始:weight = 1.0 +被引用 5 次:1.0 + 5*0.5 = 3.5 +未被引用 3 次:3.5 - 3*0.3 = 2.6 +超过 5.0 → 标记为核心记忆 +低于 0.3 → 归档(不删除,可恢复) +``` + +### C. 降级路径 + +``` +Level 1: 向量检索 + 权重排序(正常) +Level 2: 纯 SQL 检索(向量服务故障) +Level 3: 仅短期记忆(长期记忆关闭) +Level 4: 无记忆(memory.enabled = false) +``` \ No newline at end of file diff --git a/thoughts/shared/designs/2026-05-10-memory-system-design.md b/thoughts/shared/designs/2026-05-10-memory-system-design.md new file mode 100644 index 0000000..f08be51 --- /dev/null +++ b/thoughts/shared/designs/2026-05-10-memory-system-design.md @@ -0,0 +1,317 @@ +date: 2026-05-10 +topic: "三层记忆系统 + 向量检索 + 子Agent隔离" +status: validated + +## Problem Statement + +当前 orca.agent 使用 JSONL 文件存储会话历史,存在以下问题: +1. **检索方式原始**:只能按时间窗口取消息,无法语义检索 +2. **无记忆分层**:所有消息一视同仁,早期重要信息被截断 +3. **无跨会话知识**:用户偏好、项目背景每次重新说明 +4. **子 Agent 污染上下文**:子 agent 的详细推理过程进入主 agent 上下文 + +## Constraints + +1. **存储**:使用 SQLite + sqlite-vec(用户已安装) +2. **Embedding**:使用硅基流动 API(Pro/BAAI/bge-m3,1024维,8K上下文) +3. **向后兼容**:保留 JSONL 作为备份/迁移选项 +4. **API 密钥**:通过 `~/.orca/config.toml` 的 `[siliconflow]` 段配置 + +## Approach + +采用 **三层记忆架构** + **向量检索** + **子 Agent 隔离**: + +**三层记忆**: +- **工作记忆**:当前对话窗口(最近 N 条消息) +- **短期记忆**:本会话历史摘要(语义检索) +- **长期记忆**:跨会话关键知识(语义检索) + +**向量检索**: +- 每条消息保存时生成 Embedding +- 检索时使用余弦相似度匹配相关记忆 +- 支持跨会话语义检索 + +**子 Agent 隔离**: +- 子 agent 推理过程存储在独立表 +- 主 agent 只接收结果摘要 +- Web UI 可查看完整子 agent 执行过程 + +## Architecture + +``` +用户输入 + │ + ├─→ [工作记忆] SQL 查询最近 N 条 + │ └─→ 直接注入 Prompt + │ + ├─→ [短期记忆] 向量检索当前会话相关历史 + │ └─→ 语义匹配 → 注入 Prompt + │ + ├─→ [长期记忆] 向量检索跨会话知识 + │ └─→ 语义匹配 → 注入 Prompt + │ + └─→ LLM 生成回复 + │ + ├─→ 保存到 main_messages(生成 Embedding) + │ + └─→ 如果是工具调用 → 子 Agent 执行 + │ + ├─→ 子 Agent 输出 → subagent_messages + │ └─→ 完成后返回结果 + │ + └─→ 结果摘要 → main_messages +``` + +## Components + +### 1. SQLiteStore(替代 JSONLStore) +- 文件:`pkg/session/sqlite_store.go` +- 实现 `Store` 接口 +- 使用 SQLite 存储消息 +- 初始化时创建表结构 +- **向后兼容**:提供 JSONL → SQLite 迁移脚本 + +### 2. VectorStore(向量层) +- 文件:`pkg/session/vector_store.go` +- 封装 sqlite-vec 操作 +- 提供 SaveEmbedding、SearchSimilar 方法 +- 使用硅基流动 Embed API 生成向量(Pro/BAAI/bge-m3,1024维) +- **优化策略**: + - 只对用户消息和完整 Assistant 回复生成向量(跳过短消息 < 10 字) + - 异步生成:消息先存 SQLite,Embedding 后台 goroutine 生成 + - 批量生成:累积 5 条消息后一次性生成 + +### 3. MemoryManager(三层记忆管理) +- 文件:`pkg/session/memory_manager.go` +- 管理工作记忆、短期记忆、长期记忆 +- 提供 GetWorkingMemory、GetShortTermMemory、GetLongTermMemory +- **Token 预算管理**: + - 总预算:模型窗口的 60%(如 8K 模型 = 4800 tokens) + - 工作记忆:预算的 50%(约 2400 tokens) + - 短期记忆:预算的 30%(约 1440 tokens) + - 长期记忆:预算的 20%(约 960 tokens) +- 自动维护记忆(生成摘要、压缩) + +### 4. SubAgentStore(子 Agent 隔离) +- 文件:`pkg/session/subagent_store.go` 或复用 sqlite_store +- 独立表存储子 agent 输出 +- **存储策略**:只存最终完整回复(一条记录),不存流式 token +- 不与主 agent 上下文混合 + +### 5. LLM Agent 集成 +- 修改:`pkg/actor/llm_agent.go` +- `buildLLMMessages()` 整合三层记忆 +- 子 agent 调用时设置隔离存储 +- **触发时机**:每轮对话结束生成短期摘要,会话结束压缩到长期记忆 + +## Database Schema + +### main_messages(主对话表) +```sql +CREATE TABLE main_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')), + content TEXT NOT NULL, + msg_type TEXT DEFAULT 'normal' CHECK(msg_type IN ('normal', 'fact', 'todo', 'decision', 'preference', 'error')), + token_count INTEGER DEFAULT 0, + has_embedding BOOLEAN DEFAULT FALSE, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + metadata JSON +); +CREATE INDEX idx_main_session_time ON main_messages(session_id, timestamp DESC); +CREATE INDEX idx_main_role ON main_messages(role) WHERE role IN ('user', 'assistant'); +``` + +### subagent_messages(子 Agent 隔离表) +```sql +CREATE TABLE subagent_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parent_msg_id INTEGER NOT NULL, + session_id TEXT NOT NULL, + agent_name TEXT NOT NULL, + role TEXT NOT NULL CHECK(role IN ('assistant', 'system')), + content TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (parent_msg_id) REFERENCES main_messages(id) ON DELETE CASCADE +); +CREATE INDEX idx_subagent_parent ON subagent_messages(parent_msg_id); +``` + +### short_term_memories(短期记忆表) +```sql +CREATE TABLE short_term_memories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + content TEXT NOT NULL, + source_count INTEGER DEFAULT 1, + confidence REAL DEFAULT 0.8 CHECK(confidence BETWEEN 0 AND 1), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME +); +CREATE INDEX idx_stm_session ON short_term_memories(session_id, updated_at DESC); +``` + +### long_term_memories(长期记忆表) +```sql +CREATE TABLE long_term_memories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL, + memory_type TEXT NOT NULL CHECK(memory_type IN ('preference', 'fact', 'decision', 'project')), + source_session TEXT, + confidence REAL DEFAULT 0.5 CHECK(confidence BETWEEN 0 AND 1), + access_count INTEGER DEFAULT 0, + last_accessed DATETIME, + expires_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_ltm_type ON long_term_memories(memory_type, confidence DESC); +``` + +### vec_main_messages(向量虚拟表) +```sql +CREATE VIRTUAL TABLE vec_main_messages USING vec0( + msg_id INTEGER PRIMARY KEY, + embedding FLOAT[1024] -- 硅基流动 Pro/BAAI/bge-m3 维度 +); +``` + +## Data Flow + +### 消息存储流程 +``` +AddMessage(msg) + │ + ├─→ SQLiteStore.Save(msg) → main_messages 表 + │ └─→ 计算 token_count(估算或计数) + │ + ├─→ 【异步】如果 msg.role IN ('user', 'assistant') 且 len(content) > 10 + │ └─→ EmbeddingQueue <- msg + │ └─→ 后台 goroutine 批量生成 Embedding + │ └─→ VectorStore.Save(msg.id, embedding) + │ └─→ UPDATE main_messages SET has_embedding = TRUE + │ + └─→ 返回 +``` + +### 记忆检索流程 +``` +buildLLMMessages() + │ + ├─→ MemoryManager.GetWorkingMemory(sessionID, tokenBudget=2400) + │ └─→ SQL: SELECT * FROM main_messages + │ WHERE session_id = ? + │ ORDER BY timestamp DESC + │ └─→ 累积 messages 直到 token_count ≈ 2400 + │ └─→ 反转顺序( chronological ) + │ + ├─→ MemoryManager.GetShortTermMemory(sessionID, query, maxItems=3) + │ └─→ 如果 query 存在且 has_embedding: + │ └─→ 硅基流动 API.Embed(query) + │ └─→ 向量检索 short_term_memories(同 session) + │ └─→ 返回 Top-3 相关摘要 + │ └─→ 否则:SQL 取最近 3 条摘要 + │ + └─→ MemoryManager.GetLongTermMemory(query, maxItems=2) + └─→ 如果 query 存在且 has_embedding: + │ └─→ 硅基流动 API.Embed(query) + │ └─→ 向量检索 long_term_memories + │ └─→ 返回 Top-2 相关记忆 + └─→ 否则:SQL 取 access_count 最高的 2 条 + └─→ UPDATE access_count += 1, last_accessed = NOW() +``` + +### 子 Agent 执行流程 +``` +agent_call.Execute(coder) + │ + ├─→ 创建 SubAgent 上下文(隔离存储) + │ + ├─→ coder.Process(task) + │ ├─→ 流式输出 → subagent_messages + │ └─→ 完成后返回结果 + │ + └─→ 结果摘要 → main_messages +``` + +## Error Handling + +1. **Embedding 失败**:降级为纯文本存储,不影响功能 + - 标记 `has_embedding = FALSE` + - 后续检索使用 SQL 时间排序代替向量相似度 + +2. **sqlite-vec 不可用**:降级为纯 SQL 查询(无向量检索) + - 短期记忆:取最近 N 条摘要(按时间排序) + - 长期记忆:取 access_count 最高的记忆(按访问计数排序) + +3. **硅基流动 API 不可用**:跳过向量检索,纯文本模式运行 + - 启动时检测 API 可用性(读取 config.toml 验证 api_key) + - 不可用时禁用向量功能,回退到 SQL 时间排序 + - 记录警告日志,不影响主功能 + +4. **存储失败**:保留 JSONL 作为备份 + - 初始化时如果 SQLite 失败,回退到 JSONLStore + - 提供迁移脚本 `cmd/migrate-jsonl-to-sqlite` + +5. **Token 预算超限**:优先保留工作记忆 + - 先削减长期记忆(降为 1 条) + - 再削减短期记忆(降为 1 条) + - 最后削减工作记忆(减少消息数量) + +6. **子 Agent 流式输出过大**:截断保护 + - 单条子 Agent 回复限制 10000 tokens + - 超出部分截断并标记 `[内容已截断]` + +## Testing Strategy + +1. **单元测试**: + - SQLiteStore CRUD 操作 + - VectorStore 相似度搜索 + - MemoryManager 三层检索 + +2. **集成测试**: + - 端到端消息存储和检索 + - 子 Agent 隔离验证 + - 向量检索准确性 + +3. **迁移测试**: + - JSONL → SQLite 数据迁移 + - 向后兼容性 + +## Memory Maintenance Strategy + +### 短期记忆生成 +- **触发时机**:每轮对话结束(Assistant 回复完成后) +- **生成方式**:调用 LLM 生成 1-2 句话摘要 +- **Prompt 示例**:"请用一句话总结本轮对话的关键信息,不超过 50 字:" +- **存储**:插入 `short_term_memories` 表 + +### 长期记忆生成 +- **触发时机**:会话结束或超过 20 轮对话 +- **生成方式**: + 1. 合并短期记忆摘要 + 2. 调用 LLM 提取关键事实、偏好、决策 + 3. 分类为 preference/fact/decision/project +- **去重**:与新长期记忆做文本相似度比较,相似度 > 0.8 则更新旧记录 + +### 记忆清理 +- **短期记忆**:保留最近 10 条,旧的自动删除 +- **长期记忆**:过期检查(`expires_at`),过期后降权而非删除 +- **访问计数**:长期记忆根据 `access_count` 排序,低频记忆逐步淘汰 + +## Open Questions + +1. **Embedding 模型选择**:硅基流动 Pro/BAAI/bge-m3,1024维,8K上下文 + - **配置**:通过 `SILICONFLOW_API_KEY` 环境变量认证 + - **模型ID**:`Pro/BAAI/bge-m3` + +2. **向量维度**:1024维,sqlite-vec 完全支持 + +3. **性能**:大量消息时向量检索性能如何?是否需要索引优化? + - **优化**:sqlite-vec 自动创建 IVF 索引,10 万条消息内性能良好 + +4. **Token 计数**:是否需要在 Go 端实现 tiktoken? + - **建议**:初期用简单估算(1 中文字 ≈ 1.5 tokens),后续引入 tiktoken + +5. **迁移策略**:是否强制迁移现有 JSONL? + - **建议**:不强制迁移,新会话使用 SQLite,旧会话仍从 JSONL 读取 diff --git a/thoughts/shared/plans/2025-05-11-orca-memory-plan.md b/thoughts/shared/plans/2025-05-11-orca-memory-plan.md new file mode 100644 index 0000000..5e4acc7 --- /dev/null +++ b/thoughts/shared/plans/2025-05-11-orca-memory-plan.md @@ -0,0 +1,593 @@ +# Orca 记忆系统实施计划 + +**基于设计文档**: `thoughts/shared/designs/2025-05-11-orca-memory-final-design.md` +**实施方式**: 增量升级(在已有 v1 基础上扩展) +**预估工期**: 5-7 天 + +--- + +## Phase 1: 数据库 Schema 扩展(第 1 天) + +### 1.1 新增 `memory_usage_log` 表 + +**文件**: `pkg/session/sqlite_store.go` — `initSchema()` 方法 + +```go +// 在 initSchema() 中添加 +schema += ` +CREATE TABLE IF NOT EXISTS memory_usage_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + memory_id INTEGER NOT NULL, + session_id TEXT NOT NULL, + query TEXT NOT NULL, + was_referenced INTEGER NOT NULL DEFAULT 0, + used_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (memory_id) REFERENCES long_term_memories(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_usage_memory ON memory_usage_log(memory_id); +CREATE INDEX IF NOT EXISTS idx_usage_session ON memory_usage_log(session_id); +` +``` + +**依赖**: 无 +**测试**: 验证表创建成功,外键约束生效 + +--- + +### 1.2 新增 `dialogue_buffer` 表 + +**文件**: `pkg/session/sqlite_store.go` — `initSchema()` 方法 + +```go +// 在 initSchema() 中添加 +schema += ` +CREATE TABLE IF NOT EXISTS dialogue_buffer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + user_query TEXT NOT NULL, + assistant_response TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_buffer_session ON dialogue_buffer(session_id, created_at); +` +``` + +**依赖**: 无 +**测试**: 验证表创建成功 + +--- + +### 1.3 扩展 `long_term_memories` 表 + +**文件**: `pkg/session/sqlite_store.go` + +当前表缺少 `weight`, `tags`, `archived` 字段。由于是 ALTER TABLE,需要小心处理: + +```go +// 在 initSchema() 中修改 long_term_memories 定义 +// 或添加迁移逻辑 +migrations := []string{ + `ALTER TABLE long_term_memories ADD COLUMN weight REAL NOT NULL DEFAULT 1.0`, + `ALTER TABLE long_term_memories ADD COLUMN tags TEXT`, + `ALTER TABLE long_term_memories ADD COLUMN archived INTEGER NOT NULL DEFAULT 0`, +} +``` + +**注意**: SQLite 支持 ADD COLUMN,但有限制(不能加 UNIQUE / PRIMARY KEY) + +**依赖**: 无 +**测试**: 验证迁移后数据完整性 + +--- + +## Phase 2: 配置扩展(第 1-2 天) + +### 2.1 扩展 Config 结构体 + +**文件**: `internal/config/config.go` + +```go +type Config struct { + // ... 现有字段 ... + + Memory MemoryConfig // 新增 + MemoryAgent MemoryAgentConfig // 新增 +} + +type MemoryConfig struct { + Enabled bool `toml:"enabled"` + MaxHistory int `toml:"max_history"` + + ShortTerm ShortTermConfig `toml:"short_term"` + LongTerm LongTermConfig `toml:"long_term"` + Inject InjectConfig `toml:"inject"` +} + +type ShortTermConfig struct { + MaxItems int `toml:"max_items"` + CompressionThreshold int `toml:"compression_threshold"` +} + +type LongTermConfig struct { + Enabled bool `toml:"enabled"` + VectorIndex bool `toml:"vector_index"` + MaxReturn int `toml:"max_return"` + ArchiveThreshold float64 `toml:"archive_threshold"` +} + +type InjectConfig struct { + FirstRoundEmpty bool `toml:"first_round_empty"` + ShortTermCount int `toml:"short_term_count"` + LongTermTrigger string `toml:"long_term_trigger"` + MinQueryLength int `toml:"min_query_length"` +} + +type MemoryAgentConfig struct { + Enabled bool `toml:"enabled"` + Provider string `toml:"provider"` + Model string `toml:"model"` + APIKey string `toml:"api_key"` + BaseURL string `toml:"base_url"` + Timeout string `toml:"timeout"` + Extract ExtractConfig `toml:"extract"` + Summarize SummarizeConfig `toml:"summarize"` +} + +type ExtractConfig struct { + BatchSize int `toml:"batch_size"` + MaxFacts int `toml:"max_facts"` + MinConfidence float64 `toml:"min_confidence"` + AutoTag bool `toml:"auto_tag"` +} + +type SummarizeConfig struct { + Enabled bool `toml:"enabled"` + TriggerTokens int `toml:"trigger_tokens"` +} +``` + +**依赖**: Phase 1 完成 +**测试**: 验证 TOML 解析正确 + +--- + +### 2.2 更新 config.toml.example + +**文件**: `config.toml.example` + +添加完整的 `[memory]` 和 `[memory_agent]` 示例配置。 + +**依赖**: 2.1 完成 + +--- + +## Phase 3: MemoryManager 扩展(第 2-3 天) + +### 3.1 添加权重管理方法 + +**文件**: `pkg/session/memory_manager.go` + +```go +// RecordMemoryUsage 记录记忆使用并更新权重 +func (mm *MemoryManager) RecordMemoryUsage(memoryID int64, sessionID, query string, referenced bool) error + +// UpdateMemoryWeight 更新单条记忆权重 +func (mm *MemoryManager) UpdateMemoryWeight(memoryID int64, delta float64) error + +// ArchiveLowWeightMemories 归档低权重记忆 +func (mm *MemoryManager) ArchiveLowWeightMemories(threshold float64) (int, error) + +// GetCoreMemories 获取核心记忆(高权重) +func (mm *MemoryManager) GetCoreMemories(minWeight float64) ([]LongTermMemory, error) + +// GetArchivedMemories 获取已归档记忆 +func (mm *MemoryManager) GetArchivedMemories() ([]LongTermMemory, error) +``` + +**依赖**: Phase 1, Phase 2 +**测试**: 验证权重计算正确,归档逻辑生效 + +--- + +### 3.2 添加对话缓冲管理 + +**文件**: `pkg/session/memory_manager.go` + +```go +// BufferDialogue 缓冲对话 +func (mm *MemoryManager) BufferDialogue(sessionID, userQuery, assistantResponse string) error + +// GetBufferedDialogues 获取缓冲的对话 +func (mm *MemoryManager) GetBufferedDialogues(sessionID string, limit int) ([]Dialogue, error) + +// ClearBuffer 清空缓冲 +func (mm *MemoryManager) ClearBuffer(sessionID string) error + +// ShouldExtract 检查是否触发提取 +func (mm *MemoryManager) ShouldExtract(sessionID string) (bool, error) +``` + +**依赖**: Phase 1 +**测试**: 验证缓冲写入、读取、清空 + +--- + +### 3.3 增强记忆注入逻辑 + +**文件**: `pkg/session/memory_manager.go` + +```go +// ShouldInjectMemory 判断是否注入记忆 +func (mm *MemoryManager) ShouldInjectMemory(query string, msgCount int) bool + +// BuildMemoryContextWithStats 构建记忆上下文(增强版) +// 现有方法需要增强:支持 first_round_empty, min_query_length +``` + +修改 `BuildMemoryContextWithStats` 支持配置参数: +- `first_round_empty`: msgCount == 0 时返回空 +- `min_query_length`: len(query) < threshold 时返回空 +- `long_term_trigger`: 检测技术关键词 + +**依赖**: Phase 2 +**测试**: 验证各种注入条件 + +--- + +### 3.4 添加长期记忆写入 + +**文件**: `pkg/session/memory_manager.go` + +```go +// AddLongTermMemoryWithEmbedding 添加长期记忆并生成向量 +func (mm *MemoryManager) AddLongTermMemoryWithEmbedding(content, memoryType string, tags []string, confidence float64) error + +// UpdateLongTermMemory 更新长期记忆 +func (mm *MemoryManager) UpdateLongTermMemory(id int64, updates map[string]interface{}) error +``` + +**依赖**: Phase 1 +**测试**: 验证写入 + 向量生成 + +--- + +## Phase 4: MemoryExtractorAgent(第 3-4 天) + +### 4.1 创建 Agent Prompt 文件 + +**文件**: `~/.orca/agents/memory_extractor.md` + +内容来自设计文档,需要安装到用户目录。 + +**文件**: `pkg/actor/memory_extractor_prompt.go`(内嵌默认 prompt,作为 fallback) + +```go +package actor + +const DefaultMemoryExtractorPrompt = `...` +``` + +**依赖**: 无 +**测试**: 验证 prompt 加载 + +--- + +### 4.2 实现 MemoryExtractorAgent + +**文件**: `pkg/actor/memory_extractor_agent.go` + +```go +package actor + +type MemoryExtractorAgent struct { + *SubAgent + config MemoryAgentConfig +} + +// NewMemoryExtractorAgent 创建提取 Agent +func NewMemoryExtractorAgent(id string, llmBackend llm.LLM, cfg MemoryAgentConfig) (*MemoryExtractorAgent, error) + +// ExtractFacts 从对话中提取事实 +func (mea *MemoryExtractorAgent) ExtractFacts(dialogues []Dialogue) ([]Fact, error) + +// parseFactJSON 解析提取结果 +func parseFactJSON(content string) ([]Fact, error) +``` + +**类型定义**: +```go +type Dialogue struct { + UserQuery string + AssistantResponse string +} + +type Fact struct { + Content string + Type string // fact/preference/project + Tags []string + Confidence float64 + Replace *string // 被替换的旧事实 +} +``` + +**依赖**: Phase 2, Phase 3 +**测试**: 验证提取逻辑,JSON 解析 + +--- + +### 4.3 集成到 Kernel + +**文件**: `pkg/kernel/kernel.go` + +在 Kernel 初始化时创建 MemoryExtractorAgent: + +```go +// 在 NewWithConfig() 中 +if cfg.MemoryAgent.Enabled { + kernel.memoryExtractor, err = actor.NewMemoryExtractorAgent( + "memory_extractor", + kernel.llmBackend, + cfg.MemoryAgent, + ) +} +``` + +**依赖**: 4.2 +**测试**: 验证 Kernel 启动 + +--- + +## Phase 5: LLMAgent 集成(第 4-5 天) + +### 5.1 修改 buildLLMMessages + +**文件**: `pkg/actor/llm_agent.go` + +增强 `buildLLMMessages()` 方法: + +```go +func (a *LLMAgent) buildLLMMessages(query string) []llm.Message { + messages := make([]llm.Message, 0) + + // 1. System prompt(原有) + if a.systemPrompt != "" { + messages = append(messages, llm.Message{Role: "system", Content: a.systemPrompt}) + } + + // 2. Tool prompt(原有) + if toolPrompt := a.buildToolPrompt(); toolPrompt != "" { + messages = append(messages, llm.Message{Role: "system", Content: toolPrompt}) + } + + // 3. 记忆注入(增强) + if a.memoryManager != nil { + msgCount := a.sessionMgr.GetMessageCount(a.sessionID) + if a.memoryManager.ShouldInjectMemory(query, msgCount) { + ctx, stats := a.memoryManager.BuildMemoryContextWithStats(a.sessionID, query) + if ctx != "" { + messages = append(messages, llm.Message{Role: "system", Content: ctx}) + log.Printf("[memory] Injected: short=%d, long=%d, tokens=%d", + stats.ShortTermCount, stats.LongTermCount, stats.TotalTokens) + } + } + } + + // 4. 对话历史(原有) + // ... + + return messages +} +``` + +**依赖**: Phase 3 +**测试**: 验证注入条件,Token 预算 + +--- + +### 5.2 修改消息处理流程 + +**文件**: `pkg/actor/llm_agent.go` + +在消息保存后添加记忆维护: + +```go +func (a *LLMAgent) handleUserMessage(ctx context.Context, msg Message) error { + // ... 原有逻辑 ... + + // 保存消息(原有) + a.sessionMgr.SaveMessage(...) + + // 记忆维护(新增) + if a.memoryManager != nil { + a.memoryManager.MaintainSessionMemory(a.sessionID, query, response) + } + + return nil +} +``` + +**依赖**: Phase 3 +**测试**: 验证 STM 生成,缓冲写入 + +--- + +### 5.3 添加异步提取触发 + +**文件**: `pkg/actor/llm_agent.go` 或 `pkg/session/memory_manager.go` + +```go +// triggerExtraction 异步触发事实提取 +func (mm *MemoryManager) triggerExtraction(sessionID string) { + dialogues, _ := mm.GetBufferedDialogues(sessionID, mm.config.Extract.BatchSize) + + go func() { + // 调用 MemoryExtractorAgent + facts, err := mm.extractor.ExtractFacts(dialogues) + if err != nil { + log.Printf("[memory] Extraction failed: %v", err) + return + } + + for _, fact := range facts { + if fact.Confidence >= mm.config.Extract.MinConfidence { + mm.AddLongTermMemoryWithEmbedding(...) + } + } + + mm.ClearBuffer(sessionID) + }() +} +``` + +**依赖**: Phase 4 +**测试**: 验证异步执行,错误处理 + +--- + +## Phase 6: 权重反馈系统(第 5 天) + +### 6.1 记忆引用检测 + +**文件**: `pkg/session/memory_manager.go` + +```go +// DetectMemoryReference 检测 Agent 回复是否引用了某条记忆 +func (mm *MemoryManager) DetectMemoryReference(response string, memories []LongTermMemory) []int64 { + // 简单实现:检查记忆中关键词是否出现在回复中 + // 高级实现:使用 Embedding 相似度 +} +``` + +**依赖**: Phase 3 +**测试**: 验证检测准确率 + +--- + +### 6.2 自动归档任务 + +**文件**: `pkg/session/memory_manager.go` + +```go +// RunMaintenance 运行记忆维护任务 +func (mm *MemoryManager) RunMaintenance() error { + // 1. 归档低权重记忆 + mm.ArchiveLowWeightMemories(mm.config.LongTerm.ArchiveThreshold) + + // 2. 清理过期 STM + mm.Cleanup() + + // 3. 其他维护... +} +``` + +**依赖**: Phase 3 +**测试**: 验证归档逻辑 + +--- + +## Phase 7: CLI 命令(第 5-6 天) + +### 7.1 添加 CLI 命令 + +**文件**: `cmd/orca/main.go` 或新建 `cmd/orca/memory.go` + +```go +// orca memory list +// orca memory search "query" +// orca memory delete +// orca memory clean +// orca memory stats +// orca memory export > backup.json +// orca memory import < backup.json +``` + +**依赖**: Phase 3 +**测试**: 验证命令执行 + +--- + +## Phase 8: 测试与优化(第 6-7 天) + +### 8.1 单元测试 + +| 模块 | 测试项 | 文件 | +|------|--------|------| +| Schema | 表创建、迁移 | `pkg/session/sqlite_store_test.go` | +| Config | TOML 解析 | `internal/config/config_test.go` | +| Weight | 权重计算、归档 | `pkg/session/memory_manager_test.go` | +| Buffer | 缓冲写入、清空 | `pkg/session/memory_manager_test.go` | +| Extractor | 事实提取、JSON 解析 | `pkg/actor/memory_extractor_test.go` | +| Injection | 注入条件、Token 预算 | `pkg/actor/llm_agent_test.go` | + +### 8.2 集成测试 + +```go +// test_memory_system_v2.go +func TestMemorySystemV2(t *testing.T) { + // 1. 创建会话 + // 2. 发送消息(验证 STM 生成) + // 3. 发送 5 条消息(验证 LTM 提取触发) + // 4. 查询长期记忆(验证向量检索) + // 5. 验证权重变化 + // 6. 验证归档逻辑 +} +``` + +### 8.3 性能测试 + +| 指标 | 目标 | 测试方法 | +|------|------|---------| +| 向量检索 | < 100ms | BenchmarkVectorSearch | +| Embedding 缓存 | > 95% 命中 | BenchmarkEmbeddingCache | +| 权重更新 | < 10ms | BenchmarkWeightUpdate | +| 整体延迟 | < 200ms | BenchmarkMemoryInjection | + +--- + +## 依赖图 + +``` +Phase 1 (Schema) + │ + ├──→ Phase 2 (Config) + │ │ + │ ├──→ Phase 3 (MemoryManager) + │ │ │ + │ │ ├──→ Phase 4 (ExtractorAgent) + │ │ │ │ + │ │ │ └──→ Phase 5 (LLMAgent) + │ │ │ + │ │ └──→ Phase 6 (Weight System) + │ │ + │ └──→ Phase 7 (CLI) + │ + └──→ Phase 8 (Test) +``` + +--- + +## 风险与缓解 + +| 风险 | 可能性 | 影响 | 缓解 | +|------|--------|------|------| +| Schema 迁移失败 | 中 | 高 | 备份数据库,测试迁移脚本 | +| MemoryExtractor 幻觉 | 高 | 中 | 高置信度阈值,人工抽查 | +| 权重系统不准确 | 中 | 低 | 提供手动调整 CLI | +| 性能退化 | 低 | 中 | 监控,及时优化索引 | +| 向后兼容 | 中 | 高 | 保留旧配置解析逻辑 | + +--- + +## 验收标准 + +- [ ] 所有新增表创建成功 +- [ ] 配置解析正确,默认值合理 +- [ ] STM 自动生成(每轮对话) +- [ ] LTM 批量提取(每 5 轮) +- [ ] 向量检索正常(语义相似度) +- [ ] 权重自动更新(引用检测) +- [ ] 低权重记忆自动归档 +- [ ] CLI 命令可用 +- [ ] 单元测试覆盖率 > 80% +- [ ] 集成测试通过 +- [ ] 性能指标达标 diff --git a/web-dev.log b/web-dev.log new file mode 100644 index 0000000..2e7196a --- /dev/null +++ b/web-dev.log @@ -0,0 +1,71 @@ + +> web@0.0.0 dev +> vite + + + VITE v8.0.11 ready in 215 ms + + ➜ Local: http://localhost:5173/ + ➜ Network: use --host to expose +19:50:41 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +19:50:41 [vite] (client) hmr invalidate /src/contexts/WebSocketContext.tsx Could not Fast Refresh ("useWebSocket" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports +19:50:41 [vite] (client) hmr update /src/App.tsx, /src/components/Header.tsx, /src/components/ChatPanel.tsx, /src/components/Sidebar.tsx, /src/components/SessionList.tsx +19:52:24 [vite] (client) hmr update /src/components/ChatPanel.tsx, /src/index.css +19:54:20 [vite] (client) hmr update /src/index.css, /src/components/TaskCard.tsx +19:55:32 [vite] (client) hmr update /src/index.css, /src/components/TaskCard.tsx +19:56:24 [vite] (client) hmr update /src/index.css, /src/components/TaskCard.tsx +19:57:09 [vite] (client) hmr update /src/index.css, /src/components/TaskModal.tsx +19:58:18 [vite] (client) hmr update /src/index.css, /src/components/TaskCard.tsx +19:58:41 [vite] (client) hmr update /src/components/ChatPanel.tsx, /src/index.css +19:59:44 [vite] (client) hmr update /src/index.css, /src/components/TaskCard.tsx +20:00:00 [vite] (client) hmr update /src/components/ChatPanel.tsx, /src/index.css +20:00:19 [vite] http proxy error: /api/sessions +AggregateError [ECONNREFUSED]: + at internalConnectMultiple (node:net:1122:18) + at afterConnectMultiple (node:net:1689:7) +20:00:19 [vite] http proxy error: /api/sessions +AggregateError [ECONNREFUSED]: + at internalConnectMultiple (node:net:1122:18) + at afterConnectMultiple (node:net:1689:7) +20:05:13 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +20:05:13 [vite] Internal server error: Transform failed with 1 error: + +[PARSE_ERROR] Error: Identifier `currentStreamingContentRef` has already been declared + ╭─[ src/contexts/WebSocketContext.tsx:66:9 ] + │ +  66 │   const currentStreamingContentRef = useRef(""); +  │ ─────────────┬──────────── +  │ ╰────────────── `currentStreamingContentRef` has already been declared here +  │ + 321 │   const currentStreamingContentRef = useRef(currentStreamingContent); +  │ ─────────────┬──────────── +  │ ╰────────────── It can not be redeclared here +─────╯ + + Plugin: vite:oxc + File: /Users/wang/agent_dev/orca.ai/web/src/contexts/WebSocketContext.tsx + at transformWithOxc (file:///Users/wang/agent_dev/orca.ai/web/node_modules/vite/dist/node/chunks/node.js:3340:19) + at TransformPluginContext.transform (file:///Users/wang/agent_dev/orca.ai/web/node_modules/vite/dist/node/chunks/node.js:3408:26) + at EnvironmentPluginContainer.transform (file:///Users/wang/agent_dev/orca.ai/web/node_modules/vite/dist/node/chunks/node.js:30179:51) + at async loadAndTransform (file:///Users/wang/agent_dev/orca.ai/web/node_modules/vite/dist/node/chunks/node.js:24509:26) + at async viteTransformMiddleware (file:///Users/wang/agent_dev/orca.ai/web/node_modules/vite/dist/node/chunks/node.js:24303:20) +20:05:21 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +20:05:21 [vite] (client) hmr invalidate /src/contexts/WebSocketContext.tsx Could not Fast Refresh ("useWebSocket" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports +20:05:21 [vite] (client) hmr update /src/App.tsx, /src/components/Header.tsx, /src/components/ChatPanel.tsx, /src/components/Sidebar.tsx, /src/components/SessionList.tsx +20:05:47 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +20:05:47 [vite] (client) hmr invalidate /src/contexts/WebSocketContext.tsx Could not Fast Refresh ("useWebSocket" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports +20:05:47 [vite] (client) hmr update /src/App.tsx, /src/components/Header.tsx, /src/components/ChatPanel.tsx, /src/components/Sidebar.tsx, /src/components/SessionList.tsx +20:05:55 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +20:05:55 [vite] (client) hmr invalidate /src/contexts/WebSocketContext.tsx Could not Fast Refresh ("useWebSocket" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports +20:05:55 [vite] (client) hmr update /src/App.tsx, /src/components/Header.tsx, /src/components/ChatPanel.tsx, /src/components/Sidebar.tsx, /src/components/SessionList.tsx +20:06:11 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +20:06:11 [vite] (client) hmr invalidate /src/contexts/WebSocketContext.tsx Could not Fast Refresh ("useWebSocket" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports +20:06:11 [vite] (client) hmr update /src/App.tsx, /src/components/Header.tsx, /src/components/ChatPanel.tsx, /src/components/Sidebar.tsx, /src/components/SessionList.tsx +20:06:25 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +20:06:25 [vite] (client) hmr invalidate /src/contexts/WebSocketContext.tsx Could not Fast Refresh ("useWebSocket" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports +20:06:25 [vite] (client) hmr update /src/App.tsx, /src/components/Header.tsx, /src/components/ChatPanel.tsx, /src/components/Sidebar.tsx, /src/components/SessionList.tsx +20:06:40 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +20:06:41 [vite] (client) hmr invalidate /src/contexts/WebSocketContext.tsx Could not Fast Refresh ("useWebSocket" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports +20:06:41 [vite] (client) hmr update /src/App.tsx, /src/components/Header.tsx, /src/components/ChatPanel.tsx, /src/components/Sidebar.tsx, /src/components/SessionList.tsx +20:10:11 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css +20:14:41 [vite] (client) hmr update /src/contexts/WebSocketContext.tsx, /src/index.css diff --git a/web-server.log b/web-server.log new file mode 100644 index 0000000..0a4188c --- /dev/null +++ b/web-server.log @@ -0,0 +1,12 @@ +2026/05/10 18:19:53 kernel: created DeepSeek client (model=deepseek-v4-flash) +2026/05/10 18:19:53 kernel: created sub-agent "architect" from /Users/wang/.orca/prompts/architect.md +2026/05/10 18:19:53 kernel: created sub-agent "calculator" from /Users/wang/.orca/prompts/calculator.md +2026/05/10 18:19:53 kernel: created sub-agent "coder" from /Users/wang/.orca/prompts/coder.md +2026/05/10 18:19:53 kernel: created sub-agent "reviewer" from /Users/wang/.orca/prompts/reviewer.md +2026/05/10 18:19:53 kernel: created 4 sub-agents +2026/05/10 18:19:53 kernel: started (tools=6) +2026/05/10 18:19:53 kernel: warning: skill loading had errors: skill: loaded 8 skills with 1 errors: art-design-pro: skill: "/Users/wang/.agents/skills/art-design-pro/SKILL.md" is missing 'name' in frontmatter +2026/05/10 18:19:53 kernel: loaded 8 skills +Starting web server on http://localhost:8081 +2026/05/10 18:19:53 WebSocket server starting on http://localhost:8081 +2026/05/10 18:20:21 WebSocket error: websocket: close 1005 (no status) diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/web/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..b516fe7 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + orca.agent + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..82026da --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,4785 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/postcss": "^4.3.0", + "@types/react-syntax-highlighter": "^15.5.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.14.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^16.1.1", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "postcss": "^8.5.14", + "tailwindcss": "^4.3.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10", + "ws": "^8.20.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.128.0", + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.128.0.tgz", + "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", + "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", + "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.3.0.tgz", + "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "postcss": "^8.5.10", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.3", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.3.tgz", + "integrity": "sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmmirror.com/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmmirror.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/type-utils": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.2", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", + "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.3.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmmirror.com/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "16.1.1", + "resolved": "https://registry.npmmirror.com/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmmirror.com/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmmirror.com/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.18.tgz", + "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.128.0", + "@rolldown/pluginutils": "1.0.0-rc.18" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-x64": "1.0.0-rc.18", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT", + "peer": true + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.2", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz", + "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.2", + "@typescript-eslint/parser": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.0.11", + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.0.11.tgz", + "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0-rc.18", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..a11e685 --- /dev/null +++ b/web/package.json @@ -0,0 +1,44 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.3.0", + "@types/react-syntax-highlighter": "^15.5.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.14.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^16.1.1", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "postcss": "^8.5.14", + "tailwindcss": "^4.3.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10", + "ws": "^8.20.0" + } +} diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..db3c3d1 --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +} \ No newline at end of file diff --git a/web/public/favicon.svg b/web/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/web/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/icons.svg b/web/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/web/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/App.css b/web/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/web/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..c3d6635 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,43 @@ +import { useState, useEffect } from "react"; +import { WebSocketProvider } from "./contexts/WebSocketContext"; +import { Header } from "./components/Header"; +import { ChatPanel } from "./components/ChatPanel"; +import { Sidebar } from "./components/Sidebar"; + +function App() { + const [isDarkMode, setIsDarkMode] = useState(() => { + const saved = localStorage.getItem("orca-theme"); + if (saved) return saved === "dark"; + return window.matchMedia("(prefers-color-scheme: dark)").matches; + }); + + useEffect(() => { + if (isDarkMode) { + document.documentElement.classList.add("dark"); + localStorage.setItem("orca-theme", "dark"); + } else { + document.documentElement.classList.remove("dark"); + localStorage.setItem("orca-theme", "light"); + } + }, [isDarkMode]); + + const toggleTheme = () => { + setIsDarkMode(!isDarkMode); + }; + + return ( + +
+
+
+
+ +
+ +
+
+
+ ); +} + +export default App; diff --git a/web/src/assets/hero.png b/web/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/web/src/assets/hero.png differ diff --git a/web/src/assets/react.svg b/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/assets/vite.svg b/web/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/web/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/web/src/components/ChatPanel.tsx b/web/src/components/ChatPanel.tsx new file mode 100644 index 0000000..452fd4a --- /dev/null +++ b/web/src/components/ChatPanel.tsx @@ -0,0 +1,246 @@ +import React, { useRef, useEffect, useState } from "react"; +import { Send, User, AlertCircle, Loader2 } from "lucide-react"; +import { useWebSocket } from "../contexts/WebSocketContext"; +import type { Task } from "../contexts/WebSocketContext"; +import { StreamingMarkdown } from "./StreamingMarkdown"; +import { TaskCard } from "./TaskCard"; +import { TaskModal } from "./TaskModal"; + +interface ContentPart { + type: "text" | "agent_call"; + content: string; + agent?: string; + task?: string; +} + +function parseMessageContent(text: string): ContentPart[] { + const parts: ContentPart[] = []; + let lastIndex = 0; + + const arrayRegex = /(\[\s*\{[\s\S]*?"tool"\s*:\s*"agent_call"[\s\S]*?\}\s*\])/g; + let match; + + while ((match = arrayRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push({ type: "text", content: text.slice(lastIndex, match.index) }); + } + + try { + const arr = JSON.parse(match[1]); + if (Array.isArray(arr)) { + for (const item of arr) { + if (item.tool === "agent_call" && item.arguments) { + parts.push({ + type: "agent_call", + content: match[1], + agent: item.arguments.agent || "unknown", + task: item.arguments.task || item.arguments.prompt || "执行任务", + }); + } + } + } + } catch { + parts.push({ type: "text", content: match[0] }); + } + + lastIndex = match.index + match[0].length; + } + + if (lastIndex < text.length) { + parts.push({ type: "text", content: text.slice(lastIndex) }); + } + + if (parts.length === 0) { + parts.push({ type: "text", content: text }); + } + + return parts; +} + +function findTaskForAgentCall(tasks: Task[], agent: string, taskDesc: string): Task | undefined { + return tasks.find( + (t) => + t.agent === agent && + (t.task === taskDesc || taskDesc.includes(t.task) || t.task.includes(taskDesc)) + ); +} + +export const ChatPanel: React.FC = () => { + const { messages, isLoading, sendMessage, currentStreamingContent, tasks } = useWebSocket(); + const [selectedTask, setSelectedTask] = useState(null); + const scrollRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages, currentStreamingContent]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const value = inputRef.current?.value || ""; + if (!value.trim() || isLoading) return; + sendMessage(value.trim()); + if (inputRef.current) { + inputRef.current.value = ""; + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + const streamingMessage = isLoading + ? { + id: "streaming", + role: "assistant" as const, + content: currentStreamingContent, + timestamp: Date.now(), + agent: undefined, + } + : null; + + const displayMessages = streamingMessage + ? [...messages, streamingMessage] + : messages; + + const renderMessageContent = (content: string, isStreaming: boolean) => { + const parts = parseMessageContent(content); + + return ( +
+ {parts.map((part, idx) => { + if (part.type === "agent_call" && part.agent && part.task) { + const task = findTaskForAgentCall(tasks, part.agent, part.task); + return ( + + task + ? setSelectedTask(task) + : setSelectedTask({ + id: `inline-${idx}`, + agent: part.agent || "unknown", + task: part.task || "", + status: "completed", + content: part.content || "", + startTime: Date.now(), + }) + } + /> + ); + } else { + return ( + + ); + } + })} +
+ ); + }; + + return ( +
+
+ {displayMessages.length === 0 && ( +
+
+

开始与 agent 对话

+
+
+ )} + +
+
+ {displayMessages.map((msg) => ( +
+
+ {msg.role === "user" ? ( + <> + + You + + ) : msg.role === "system" ? ( + <> + + System + + ) : ( + <> +
+ AI +
+ + {msg.agent || "Assistant"} + + + )} +
+ +
+ {msg.role === "assistant" && msg.id === "streaming" ? ( + msg.content.trim().length > 0 ? ( + renderMessageContent(msg.content, true) + ) : ( +
+ + AI 正在思考... +
+ ) + ) : msg.role === "assistant" ? ( + renderMessageContent(msg.content, false) + ) : ( +

{msg.content}

+ )} +
+
+ ))} +
+
+
+ +
+
+