Some checks failed
Beta Release / beta (push) Failing after 33s
- Agent slot limiter for concurrent tool execution - Conversation summarization with soft-delete (MarkSummarized) - ANSI stripping in terminal tool output - Configurable crush-run timeout (default 600s, max 900s) - Starship theme refactor, AI tools config grid, system update UI - Streaming segments refactor, summarized messages block in feed - CSS: headings, scrollbars, tool cards, summary block styles - i18n additions (en+fr) for tools, updates, config 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
292 lines
7.2 KiB
Go
292 lines
7.2 KiB
Go
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) handleSkillsUndeploy(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 := skills.Undeploy(body.Name); err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]string{"status": "undeployed", "skill": body.Name})
|
|
}
|
|
|
|
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)",
|
|
})
|
|
} |