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