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	var ids []string
508	id := r.URL.Query().Get("id")
509	if id != "" {
510		ids = append(ids, id)
511	}
512
513	// Get IDs from body
514	var args []proto.Instance
515	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
516		c.logError(r, "failed to decode request", "error", err)
517		jsonError(w, http.StatusBadRequest, "failed to decode request")
518		return
519	}
520	ids = append(ids, func() []string {
521		out := make([]string, len(args))
522		for i, arg := range args {
523			out[i] = arg.ID
524		}
525		return out
526	}()...)
527
528	for _, id := range ids {
529		ins, ok := c.instances.Get(id)
530		if ok {
531			ins.App.Shutdown()
532		}
533		c.instances.Del(id)
534	}
535}
536
537func (c *controllerV1) handlePostInstances(w http.ResponseWriter, r *http.Request) {
538	var args proto.Instance
539	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
540		c.logError(r, "failed to decode request", "error", err)
541		jsonError(w, http.StatusBadRequest, "failed to decode request")
542		return
543	}
544
545	if args.Path == "" {
546		c.logError(r, "path is required")
547		jsonError(w, http.StatusBadRequest, "path is required")
548		return
549	}
550
551	id := uuid.New().String()
552	cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
553	if err != nil {
554		c.logError(r, "failed to initialize config", "error", err)
555		jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
556		return
557	}
558
559	if cfg.Permissions == nil {
560		cfg.Permissions = &config.Permissions{}
561	}
562	cfg.Permissions.SkipRequests = args.YOLO
563
564	if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
565		c.logError(r, "failed to create data directory", "error", err)
566		jsonError(w, http.StatusInternalServerError, "failed to create data directory")
567		return
568	}
569
570	// Connect to DB; this will also run migrations.
571	conn, err := db.Connect(c.ctx, cfg.Options.DataDirectory)
572	if err != nil {
573		c.logError(r, "failed to connect to database", "error", err)
574		jsonError(w, http.StatusInternalServerError, "failed to connect to database")
575		return
576	}
577
578	appInstance, err := app.New(c.ctx, conn, cfg)
579	if err != nil {
580		slog.Error("failed to create app instance", "error", err)
581		jsonError(w, http.StatusInternalServerError, "failed to create app instance")
582		return
583	}
584
585	ins := &Instance{
586		App:   appInstance,
587		State: InstanceStateCreated,
588		id:    id,
589		path:  args.Path,
590		cfg:   cfg,
591	}
592
593	c.instances.Set(id, ins)
594	jsonEncode(w, proto.Instance{
595		ID:      id,
596		Path:    args.Path,
597		DataDir: cfg.Options.DataDirectory,
598		Debug:   cfg.Options.Debug,
599		YOLO:    cfg.Permissions.SkipRequests,
600	})
601}
602
603func createDotCrushDir(dir string) error {
604	if err := os.MkdirAll(dir, 0o700); err != nil {
605		return fmt.Errorf("failed to create data directory: %q %w", dir, err)
606	}
607
608	gitIgnorePath := filepath.Join(dir, ".gitignore")
609	if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
610		if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
611			return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
612		}
613	}
614
615	return nil
616}
617
618func jsonEncode(w http.ResponseWriter, v any) {
619	w.Header().Set("Content-Type", "application/json")
620	_ = json.NewEncoder(w).Encode(v)
621}
622
623func jsonError(w http.ResponseWriter, status int, message string) {
624	w.Header().Set("Content-Type", "application/json")
625	w.WriteHeader(status)
626	_ = json.NewEncoder(w).Encode(proto.Error{Message: message})
627}