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