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/lsp"
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, r *http.Request) {
28 w.WriteHeader(http.StatusOK)
29}
30
31func (c *controllerV1) handleGetVersion(w http.ResponseWriter, r *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 c.logError(r, "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, r *http.Request) {
64 jsonEncode(w, c.cfg)
65}
66
67func (c *controllerV1) handleGetInstances(w http.ResponseWriter, r *http.Request) {
68 instances := []proto.Instance{}
69 for _, ins := range c.instances.Seq2() {
70 instances = append(instances, proto.Instance{
71 ID: ins.ID(),
72 Path: ins.Path(),
73 YOLO: ins.cfg.Permissions != nil && ins.cfg.Permissions.SkipRequests,
74 DataDir: ins.cfg.Options.DataDirectory,
75 Debug: ins.cfg.Options.Debug,
76 })
77 }
78 jsonEncode(w, instances)
79}
80
81func (c *controllerV1) handleGetInstanceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
82 id := r.PathValue("id")
83 ins, ok := c.instances.Get(id)
84 if !ok {
85 c.logError(r, "instance not found", "id", id)
86 jsonError(w, http.StatusNotFound, "instance not found")
87 return
88 }
89
90 var lsp *lsp.Client
91 lspName := r.PathValue("lsp")
92 for name, client := range ins.LSPClients.Seq2() {
93 if name == lspName {
94 lsp = client
95 break
96 }
97 }
98
99 if lsp == nil {
100 c.logError(r, "LSP client not found", "id", id, "lsp", lspName)
101 jsonError(w, http.StatusNotFound, "LSP client not found")
102 return
103 }
104
105 diagnostics := lsp.GetDiagnostics()
106 jsonEncode(w, diagnostics)
107}
108
109func (c *controllerV1) handleGetInstanceLSPs(w http.ResponseWriter, r *http.Request) {
110 id := r.PathValue("id")
111 ins, ok := c.instances.Get(id)
112 if !ok {
113 c.logError(r, "instance not found", "id", id)
114 jsonError(w, http.StatusNotFound, "instance not found")
115 return
116 }
117
118 lspClients := ins.GetLSPStates()
119 jsonEncode(w, lspClients)
120}
121
122func (c *controllerV1) handleGetInstanceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
123 id := r.PathValue("id")
124 ins, ok := c.instances.Get(id)
125 if !ok {
126 c.logError(r, "instance not found", "id", id)
127 jsonError(w, http.StatusNotFound, "instance not found")
128 return
129 }
130
131 sid := r.PathValue("sid")
132 queued := ins.App.CoderAgent.QueuedPrompts(sid)
133 jsonEncode(w, queued)
134}
135
136func (c *controllerV1) handlePostInstanceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
137 id := r.PathValue("id")
138 ins, ok := c.instances.Get(id)
139 if !ok {
140 c.logError(r, "instance not found", "id", id)
141 jsonError(w, http.StatusNotFound, "instance not found")
142 return
143 }
144
145 sid := r.PathValue("sid")
146 ins.App.CoderAgent.ClearQueue(sid)
147}
148
149func (c *controllerV1) handleGetInstanceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
150 id := r.PathValue("id")
151 ins, ok := c.instances.Get(id)
152 if !ok {
153 c.logError(r, "instance not found", "id", id)
154 jsonError(w, http.StatusNotFound, "instance not found")
155 return
156 }
157
158 sid := r.PathValue("sid")
159 if err := ins.App.CoderAgent.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) handlePostInstanceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
167 id := r.PathValue("id")
168 ins, ok := c.instances.Get(id)
169 if !ok {
170 c.logError(r, "instance not found", "id", id)
171 jsonError(w, http.StatusNotFound, "instance not found")
172 return
173 }
174
175 sid := r.PathValue("sid")
176 if ins.App.CoderAgent != nil {
177 ins.App.CoderAgent.Cancel(sid)
178 }
179}
180
181func (c *controllerV1) handleGetInstanceAgentSession(w http.ResponseWriter, r *http.Request) {
182 id := r.PathValue("id")
183 ins, ok := c.instances.Get(id)
184 if !ok {
185 c.logError(r, "instance not found", "id", id)
186 jsonError(w, http.StatusNotFound, "instance not found")
187 return
188 }
189
190 sid := r.PathValue("sid")
191 se, err := ins.App.Sessions.Get(r.Context(), sid)
192 if err != nil {
193 c.logError(r, "failed to get session", "error", err, "id", id, "sid", sid)
194 jsonError(w, http.StatusInternalServerError, "failed to get session")
195 return
196 }
197
198 var isSessionBusy bool
199 if ins.App.CoderAgent != nil {
200 isSessionBusy = ins.App.CoderAgent.IsSessionBusy(sid)
201 }
202
203 jsonEncode(w, proto.AgentSession{
204 Session: se,
205 IsBusy: isSessionBusy,
206 })
207}
208
209func (c *controllerV1) handlePostInstanceAgent(w http.ResponseWriter, r *http.Request) {
210 id := r.PathValue("id")
211 ins, ok := c.instances.Get(id)
212 if !ok {
213 c.logError(r, "instance not found", "id", id)
214 jsonError(w, http.StatusNotFound, "instance not found")
215 return
216 }
217
218 w.Header().Set("Accept", "application/json")
219
220 var msg proto.AgentMessage
221 if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
222 c.logError(r, "failed to decode request", "error", err)
223 jsonError(w, http.StatusBadRequest, "failed to decode request")
224 return
225 }
226
227 if ins.App.CoderAgent == nil {
228 c.logError(r, "coder agent not initialized", "id", id)
229 jsonError(w, http.StatusBadRequest, "coder agent not initialized")
230 return
231 }
232
233 // NOTE: This needs to be on the server's context because the agent runs
234 // the request asynchronously.
235 // TODO: Look into this one more and make it work synchronously.
236 if _, err := ins.App.CoderAgent.Run(c.ctx, msg.SessionID, msg.Prompt, msg.Attachments...); err != nil {
237 c.logError(r, "failed to enqueue message", "error", err, "id", id, "sid", msg.SessionID)
238 jsonError(w, http.StatusInternalServerError, "failed to enqueue message")
239 return
240 }
241}
242
243func (c *controllerV1) handleGetInstanceAgent(w http.ResponseWriter, r *http.Request) {
244 id := r.PathValue("id")
245 ins, ok := c.instances.Get(id)
246 if !ok {
247 c.logError(r, "instance not found", "id", id)
248 jsonError(w, http.StatusNotFound, "instance not found")
249 return
250 }
251
252 var agentInfo proto.AgentInfo
253 if ins.App.CoderAgent != nil {
254 agentInfo = proto.AgentInfo{
255 Model: ins.App.CoderAgent.Model(),
256 IsBusy: ins.App.CoderAgent.IsBusy(),
257 }
258 }
259 jsonEncode(w, agentInfo)
260}
261
262func (c *controllerV1) handlePostInstanceAgentUpdate(w http.ResponseWriter, r *http.Request) {
263 id := r.PathValue("id")
264 ins, ok := c.instances.Get(id)
265 if !ok {
266 c.logError(r, "instance not found", "id", id)
267 jsonError(w, http.StatusNotFound, "instance not found")
268 return
269 }
270
271 if err := ins.App.UpdateAgentModel(); err != nil {
272 c.logError(r, "failed to update agent model", "error", err)
273 jsonError(w, http.StatusInternalServerError, "failed to update agent model")
274 return
275 }
276}
277
278func (c *controllerV1) handlePostInstanceAgentInit(w http.ResponseWriter, r *http.Request) {
279 id := r.PathValue("id")
280 ins, ok := c.instances.Get(id)
281 if !ok {
282 c.logError(r, "instance not found", "id", id)
283 jsonError(w, http.StatusNotFound, "instance not found")
284 return
285 }
286
287 if err := ins.App.InitCoderAgent(); err != nil {
288 c.logError(r, "failed to initialize coder agent", "error", err)
289 jsonError(w, http.StatusInternalServerError, "failed to initialize coder agent")
290 return
291 }
292}
293
294func (c *controllerV1) handleGetInstanceSessionHistory(w http.ResponseWriter, r *http.Request) {
295 id := r.PathValue("id")
296 ins, ok := c.instances.Get(id)
297 if !ok {
298 c.logError(r, "instance not found", "id", id)
299 jsonError(w, http.StatusNotFound, "instance not found")
300 return
301 }
302
303 sid := r.PathValue("sid")
304 historyItems, err := ins.App.History.ListBySession(r.Context(), sid)
305 if err != nil {
306 c.logError(r, "failed to list history", "error", err, "id", id, "sid", sid)
307 jsonError(w, http.StatusInternalServerError, "failed to list history")
308 return
309 }
310
311 jsonEncode(w, historyItems)
312}
313
314func (c *controllerV1) handleGetInstanceSessionMessages(w http.ResponseWriter, r *http.Request) {
315 id := r.PathValue("id")
316 ins, ok := c.instances.Get(id)
317 if !ok {
318 c.logError(r, "instance not found", "id", id)
319 jsonError(w, http.StatusNotFound, "instance not found")
320 return
321 }
322
323 sid := r.PathValue("sid")
324 messages, err := ins.App.Messages.List(r.Context(), sid)
325 if err != nil {
326 c.logError(r, "failed to list messages", "error", err, "id", id, "sid", sid)
327 jsonError(w, http.StatusInternalServerError, "failed to list messages")
328 return
329 }
330
331 jsonEncode(w, messages)
332}
333
334func (c *controllerV1) handleGetInstanceSession(w http.ResponseWriter, r *http.Request) {
335 id := r.PathValue("id")
336 ins, ok := c.instances.Get(id)
337 if !ok {
338 c.logError(r, "instance not found", "id", id)
339 jsonError(w, http.StatusNotFound, "instance not found")
340 return
341 }
342
343 sid := r.PathValue("sid")
344 session, err := ins.App.Sessions.Get(r.Context(), sid)
345 if err != nil {
346 c.logError(r, "failedto get session", "error", err, "id", id, "sid", sid)
347 jsonError(w, http.StatusInternalServerError, "failed to get session")
348 return
349 }
350
351 jsonEncode(w, session)
352}
353
354func (c *controllerV1) handlePostInstanceSessions(w http.ResponseWriter, r *http.Request) {
355 id := r.PathValue("id")
356 ins, ok := c.instances.Get(id)
357 if !ok {
358 c.logError(r, "instance not found", "id", id)
359 jsonError(w, http.StatusNotFound, "instance not found")
360 return
361 }
362
363 var args session.Session
364 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
365 c.logError(r, "failed to decode request", "error", err)
366 jsonError(w, http.StatusBadRequest, "failed to decode request")
367 return
368 }
369
370 sess, err := ins.App.Sessions.Create(r.Context(), args.Title)
371 if err != nil {
372 c.logError(r, "failed to create session", "error", err, "id", id)
373 jsonError(w, http.StatusInternalServerError, "failed to create session")
374 return
375 }
376
377 jsonEncode(w, sess)
378}
379
380func (c *controllerV1) handleGetInstanceSessions(w http.ResponseWriter, r *http.Request) {
381 id := r.PathValue("id")
382 ins, ok := c.instances.Get(id)
383 if !ok {
384 c.logError(r, "instance not found", "id", id)
385 jsonError(w, http.StatusNotFound, "instance not found")
386 return
387 }
388
389 sessions, err := ins.App.Sessions.List(r.Context())
390 if err != nil {
391 c.logError(r, "failed to list sessions", "error", err)
392 jsonError(w, http.StatusInternalServerError, "failed to list sessions")
393 return
394 }
395
396 jsonEncode(w, sessions)
397}
398
399func (c *controllerV1) handlePostInstancePermissionsGrant(w http.ResponseWriter, r *http.Request) {
400 id := r.PathValue("id")
401 ins, ok := c.instances.Get(id)
402 if !ok {
403 c.logError(r, "instance not found", "id", id)
404 jsonError(w, http.StatusNotFound, "instance not found")
405 return
406 }
407
408 var req proto.PermissionGrant
409 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
410 c.logError(r, "failed to decode request", "error", err)
411 jsonError(w, http.StatusBadRequest, "failed to decode request")
412 return
413 }
414
415 switch req.Action {
416 case proto.PermissionAllow:
417 ins.App.Permissions.Grant(req.Permission)
418 case proto.PermissionAllowForSession:
419 ins.App.Permissions.GrantPersistent(req.Permission)
420 case proto.PermissionDeny:
421 ins.App.Permissions.Deny(req.Permission)
422 default:
423 c.logError(r, "invalid permission action", "action", req.Action)
424 jsonError(w, http.StatusBadRequest, "invalid permission action")
425 return
426 }
427}
428
429func (c *controllerV1) handlePostInstancePermissionsSkip(w http.ResponseWriter, r *http.Request) {
430 id := r.PathValue("id")
431 ins, ok := c.instances.Get(id)
432 if !ok {
433 c.logError(r, "instance not found", "id", id)
434 jsonError(w, http.StatusNotFound, "instance not found")
435 return
436 }
437
438 var req proto.PermissionSkipRequest
439 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
440 c.logError(r, "failed to decode request", "error", err)
441 jsonError(w, http.StatusBadRequest, "failed to decode request")
442 return
443 }
444
445 ins.App.Permissions.SetSkipRequests(req.Skip)
446}
447
448func (c *controllerV1) handleGetInstancePermissionsSkip(w http.ResponseWriter, r *http.Request) {
449 id := r.PathValue("id")
450 ins, ok := c.instances.Get(id)
451 if !ok {
452 c.logError(r, "instance not found", "id", id)
453 jsonError(w, http.StatusNotFound, "instance not found")
454 return
455 }
456
457 skip := ins.App.Permissions.SkipRequests()
458 jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
459}
460
461func (c *controllerV1) handleGetInstanceEvents(w http.ResponseWriter, r *http.Request) {
462 flusher := http.NewResponseController(w)
463 id := r.PathValue("id")
464 ins, ok := c.instances.Get(id)
465 if !ok {
466 c.logError(r, "instance not found", "id", id)
467 jsonError(w, http.StatusNotFound, "instance not found")
468 return
469 }
470
471 w.Header().Set("Content-Type", "text/event-stream")
472 w.Header().Set("Cache-Control", "no-cache")
473 w.Header().Set("Connection", "keep-alive")
474
475 for {
476 select {
477 case <-r.Context().Done():
478 c.logDebug(r, "stopping event stream")
479 return
480 case ev := <-ins.App.Events():
481 c.logDebug(r, "sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
482 data, err := json.Marshal(ev)
483 if err != nil {
484 c.logError(r, "failed to marshal event", "error", err)
485 continue
486 }
487
488 fmt.Fprintf(w, "data: %s\n\n", data)
489 flusher.Flush()
490 }
491 }
492}
493
494func (c *controllerV1) handleGetInstanceConfig(w http.ResponseWriter, r *http.Request) {
495 id := r.PathValue("id")
496 ins, ok := c.instances.Get(id)
497 if !ok {
498 c.logError(r, "instance not found", "id", id)
499 jsonError(w, http.StatusNotFound, "instance not found")
500 return
501 }
502
503 jsonEncode(w, ins.cfg)
504}
505
506func (c *controllerV1) handleDeleteInstances(w http.ResponseWriter, r *http.Request) {
507 id := r.PathValue("id")
508 ins, ok := c.instances.Get(id)
509 if ok {
510 ins.App.Shutdown()
511 }
512 c.instances.Del(id)
513}
514
515func (c *controllerV1) handlePostInstances(w http.ResponseWriter, r *http.Request) {
516 var args proto.Instance
517 if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
518 c.logError(r, "failed to decode request", "error", err)
519 jsonError(w, http.StatusBadRequest, "failed to decode request")
520 return
521 }
522
523 if args.Path == "" {
524 c.logError(r, "path is required")
525 jsonError(w, http.StatusBadRequest, "path is required")
526 return
527 }
528
529 id := uuid.New().String()
530 cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
531 if err != nil {
532 c.logError(r, "failed to initialize config", "error", err)
533 jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
534 return
535 }
536
537 if cfg.Permissions == nil {
538 cfg.Permissions = &config.Permissions{}
539 }
540 cfg.Permissions.SkipRequests = args.YOLO
541
542 if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
543 c.logError(r, "failed to create data directory", "error", err)
544 jsonError(w, http.StatusInternalServerError, "failed to create data directory")
545 return
546 }
547
548 // Connect to DB; this will also run migrations.
549 conn, err := db.Connect(c.ctx, cfg.Options.DataDirectory)
550 if err != nil {
551 c.logError(r, "failed to connect to database", "error", err)
552 jsonError(w, http.StatusInternalServerError, "failed to connect to database")
553 return
554 }
555
556 appInstance, err := app.New(c.ctx, conn, cfg)
557 if err != nil {
558 slog.Error("failed to create app instance", "error", err)
559 jsonError(w, http.StatusInternalServerError, "failed to create app instance")
560 return
561 }
562
563 ins := &Instance{
564 App: appInstance,
565 State: InstanceStateCreated,
566 id: id,
567 path: args.Path,
568 cfg: cfg,
569 }
570
571 c.instances.Set(id, ins)
572 jsonEncode(w, proto.Instance{
573 ID: id,
574 Path: args.Path,
575 DataDir: cfg.Options.DataDirectory,
576 Debug: cfg.Options.Debug,
577 YOLO: cfg.Permissions.SkipRequests,
578 })
579}
580
581func createDotCrushDir(dir string) error {
582 if err := os.MkdirAll(dir, 0o700); err != nil {
583 return fmt.Errorf("failed to create data directory: %q %w", dir, err)
584 }
585
586 gitIgnorePath := filepath.Join(dir, ".gitignore")
587 if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
588 if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
589 return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
590 }
591 }
592
593 return nil
594}
595
596func jsonEncode(w http.ResponseWriter, v any) {
597 w.Header().Set("Content-Type", "application/json")
598 _ = json.NewEncoder(w).Encode(v)
599}
600
601func jsonError(w http.ResponseWriter, status int, message string) {
602 w.Header().Set("Content-Type", "application/json")
603 w.WriteHeader(status)
604 _ = json.NewEncoder(w).Encode(proto.Error{Message: message})
605}