feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard
All checks were successful
Beta Release / beta (push) Successful in 2m24s
All checks were successful
Beta Release / beta (push) Successful in 2m24s
Major changes: - Refactor CLI entry point to Cobra commands (root, setup, scan, doctor, install, update, lsp, mcp, skills, config, version) - Add LSP registry with health checks, auto-install, and editor config generation - Add MCP registry with editor detection, status tracking, and per-editor configuration - Add workflow engine with planner and step execution for automated task chains - Add conversation search, export (Markdown/JSON), and detailed token counting - Add streaming shell chat handler with tool call/result events - Add skill validation, dry-run testing, and export endpoints - Enrich dashboard with Tools/Activity/Status tabs and tool cards grid - Add PRD documentation - Complete i18n for both EN and FR 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/muyue/muyue/internal/lsp"
|
||||
"github.com/muyue/muyue/internal/mcp"
|
||||
@@ -95,9 +97,14 @@ func (s *Server) handleLSP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) {
|
||||
servers := mcp.ScanServers()
|
||||
home, _ := os.UserHomeDir()
|
||||
editors := mcp.DetectInstalledEditors(home)
|
||||
statuses := mcp.GetAllStatuses()
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"servers": servers,
|
||||
"configured": true,
|
||||
"servers": servers,
|
||||
"configured": true,
|
||||
"detected_editors": editors,
|
||||
"statuses": statuses,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,11 +113,297 @@ func (s *Server) handleMCPConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := mcp.ConfigureAll(s.config); err != nil {
|
||||
|
||||
var body struct {
|
||||
Editor string `json:"editor,omitempty"`
|
||||
}
|
||||
if r.Body != nil {
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
}
|
||||
|
||||
if body.Editor != "" {
|
||||
if err := mcp.ConfigureForEditor(s.config, body.Editor); err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := mcp.ConfigureAll(s.config); err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleMCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
statuses := mcp.GetAllStatuses()
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"statuses": statuses,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleMCPRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
reg, err := mcp.LoadRegistry()
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"registry": reg,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleLSPHealth(w http.ResponseWriter, r *http.Request) {
|
||||
servers := lsp.ScanServers()
|
||||
type healthInfo struct {
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Installed bool `json:"installed"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
var results []healthInfo
|
||||
for _, srv := range servers {
|
||||
healthy, detail := lsp.HealthCheck(srv.Name)
|
||||
results = append(results, healthInfo{
|
||||
Name: srv.Name,
|
||||
Language: srv.Language,
|
||||
Installed: srv.Installed,
|
||||
Healthy: healthy,
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"servers": results,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleLSPAutoInstall(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ProjectDir string `json:"project_dir,omitempty"`
|
||||
}
|
||||
if r.Body != nil {
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
}
|
||||
|
||||
if body.ProjectDir == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
body.ProjectDir = home
|
||||
}
|
||||
|
||||
results, err := lsp.AutoInstallForProject(body.ProjectDir)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"results": results,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleLSPEditorConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Editor string `json:"editor"`
|
||||
Names []string `json:"names,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
allServers := lsp.ScanServers()
|
||||
var selected []lsp.LSPServer
|
||||
if len(body.Names) > 0 {
|
||||
nameSet := map[string]bool{}
|
||||
for _, n := range body.Names {
|
||||
nameSet[n] = true
|
||||
}
|
||||
for _, srv := range allServers {
|
||||
if nameSet[srv.Name] {
|
||||
selected = append(selected, srv)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, srv := range allServers {
|
||||
if srv.Installed {
|
||||
selected = append(selected, srv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config, err := lsp.GenerateEditorConfigs(selected, body.Editor, "")
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"editor": body.Editor,
|
||||
"config": config,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleSkillValidate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
skill, err := skills.Get(body.Name)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
errs := skills.Validate(skill)
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"name": body.Name,
|
||||
"valid": len(errs) == 0,
|
||||
"errors": errs,
|
||||
"dependencies": skills.CheckDependencies(skill),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleSkillTest(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
SampleTask string `json:"sample_task,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
result := skills.DryRun(body.Name, body.SampleTask)
|
||||
writeJSON(w, result)
|
||||
}
|
||||
|
||||
func (s *Server) handleSkillExport(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
ExportPath string `json:"export_path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
if body.ExportPath == "" {
|
||||
body.ExportPath = home + "/.muyue/exports/" + body.Name + ".md"
|
||||
}
|
||||
|
||||
if err := skills.Export(body.Name, body.ExportPath); err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "ok", "path": body.ExportPath})
|
||||
}
|
||||
|
||||
func (s *Server) handleSkillImport(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ImportPath string `json:"import_path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
skill, err := skills.Import(body.ImportPath)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := skills.Create(skill); err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"status": "ok", "skill": skill.Name})
|
||||
}
|
||||
|
||||
func (s *Server) handleDashboardStatus(w http.ResponseWriter, r *http.Request) {
|
||||
mcpStatuses := mcp.GetAllStatuses()
|
||||
lspServers := lsp.ScanServers()
|
||||
skillList, _ := skills.List()
|
||||
|
||||
mcpHealthy := 0
|
||||
mcpTotal := len(mcpStatuses)
|
||||
for _, st := range mcpStatuses {
|
||||
if st.Healthy {
|
||||
mcpHealthy++
|
||||
}
|
||||
}
|
||||
|
||||
lspInstalled := 0
|
||||
lspTotal := len(lspServers)
|
||||
for _, srv := range lspServers {
|
||||
if srv.Installed {
|
||||
lspInstalled++
|
||||
}
|
||||
}
|
||||
|
||||
skillsDeployed := len(skillList)
|
||||
var skillIssues []string
|
||||
for _, sk := range skillList {
|
||||
missing := skills.CheckDependencies(&sk)
|
||||
if len(missing) > 0 {
|
||||
for _, dep := range missing {
|
||||
skillIssues = append(skillIssues, sk.Name+": missing "+dep.Type+" "+dep.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"mcp": map[string]interface{}{
|
||||
"total": mcpTotal,
|
||||
"healthy": mcpHealthy,
|
||||
"servers": mcpStatuses,
|
||||
},
|
||||
"lsp": map[string]interface{}{
|
||||
"total": lspTotal,
|
||||
"installed": lspInstalled,
|
||||
"servers": lspServers,
|
||||
},
|
||||
"skills": map[string]interface{}{
|
||||
"total": skillsDeployed,
|
||||
"issues": skillIssues,
|
||||
"deployed": skillList,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user