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) 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}