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