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>
418 lines
9.9 KiB
Go
418 lines
9.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/muyue/muyue/internal/lsp"
|
|
"github.com/muyue/muyue/internal/mcp"
|
|
"github.com/muyue/muyue/internal/scanner"
|
|
"github.com/muyue/muyue/internal/skills"
|
|
"github.com/muyue/muyue/internal/version"
|
|
)
|
|
|
|
func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, map[string]interface{}{
|
|
"name": version.Name,
|
|
"version": version.Version,
|
|
"author": version.Author,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) {
|
|
if s.scanResult == nil {
|
|
s.scanResult = scanner.ScanSystem()
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"system": s.scanResult.System,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleTools(w http.ResponseWriter, r *http.Request) {
|
|
if s.scanResult == nil {
|
|
s.scanResult = scanner.ScanSystem()
|
|
}
|
|
type toolInfo struct {
|
|
Name string `json:"name"`
|
|
Installed bool `json:"installed"`
|
|
Version string `json:"version"`
|
|
Path string `json:"path"`
|
|
}
|
|
tools := make([]toolInfo, len(s.scanResult.Tools))
|
|
for i, t := range s.scanResult.Tools {
|
|
tools[i] = toolInfo{
|
|
Name: t.Name,
|
|
Installed: t.Installed,
|
|
Version: t.Version,
|
|
Path: t.Path,
|
|
}
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"tools": tools,
|
|
"total": len(tools),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|
if s.config == nil {
|
|
writeError(w, "no config", http.StatusNotFound)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"profile": s.config.Profile,
|
|
"terminal": s.config.Terminal,
|
|
"bmad": s.config.BMAD,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) {
|
|
if s.config == nil {
|
|
writeError(w, "no config", http.StatusNotFound)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"providers": s.config.AI.Providers,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleSkills(w http.ResponseWriter, r *http.Request) {
|
|
list, err := skills.List()
|
|
if err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"skills": list,
|
|
"count": len(list),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleLSP(w http.ResponseWriter, r *http.Request) {
|
|
servers := lsp.ScanServers()
|
|
writeJSON(w, map[string]interface{}{
|
|
"servers": servers,
|
|
})
|
|
}
|
|
|
|
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,
|
|
"detected_editors": editors,
|
|
"statuses": statuses,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleMCPConfigure(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
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]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) {
|
|
s.scanResult = scanner.ScanSystem()
|
|
writeJSON(w, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (s *Server) handleEditors(w http.ResponseWriter, r *http.Request) {
|
|
editors := scanner.ScanEditors()
|
|
writeJSON(w, map[string]interface{}{"editors": editors})
|
|
}
|