1package server
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log/slog"
8 "net/http"
9 "os"
10 "path/filepath"
11 "runtime"
12
13 "github.com/charmbracelet/crush/internal/app"
14 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/db"
16 "github.com/charmbracelet/crush/internal/permission"
17 "github.com/charmbracelet/crush/internal/proto"
18 "github.com/charmbracelet/crush/internal/session"
19 "github.com/charmbracelet/crush/internal/ui/util"
20 "github.com/charmbracelet/crush/internal/version"
21 "github.com/google/uuid"
22)
23
24type controllerV1 struct {
25 *Server
26}
27
28func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) {
29 w.WriteHeader(http.StatusOK)
30}
31
32func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
33 jsonEncode(w, proto.VersionInfo{
34 Version: version.Version,
35 Commit: version.Commit,
36 GoVersion: runtime.Version(),
37 Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
38 })
39}
40
41func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) {
42 var req proto.ServerControl
43 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
44 c.logError(r, "Failed to decode request", "error", err)
45 jsonError(w, http.StatusBadRequest, "failed to decode request")
46 return
47 }
48
49 switch req.Command {
50 case "shutdown":
51 go func() {
52 slog.Info("Shutting down server...")
53 if err := c.Shutdown(context.Background()); err != nil {
54 slog.Error("Failed to shutdown server", "error", err)
55 }
56 }()
57 default:
58 c.logError(r, "Unknown command", "command", req.Command)
59 jsonError(w, http.StatusBadRequest, "unknown command")
60 return
61 }
62}
63
64func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) {
65 jsonEncode(w, c.cfg)
66}
67
68func (c *controllerV1) handleGetWorkspaces(w http.ResponseWriter, _ *http.Request) {
69 workspaces := []proto.Workspace{}
70 for _, ws := range c.workspaces.Seq2() {
71 workspaces = append(workspaces, proto.Workspace{
72 ID: ws.id,
73 Path: ws.path,
74 YOLO: ws.cfg.Permissions != nil && ws.cfg.Permissions.SkipRequests,
75 DataDir: ws.cfg.Options.DataDirectory,
76 Debug: ws.cfg.Options.Debug,
77 Config: ws.cfg,
78 })
79 }
80 jsonEncode(w, workspaces)
81}
82
83func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
84 id := r.PathValue("id")
85 ws, ok := c.workspaces.Get(id)
86 if !ok {
87 c.logError(r, "Workspace not found", "id", id)
88 jsonError(w, http.StatusNotFound, "workspace not found")
89 return
90 }
91
92 lspName := r.PathValue("lsp")
93 var found bool
94 for name, client := range ws.LSPManager.Clients().Seq2() {
95 if name == lspName {
96 diagnostics := client.GetDiagnostics()
97 jsonEncode(w, diagnostics)
98 found = true
99 break
100 }
101 }
102
103 if !found {
104 c.logError(r, "LSP client not found", "id", id, "lsp", lspName)
105 jsonError(w, http.StatusNotFound, "LSP client not found")
106 }
107}
108
109func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) {
110 id := r.PathValue("id")
111 _, ok := c.workspaces.Get(id)
112 if !ok {
113 c.logError(r, "Workspace not found", "id", id)
114 jsonError(w, http.StatusNotFound, "workspace not found")
115 return
116 }
117
118 lspClients := app.GetLSPStates()
119 jsonEncode(w, lspClients)
120}
121
122func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
123 id := r.PathValue("id")
124 ws, ok := c.workspaces.Get(id)
125 if !ok {
126 c.logError(r, "Workspace not found", "id", id)
127 jsonError(w, http.StatusNotFound, "workspace not found")
128 return
129 }
130
131 sid := r.PathValue("sid")
132 queued := ws.App.AgentCoordinator.QueuedPrompts(sid)
133 jsonEncode(w, queued)
134}
135
136func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
137 id := r.PathValue("id")
138 ws, ok := c.workspaces.Get(id)
139 if !ok {
140 c.logError(r, "Workspace not found", "id", id)
141 jsonError(w, http.StatusNotFound, "workspace not found")
142 return
143 }
144
145 sid := r.PathValue("sid")
146 ws.App.AgentCoordinator.ClearQueue(sid)
147 w.WriteHeader(http.StatusOK)
148}
149
150func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
151 id := r.PathValue("id")
152 ws, ok := c.workspaces.Get(id)
153 if !ok {
154 c.logError(r, "Workspace not found", "id", id)
155 jsonError(w, http.StatusNotFound, "workspace not found")
156 return
157 }
158
159 sid := r.PathValue("sid")
160 if err := ws.App.AgentCoordinator.Summarize(r.Context(), sid); err != nil {
161 c.logError(r, "Failed to summarize session", "error", err, "id", id, "sid", sid)
162 jsonError(w, http.StatusInternalServerError, "failed to summarize session")
163 return
164 }
165}
166
167func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
168 id := r.PathValue("id")
169 ws, ok := c.workspaces.Get(id)
170 if !ok {
171 c.logError(r, "Workspace not found", "id", id)
172 jsonError(w, http.StatusNotFound, "workspace not found")
173 return
174 }
175
176 sid := r.PathValue("sid")
177 if ws.App.AgentCoordinator != nil {
178 ws.App.AgentCoordinator.Cancel(sid)
179 }
180 w.WriteHeader(http.StatusOK)
181}
182
183func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
184 id := r.PathValue("id")
185 ws, ok := c.workspaces.Get(id)
186 if !ok {
187 c.logError(r, "Workspace not found", "id", id)
188 jsonError(w, http.StatusNotFound, "workspace not found")
189 return
190 }
191
192 sid := r.PathValue("sid")
193 se, err := ws.App.Sessions.Get(r.Context(), sid)
194 if err != nil {
195 c.logError(r, "Failed to get session", "error", err, "id", id, "sid", sid)
196 jsonError(w, http.StatusInternalServerError, "failed to get session")
197 return
198 }
199
200 var isSessionBusy bool
201 if ws.App.AgentCoordinator != nil {
202 isSessionBusy = ws.App.AgentCoordinator.IsSessionBusy(sid)
203 }
204
205 jsonEncode(w, proto.AgentSession{
206 Session: proto.Session{
207 ID: se.ID,
208 Title: se.Title,
209 },
210 IsBusy: isSessionBusy,
211 })
212}
213
214func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
215 id := r.PathValue("id")
216 ws, ok := c.workspaces.Get(id)
217 if !ok {
218 c.logError(r, "Workspace not found", "id", id)
219 jsonError(w, http.StatusNotFound, "workspace not found")
220 return
221 }
222
223 w.Header().Set("Accept", "application/json")
224
225 var msg proto.AgentMessage
226 if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
227 c.logError(r, "Failed to decode request", "error", err)
228 jsonError(w, http.StatusBadRequest, "failed to decode request")
229 return
230 }
231
232 if ws.App.AgentCoordinator == nil {
233 c.logError(r, "Agent coordinator not initialized", "id", id)
234 jsonError(w, http.StatusBadRequest, "agent coordinator not initialized")
235 return
236 }
237
238 if _, err := ws.App.AgentCoordinator.Run(c.ctx, msg.SessionID, msg.Prompt); err != nil {
239 c.logError(r, "Failed to enqueue message", "error", err, "id", id, "sid", msg.SessionID)
240 jsonError(w, http.StatusInternalServerError, "failed to enqueue message")
241 return
242 }
243}
244
245func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
246 id := r.PathValue("id")
247 ws, ok := c.workspaces.Get(id)
248 if !ok {
249 c.logError(r, "Workspace not found", "id", id)
250 jsonError(w, http.StatusNotFound, "workspace not found")
251 return
252 }
253
254 var agentInfo proto.AgentInfo
255 if ws.App.AgentCoordinator != nil {
256 m := ws.App.AgentCoordinator.Model()
257 agentInfo = proto.AgentInfo{
258 Model: m.CatwalkCfg,
259 IsBusy: ws.App.AgentCoordinator.IsBusy(),
260 }
261 }
262 jsonEncode(w, agentInfo)
263}
264
265func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
266 id := r.PathValue("id")
267 ws, ok := c.workspaces.Get(id)
268 if !ok {
269 c.logError(r, "Workspace not found", "id", id)
270 jsonError(w, http.StatusNotFound, "workspace not found")
271 return
272 }
273
274 if err := ws.App.UpdateAgentModel(r.Context()); err != nil {
275 c.logError(r, "Failed to update agent model", "error", err)
276 jsonError(w, http.StatusInternalServerError, "failed to update agent model")
277 return
278 }
279}
280
281func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
282 id := r.PathValue("id")
283 ws, ok := c.workspaces.Get(id)
284 if !ok {
285 c.logError(r, "Workspace not found", "id", id)
286 jsonError(w, http.StatusNotFound, "workspace not found")
287 return
288 }
289
290 if err := ws.App.InitCoderAgent(r.Context()); err != nil {
291 c.logError(r, "Failed to initialize coder agent", "error", err)
292 jsonError(w, http.StatusInternalServerError, "failed to initialize coder agent")
293 return
294 }
295}
296
297func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) {
298 id := r.PathValue("id")
299 ws, ok := c.workspaces.Get(id)
300 if !ok {
301 c.logError(r, "Workspace not found", "id", id)
302 jsonError(w, http.StatusNotFound, "workspace not found")
303 return
304 }
305
306 sid := r.PathValue("sid")
307 historyItems, err := ws.App.History.ListBySession(r.Context(), sid)
308 if err != nil {
309 c.logError(r, "Failed to list history", "error", err, "id", id, "sid", sid)
310 jsonError(w, http.StatusInternalServerError, "failed to list history")
311 return
312 }
313
314 jsonEncode(w, historyItems)
315}
316
317func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) {
318 id := r.PathValue("id")
319 ws, ok := c.workspaces.Get(id)
320 if !ok {
321 c.logError(r, "Workspace not found", "id", id)
322 jsonError(w, http.StatusNotFound, "workspace not found")
323 return
324 }
325
326 sid := r.PathValue("sid")
327 messages, err := ws.App.Messages.List(r.Context(), sid)
328 if err != nil {
329 c.logError(r, "Failed to list messages", "error", err, "id", id, "sid", sid)
330 jsonError(w, http.StatusInternalServerError, "failed to list messages")
331 return
332 }
333
334 jsonEncode(w, messages)
335}
336
337func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) {
338 id := r.PathValue("id")
339 ws, ok := c.workspaces.Get(id)
340 if !ok {
341 c.logError(r, "Workspace not found", "id", id)
342 jsonError(w, http.StatusNotFound, "workspace not found")
343 return
344 }
345
346 sid := r.PathValue("sid")
347 sess, err := ws.App.Sessions.Get(r.Context(), sid)
348 if err != nil {
349 c.logError(r, "Failed to get session", "error", err, "id", id, "sid", sid)
350 jsonError(w, http.StatusInternalServerError, "failed to get session")
351 return
352 }
353
354 jsonEncode(w, sess)
355}
356
357func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
358 id := r.PathValue("id")
359 ws, ok := c.workspaces.Get(id)
360 if !ok {
361 c.logError(r, "Workspace not found", "id", id)
362 jsonError(w, http.StatusNotFound, "workspace not found")
363 return
364 }
365
366 var args session.Session
367 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
368 c.logError(r, "Failed to decode request", "error", err)
369 jsonError(w, http.StatusBadRequest, "failed to decode request")
370 return
371 }
372
373 sess, err := ws.App.Sessions.Create(r.Context(), args.Title)
374 if err != nil {
375 c.logError(r, "Failed to create session", "error", err, "id", id)
376 jsonError(w, http.StatusInternalServerError, "failed to create session")
377 return
378 }
379
380 jsonEncode(w, sess)
381}
382
383func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) {
384 id := r.PathValue("id")
385 ws, ok := c.workspaces.Get(id)
386 if !ok {
387 c.logError(r, "Workspace not found", "id", id)
388 jsonError(w, http.StatusNotFound, "workspace not found")
389 return
390 }
391
392 sessions, err := ws.App.Sessions.List(r.Context())
393 if err != nil {
394 c.logError(r, "Failed to list sessions", "error", err)
395 jsonError(w, http.StatusInternalServerError, "failed to list sessions")
396 return
397 }
398
399 jsonEncode(w, sessions)
400}
401
402func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) {
403 id := r.PathValue("id")
404 ws, ok := c.workspaces.Get(id)
405 if !ok {
406 c.logError(r, "Workspace not found", "id", id)
407 jsonError(w, http.StatusNotFound, "workspace not found")
408 return
409 }
410
411 var req proto.PermissionGrant
412 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
413 c.logError(r, "Failed to decode request", "error", err)
414 jsonError(w, http.StatusBadRequest, "failed to decode request")
415 return
416 }
417
418 perm := permission.PermissionRequest{
419 ID: req.Permission.ID,
420 SessionID: req.Permission.SessionID,
421 ToolCallID: req.Permission.ToolCallID,
422 ToolName: req.Permission.ToolName,
423 Description: req.Permission.Description,
424 Action: req.Permission.Action,
425 Params: req.Permission.Params,
426 Path: req.Permission.Path,
427 }
428
429 switch req.Action {
430 case proto.PermissionAllow:
431 ws.App.Permissions.Grant(perm)
432 case proto.PermissionAllowForSession:
433 ws.App.Permissions.GrantPersistent(perm)
434 case proto.PermissionDeny:
435 ws.App.Permissions.Deny(perm)
436 default:
437 c.logError(r, "Invalid permission action", "action", req.Action)
438 jsonError(w, http.StatusBadRequest, "invalid permission action")
439 return
440 }
441}
442
443func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
444 id := r.PathValue("id")
445 ws, ok := c.workspaces.Get(id)
446 if !ok {
447 c.logError(r, "Workspace not found", "id", id)
448 jsonError(w, http.StatusNotFound, "workspace not found")
449 return
450 }
451
452 var req proto.PermissionSkipRequest
453 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
454 c.logError(r, "Failed to decode request", "error", err)
455 jsonError(w, http.StatusBadRequest, "failed to decode request")
456 return
457 }
458
459 ws.App.Permissions.SetSkipRequests(req.Skip)
460}
461
462func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
463 id := r.PathValue("id")
464 ws, ok := c.workspaces.Get(id)
465 if !ok {
466 c.logError(r, "Workspace not found", "id", id)
467 jsonError(w, http.StatusNotFound, "workspace not found")
468 return
469 }
470
471 skip := ws.App.Permissions.SkipRequests()
472 jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
473}
474
475func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) {
476 id := r.PathValue("id")
477 ws, ok := c.workspaces.Get(id)
478 if !ok {
479 c.logError(r, "Workspace not found", "id", id)
480 jsonError(w, http.StatusNotFound, "workspace not found")
481 return
482 }
483
484 providers, _ := config.Providers(ws.cfg)
485 jsonEncode(w, providers)
486}
487
488func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) {
489 flusher := http.NewResponseController(w)
490 id := r.PathValue("id")
491 ws, ok := c.workspaces.Get(id)
492 if !ok {
493 c.logError(r, "Workspace not found", "id", id)
494 jsonError(w, http.StatusNotFound, "workspace not found")
495 return
496 }
497
498 w.Header().Set("Content-Type", "text/event-stream")
499 w.Header().Set("Cache-Control", "no-cache")
500 w.Header().Set("Connection", "keep-alive")
501
502 events := ws.App.Events()
503
504 for {
505 select {
506 case <-r.Context().Done():
507 c.logDebug(r, "Stopping event stream")
508 return
509 case ev, ok := <-events:
510 if !ok {
511 return
512 }
513 c.logDebug(r, "Sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
514 data, err := json.Marshal(ev)
515 if err != nil {
516 c.logError(r, "Failed to marshal event", "error", err)
517 continue
518 }
519
520 fmt.Fprintf(w, "data: %s\n\n", data)
521 flusher.Flush()
522 }
523 }
524}
525
526func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) {
527 id := r.PathValue("id")
528 ws, ok := c.workspaces.Get(id)
529 if !ok {
530 c.logError(r, "Workspace not found", "id", id)
531 jsonError(w, http.StatusNotFound, "workspace not found")
532 return
533 }
534
535 jsonEncode(w, ws.cfg)
536}
537
538func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) {
539 id := r.PathValue("id")
540 ws, ok := c.workspaces.Get(id)
541 if ok {
542 ws.App.Shutdown()
543 }
544 c.workspaces.Del(id)
545
546 // When the last workspace is removed, shut down the server.
547 if c.workspaces.Len() == 0 {
548 slog.Info("Last workspace removed, shutting down server...")
549 go func() {
550 if err := c.Shutdown(context.Background()); err != nil {
551 slog.Error("Failed to shutdown server", "error", err)
552 }
553 }()
554 }
555}
556
557func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
558 id := r.PathValue("id")
559 ws, ok := c.workspaces.Get(id)
560 if !ok {
561 c.logError(r, "Workspace not found", "id", id)
562 jsonError(w, http.StatusNotFound, "workspace not found")
563 return
564 }
565
566 jsonEncode(w, proto.Workspace{
567 ID: ws.id,
568 Path: ws.path,
569 YOLO: ws.cfg.Permissions != nil && ws.cfg.Permissions.SkipRequests,
570 DataDir: ws.cfg.Options.DataDirectory,
571 Debug: ws.cfg.Options.Debug,
572 Config: ws.cfg,
573 })
574}
575
576func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) {
577 var args proto.Workspace
578 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
579 c.logError(r, "Failed to decode request", "error", err)
580 jsonError(w, http.StatusBadRequest, "failed to decode request")
581 return
582 }
583
584 if args.Path == "" {
585 c.logError(r, "Path is required")
586 jsonError(w, http.StatusBadRequest, "path is required")
587 return
588 }
589
590 id := uuid.New().String()
591 cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
592 if err != nil {
593 c.logError(r, "Failed to initialize config", "error", err)
594 jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
595 return
596 }
597
598 if cfg.Permissions == nil {
599 cfg.Permissions = &config.Permissions{}
600 }
601 cfg.Permissions.SkipRequests = args.YOLO
602
603 if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
604 c.logError(r, "Failed to create data directory", "error", err)
605 jsonError(w, http.StatusInternalServerError, "failed to create data directory")
606 return
607 }
608
609 conn, err := db.Connect(c.ctx, cfg.Options.DataDirectory)
610 if err != nil {
611 c.logError(r, "Failed to connect to database", "error", err)
612 jsonError(w, http.StatusInternalServerError, "failed to connect to database")
613 return
614 }
615
616 appWorkspace, err := app.New(c.ctx, conn, cfg)
617 if err != nil {
618 slog.Error("Failed to create app workspace", "error", err)
619 jsonError(w, http.StatusInternalServerError, "failed to create app workspace")
620 return
621 }
622
623 ws := &Workspace{
624 App: appWorkspace,
625 id: id,
626 path: args.Path,
627 cfg: cfg,
628 env: args.Env,
629 }
630
631 c.workspaces.Set(id, ws)
632
633 if args.Version != "" && args.Version != version.Version {
634 slog.Warn("Client/server version mismatch",
635 "client", args.Version,
636 "server", version.Version,
637 )
638 appWorkspace.SendEvent(util.NewWarnMsg(fmt.Sprintf(
639 "Server version %q differs from client version %q. Consider restarting the server.",
640 version.Version, args.Version,
641 )))
642 }
643
644 jsonEncode(w, proto.Workspace{
645 ID: id,
646 Path: args.Path,
647 DataDir: cfg.Options.DataDirectory,
648 Debug: cfg.Options.Debug,
649 YOLO: cfg.Permissions.SkipRequests,
650 Config: cfg,
651 Env: args.Env,
652 })
653}
654
655func createDotCrushDir(dir string) error {
656 if err := os.MkdirAll(dir, 0o700); err != nil {
657 return fmt.Errorf("failed to create data directory: %q %w", dir, err)
658 }
659
660 gitIgnorePath := filepath.Join(dir, ".gitignore")
661 if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
662 if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
663 return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
664 }
665 }
666
667 return nil
668}
669
670func jsonEncode(w http.ResponseWriter, v any) {
671 w.Header().Set("Content-Type", "application/json")
672 _ = json.NewEncoder(w).Encode(v)
673}
674
675func jsonError(w http.ResponseWriter, status int, message string) {
676 w.Header().Set("Content-Type", "application/json")
677 w.WriteHeader(status)
678 _ = json.NewEncoder(w).Encode(proto.Error{Message: message})
679}