proto.go

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