package api import ( "encoding/json" "net/http" "os" "path/filepath" "strings" "time" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/lsp" "github.com/muyue/muyue/internal/skills" ) type SavedConversation struct { ID string `json:"id"` Title string `json:"title"` Summary string `json:"summary,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Messages []MessageEntry `json:"messages,omitempty"` } type MessageEntry struct { ID string `json:"id"` Role string `json:"role"` Content string `json:"content"` Time string `json:"time"` } type conversationsStore struct { Path string Items []SavedConversation } func conversationsPath() string { dir, _ := config.ConfigDir() return filepath.Join(dir, "conversations.json") } func listConversations() ([]SavedConversation, error) { path := conversationsPath() data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return []SavedConversation{}, nil } return nil, err } var store conversationsStore if err := json.Unmarshal(data, &store); err != nil { return []SavedConversation{}, nil } return store.Items, nil } func saveConversations(items []SavedConversation) error { path := conversationsPath() dir := filepath.Dir(path) os.MkdirAll(dir, 0755) data, err := json.MarshalIndent(struct { Items []SavedConversation `json:"items"` }{Items: items}, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0600) } func (s *Server) handleListConversations(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } convs, err := listConversations() if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } conv := s.convStore.Get() tokenInfo := s.convStore.ApproxTokenCountDetailed() writeJSON(w, map[string]interface{}{ "conversations": convs, "current_messages": conv, "tokens": tokenInfo.total, "tokens_by_role": tokenInfo.byRole, "summary": s.convStore.GetSummary(), }) } func (s *Server) handleDeleteConversation(w http.ResponseWriter, r *http.Request) { if r.Method != "DELETE" { writeError(w, "DELETE only", http.StatusMethodNotAllowed) return } id := strings.TrimPrefix(r.URL.Path, "/api/conversations/") id = strings.TrimPrefix(id, "/") if id == "" { s.convStore.Clear() writeJSON(w, map[string]string{"status": "cleared"}) return } convs, err := listConversations() if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } filtered := make([]SavedConversation, 0, len(convs)) found := false for _, c := range convs { if c.ID == id { found = true continue } filtered = append(filtered, c) } if !found { writeError(w, "conversation not found", http.StatusNotFound) return } if err := saveConversations(filtered); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "deleted"}) } func (s *Server) handleSearchConversations(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } query := r.URL.Query().Get("q") if query == "" { writeError(w, "query parameter 'q' is required", http.StatusBadRequest) return } results := s.convStore.Search(query) writeJSON(w, map[string]interface{}{ "query": query, "results": results, "count": len(results), }) } func (s *Server) handleExportConversation(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } format := r.URL.Query().Get("format") if format == "markdown" || format == "md" { w.Header().Set("Content-Type", "text/markdown; charset=utf-8") w.Write([]byte(s.convStore.ExportMarkdown())) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(s.convStore.ExportJSON())) } func (s *Server) handleLSPInstall(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 } if body.Name == "" { writeError(w, "name is required", http.StatusBadRequest) return } if err := lsp.InstallServer(body.Name); err != nil { writeJSON(w, map[string]interface{}{ "success": false, "error": err.Error(), }) return } writeJSON(w, map[string]interface{}{ "success": true, "server": body.Name, }) } func (s *Server) handleSkillsDeploy(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 } if body.Name != "" { skill, err := skills.Get(body.Name) if err != nil { writeError(w, err.Error(), http.StatusNotFound) return } if err := skills.Deploy(skill); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "deployed", "skill": body.Name}) return } if err := skills.DeployAll(); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "all deployed"}) } func (s *Server) handleSSHConnections(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } cfg, err := config.Load() if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]interface{}{ "connections": cfg.Terminal.SSH, }) } func (s *Server) handleSSHTest(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } var body struct { Host string `json:"host"` Port int `json:"port"` User string `json:"user"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if body.Host == "" || body.User == "" { writeError(w, "host and user are required", http.StatusBadRequest) return } if body.Port == 0 { body.Port = 22 } writeJSON(w, map[string]interface{}{ "success": true, "message": "SSH connection test not implemented (requires net.DialTimeout)", }) }