package api import ( "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "regexp" "strings" "time" "github.com/muyue/muyue/internal/agent" "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, "sudo": !agent.NeedsSudoPassword(), }) } 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}) } func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) { type providerQuota struct { Name string `json:"name"` Active bool `json:"active"` Healthy bool `json:"healthy"` Data map[string]interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } var results []providerQuota client := &http.Client{Timeout: 8 * time.Second} for _, p := range s.config.AI.Providers { q := providerQuota{Name: p.Name, Active: p.Active} switch p.Name { case "minimax": if p.APIKey == "" { q.Error = "no API key" results = append(results, q) continue } req, _ := http.NewRequest("GET", "https://api.minimax.io/v1/token_plan/remains", nil) req.Header.Set("Authorization", "Bearer "+p.APIKey) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { q.Error = err.Error() } else { body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} if json.Unmarshal(body, &data) == nil { if models, ok := data["model_remains"].([]interface{}); ok { filtered := make([]map[string]interface{}, 0) for _, m := range models { if mm, ok := m.(map[string]interface{}); ok { usage, _ := mm["current_interval_usage_count"].(float64) total, _ := mm["current_interval_total_count"].(float64) if total > 0 { filtered = append(filtered, map[string]interface{}{ "model": mm["model_name"], "used": usage, "total": total, "remaining": total - usage, "weekly_used": mm["current_weekly_usage_count"], "weekly_total": mm["current_weekly_total_count"], }) } } } q.Data = map[string]interface{}{"models": filtered} q.Healthy = true } } } case "zai": if p.APIKey == "" { q.Error = "no API key" results = append(results, q) continue } req, _ := http.NewRequest("GET", "https://api.z.ai/api/monitor/usage/quota/limit", nil) req.Header.Set("Authorization", "Bearer "+p.APIKey) req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { q.Error = err.Error() } else { body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} if json.Unmarshal(body, &data) == nil { if d, ok := data["data"].(map[string]interface{}); ok { if limits, ok := d["limits"].([]interface{}); ok { models := make([]map[string]interface{}, 0) for _, l := range limits { if lm, ok := l.(map[string]interface{}); ok { name := "Z.AI" if model, ok := lm["model"].(string); ok && model != "" { name = model } else if t, ok := lm["type"].(string); ok && t != "TIME_LIMIT" { name = t } usage, _ := lm["usage"].(float64) remaining, _ := lm["remaining"].(float64) limitVal, hasLimit := lm["limit"].(float64) total := usage + remaining if hasLimit && limitVal > 0 { total = limitVal } if total > 0 { models = append(models, map[string]interface{}{ "model": name, "used": usage, "total": total, "remaining": remaining, }) } } } if len(models) > 0 { q.Data = map[string]interface{}{"models": models} q.Healthy = true } } } } } case "mimo": q.Healthy = p.APIKey != "" if p.APIKey == "" { q.Error = "no API key" results = append(results, q) continue } mimoBase := p.BaseURL if mimoBase == "" { mimoBase = "https://token-plan-ams.xiaomimimo.com/v1" } req, _ := http.NewRequest("GET", strings.TrimRight(mimoBase, "/")+"/models", nil) req.Header.Set("Authorization", "Bearer "+p.APIKey) resp, err := client.Do(req) if err != nil { q.Error = err.Error() } else { body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} if json.Unmarshal(body, &data) == nil { if modelList, ok := data["data"].([]interface{}); ok { models := make([]map[string]interface{}, 0) for _, m := range modelList { if mm, ok := m.(map[string]interface{}); ok { id, _ := mm["id"].(string) if id != "" { models = append(models, map[string]interface{}{ "model": id, }) } } } q.Data = map[string]interface{}{"models": models, "available": len(models)} q.Healthy = true } } } case "claude", "anthropic": // Claude Code n'a pas d'API externe, vérifier l'installation claudePath := "/usr/bin/claude" if _, err := os.Stat(claudePath); err == nil { q.Healthy = true } else { q.Error = "claude code not installed" } default: q.Error = "quota not supported" } results = append(results, q) } writeJSON(w, map[string]interface{}{"providers": results}) } func (s *Server) handleProvidersConsumption(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } data := s.consumption.GetAll() writeJSON(w, map[string]interface{}{"providers": data}) } func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) { home, _ := os.UserHomeDir() type cmdEntry struct { Cmd string `json:"cmd"` Shell string `json:"shell"` } var entries []cmdEntry for _, histFile := range []string{".bash_history", ".zsh_history"} { path := filepath.Join(home, histFile) data, err := os.ReadFile(path) if err != nil { continue } shell := "bash" if strings.Contains(histFile, "zsh") { shell = "zsh" } lines := strings.Split(string(data), "\n") start := len(lines) - 50 if start < 0 { start = 0 } for i := len(lines) - 1; i >= start; i-- { line := strings.TrimSpace(lines[i]) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, ": ") { parts := strings.SplitN(line, ";", 2) if len(parts) == 2 { line = strings.TrimSpace(parts[1]) } else { continue } } if line == "" { continue } base := strings.Fields(line)[0] if len(base) < 2 { continue } if !regexp.MustCompile(`^[a-zA-Z@./]`).MatchString(base) { continue } entries = append(entries, cmdEntry{Cmd: line, Shell: shell}) } } max := 20 if len(entries) > max { entries = entries[:max] } writeJSON(w, map[string]interface{}{"commands": entries}) } func (s *Server) handleRunningProcesses(w http.ResponseWriter, r *http.Request) { type proc struct { PID int `json:"pid"` Name string `json:"name"` Command string `json:"command"` CPU string `json:"cpu"` Mem string `json:"mem"` } var procs []proc editors := []string{"code", "nvim", "vim", "emacs", "hx", "subl", "zed", "cursor"} langs := []string{"node", "python", "java", "go", "rustc", "cargo", "ruby", "php"} interesting := append(editors, langs...) interesting = append(interesting, "muyue") cmd := exec.Command("ps", "aux") out, err := cmd.Output() if err != nil { writeJSON(w, map[string]interface{}{"processes": procs}) return } lines := strings.Split(string(out), "\n") for _, line := range lines[1:] { fields := strings.Fields(line) if len(fields) < 11 { continue } fullCmd := strings.Join(fields[10:], " ") name := filepath.Base(fields[10]) matched := false for _, pattern := range interesting { if strings.Contains(name, pattern) || strings.Contains(strings.ToLower(fullCmd), pattern) { matched = true break } } if !matched { continue } var pid int fmt.Sscanf(fields[1], "%d", &pid) procs = append(procs, proc{ PID: pid, Name: name, Command: fullCmd, CPU: fields[2], Mem: fields[3], }) } writeJSON(w, map[string]interface{}{"processes": procs}) } type sysMetrics struct { CPUPercent float64 `json:"cpu_percent"` MemPercent float64 `json:"mem_percent"` MemUsedMB float64 `json:"mem_used_mb"` MemTotalMB float64 `json:"mem_total_mb"` NetRxKBs float64 `json:"net_rx_kbs"` NetTxKBs float64 `json:"net_tx_kbs"` } var ( lastCPU [2]float64 lastNet [2]float64 lastNetTs time.Time lastCPUSet bool ) func (s *Server) handleSystemMetrics(w http.ResponseWriter, r *http.Request) { m := sysMetrics{} // CPU from /proc/stat if data, err := os.ReadFile("/proc/stat"); err == nil { line := strings.Split(string(data), "\n")[0] fields := strings.Fields(line) if len(fields) >= 5 { var idle, total float64 for i := 1; i < len(fields) && i <= 4; i++ { var v float64 fmt.Sscanf(fields[i], "%f", &v) total += v if i == 4 { idle = v } } if lastCPUSet { dIdle := idle - lastCPU[0] dTotal := total - lastCPU[1] if dTotal > 0 { m.CPUPercent = (1 - dIdle/dTotal) * 100 } } lastCPU = [2]float64{idle, total} lastCPUSet = true } } // Memory from /proc/meminfo if data, err := os.ReadFile("/proc/meminfo"); err == nil { var memTotal, memAvailable float64 for _, line := range strings.Split(string(data), "\n") { fields := strings.Fields(line) if len(fields) < 2 { continue } var v float64 fmt.Sscanf(fields[1], "%f", &v) switch fields[0] { case "MemTotal:": memTotal = v case "MemAvailable:": memAvailable = v } } if memTotal > 0 { m.MemTotalMB = memTotal / 1024 m.MemUsedMB = (memTotal - memAvailable) / 1024 m.MemPercent = (memTotal - memAvailable) / memTotal * 100 } } // Network from /proc/net/dev if data, err := os.ReadFile("/proc/net/dev"); err == nil { var rxBytes, txBytes float64 for _, line := range strings.Split(string(data), "\n")[2:] { fields := strings.Fields(line) if len(fields) < 10 { continue } iface := strings.TrimSuffix(fields[0], ":") if iface == "lo" { continue } var rx, tx float64 fmt.Sscanf(fields[1], "%f", &rx) fmt.Sscanf(fields[9], "%f", &tx) rxBytes += rx txBytes += tx } now := time.Now() if !lastNetTs.IsZero() { elapsed := now.Sub(lastNetTs).Seconds() if elapsed > 0 { m.NetRxKBs = (rxBytes - lastNet[0]) / 1024 / elapsed m.NetTxKBs = (txBytes - lastNet[1]) / 1024 / elapsed if m.NetRxKBs < 0 { m.NetRxKBs = 0 } if m.NetTxKBs < 0 { m.NetTxKBs = 0 } } } lastNet = [2]float64{rxBytes, txBytes} lastNetTs = now } writeJSON(w, m) }