proto.go

  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) handlePostInstanceAgent(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	var msg proto.AgentMessage
151	if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
152		c.logError(r, "failed to decode request", "error", err)
153		jsonError(w, http.StatusBadRequest, "failed to decode request")
154		return
155	}
156
157	if ins.App.CoderAgent == nil {
158		c.logError(r, "coder agent not initialized", "id", id)
159		jsonError(w, http.StatusBadRequest, "coder agent not initialized")
160		return
161	}
162
163	if _, err := ins.App.CoderAgent.Run(r.Context(), msg.SessionID, msg.Prompt, msg.Attachments...); err != nil {
164		c.logError(r, "failed to enqueue message", "error", err, "id", id, "sid", msg.SessionID)
165		jsonError(w, http.StatusInternalServerError, "failed to enqueue message")
166		return
167	}
168}
169
170func (c *controllerV1) handleGetInstanceAgent(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	var agentInfo proto.AgentInfo
180	if ins.App.CoderAgent != nil {
181		agentInfo = proto.AgentInfo{
182			Model:  ins.App.CoderAgent.Model(),
183			IsBusy: ins.App.CoderAgent.IsBusy(),
184		}
185	}
186	jsonEncode(w, agentInfo)
187}
188
189func (c *controllerV1) handlePostInstanceAgentUpdate(w http.ResponseWriter, r *http.Request) {
190	id := r.PathValue("id")
191	ins, ok := c.instances.Get(id)
192	if !ok {
193		c.logError(r, "instance not found", "id", id)
194		jsonError(w, http.StatusNotFound, "instance not found")
195		return
196	}
197
198	if err := ins.App.UpdateAgentModel(); err != nil {
199		c.logError(r, "failed to update agent model", "error", err)
200		jsonError(w, http.StatusInternalServerError, "failed to update agent model")
201		return
202	}
203}
204
205func (c *controllerV1) handlePostInstanceAgentInit(w http.ResponseWriter, r *http.Request) {
206	id := r.PathValue("id")
207	ins, ok := c.instances.Get(id)
208	if !ok {
209		c.logError(r, "instance not found", "id", id)
210		jsonError(w, http.StatusNotFound, "instance not found")
211		return
212	}
213
214	if err := ins.App.InitCoderAgent(); err != nil {
215		c.logError(r, "failed to initialize coder agent", "error", err)
216		jsonError(w, http.StatusInternalServerError, "failed to initialize coder agent")
217		return
218	}
219}
220
221func (c *controllerV1) handleGetInstanceSessionHistory(w http.ResponseWriter, r *http.Request) {
222	id := r.PathValue("id")
223	ins, ok := c.instances.Get(id)
224	if !ok {
225		c.logError(r, "instance not found", "id", id)
226		jsonError(w, http.StatusNotFound, "instance not found")
227		return
228	}
229
230	sid := r.PathValue("sid")
231	historyItems, err := ins.App.History.ListBySession(r.Context(), sid)
232	if err != nil {
233		c.logError(r, "failed to list history", "error", err, "id", id, "sid", sid)
234		jsonError(w, http.StatusInternalServerError, "failed to list history")
235		return
236	}
237
238	jsonEncode(w, historyItems)
239}
240
241func (c *controllerV1) handleGetInstanceSessionMessages(w http.ResponseWriter, r *http.Request) {
242	id := r.PathValue("id")
243	ins, ok := c.instances.Get(id)
244	if !ok {
245		c.logError(r, "instance not found", "id", id)
246		jsonError(w, http.StatusNotFound, "instance not found")
247		return
248	}
249
250	sid := r.PathValue("sid")
251	messages, err := ins.App.Messages.List(r.Context(), sid)
252	if err != nil {
253		c.logError(r, "failed to list messages", "error", err, "id", id, "sid", sid)
254		jsonError(w, http.StatusInternalServerError, "failed to list messages")
255		return
256	}
257
258	jsonEncode(w, messages)
259}
260
261func (c *controllerV1) handleGetInstanceSession(w http.ResponseWriter, r *http.Request) {
262	id := r.PathValue("id")
263	ins, ok := c.instances.Get(id)
264	if !ok {
265		c.logError(r, "instance not found", "id", id)
266		jsonError(w, http.StatusNotFound, "instance not found")
267		return
268	}
269
270	sid := r.PathValue("sid")
271	session, err := ins.App.Sessions.Get(r.Context(), sid)
272	if err != nil {
273		c.logError(r, "failed to get session", "error", err, "id", id, "sid", sid)
274		jsonError(w, http.StatusInternalServerError, "failed to get session")
275		return
276	}
277
278	jsonEncode(w, session)
279}
280
281func (c *controllerV1) handlePostInstanceSessions(w http.ResponseWriter, r *http.Request) {
282	id := r.PathValue("id")
283	ins, ok := c.instances.Get(id)
284	if !ok {
285		c.logError(r, "instance not found", "id", id)
286		jsonError(w, http.StatusNotFound, "instance not found")
287		return
288	}
289
290	var args session.Session
291	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
292		c.logError(r, "failed to decode request", "error", err)
293		jsonError(w, http.StatusBadRequest, "failed to decode request")
294		return
295	}
296
297	sess, err := ins.App.Sessions.Create(r.Context(), args.Title)
298	if err != nil {
299		c.logError(r, "failed to create session", "error", err, "id", id)
300		jsonError(w, http.StatusInternalServerError, "failed to create session")
301		return
302	}
303
304	jsonEncode(w, sess)
305}
306
307func (c *controllerV1) handleGetInstanceSessions(w http.ResponseWriter, r *http.Request) {
308	id := r.PathValue("id")
309	ins, ok := c.instances.Get(id)
310	if !ok {
311		c.logError(r, "instance not found", "id", id)
312		jsonError(w, http.StatusNotFound, "instance not found")
313		return
314	}
315
316	sessions, err := ins.App.Sessions.List(r.Context())
317	if err != nil {
318		c.logError(r, "failed to list sessions", "error", err)
319		jsonError(w, http.StatusInternalServerError, "failed to list sessions")
320		return
321	}
322
323	jsonEncode(w, sessions)
324}
325
326func (c *controllerV1) handleGetInstanceEvents(w http.ResponseWriter, r *http.Request) {
327	flusher := http.NewResponseController(w)
328	id := r.PathValue("id")
329	ins, ok := c.instances.Get(id)
330	if !ok {
331		c.logError(r, "instance not found", "id", id)
332		jsonError(w, http.StatusNotFound, "instance not found")
333		return
334	}
335
336	w.Header().Set("Content-Type", "text/event-stream")
337	w.Header().Set("Cache-Control", "no-cache")
338	w.Header().Set("Connection", "keep-alive")
339
340	for {
341		select {
342		case <-r.Context().Done():
343			return
344		case ev := <-ins.App.Events():
345			data, err := json.Marshal(ev)
346			if err != nil {
347				c.logError(r, "failed to marshal event", "error", err)
348				continue
349			}
350
351			fmt.Fprintf(w, "data: %s\n\n", data)
352			flusher.Flush()
353		}
354	}
355}
356
357func (c *controllerV1) handleDeleteInstances(w http.ResponseWriter, r *http.Request) {
358	var ids []string
359	id := r.URL.Query().Get("id")
360	if id != "" {
361		ids = append(ids, id)
362	}
363
364	// Get IDs from body
365	var args []proto.Instance
366	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
367		c.logError(r, "failed to decode request", "error", err)
368		jsonError(w, http.StatusBadRequest, "failed to decode request")
369		return
370	}
371	ids = append(ids, func() []string {
372		out := make([]string, len(args))
373		for i, arg := range args {
374			out[i] = arg.ID
375		}
376		return out
377	}()...)
378
379	for _, id := range ids {
380		c.instances.Del(id)
381	}
382}
383
384func (c *controllerV1) handlePostInstances(w http.ResponseWriter, r *http.Request) {
385	var args proto.Instance
386	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
387		c.logError(r, "failed to decode request", "error", err)
388		jsonError(w, http.StatusBadRequest, "failed to decode request")
389		return
390	}
391
392	ctx := r.Context()
393	hasher := sha256.New()
394	hasher.Write([]byte(filepath.Clean(args.Path)))
395	id := hex.EncodeToString(hasher.Sum(nil))
396	if existing, ok := c.instances.Get(id); ok {
397		jsonEncode(w, proto.Instance{
398			ID:   existing.ID(),
399			Path: existing.Path(),
400			// TODO: Investigate if this makes sense.
401			YOLO:    existing.cfg.Permissions != nil && existing.cfg.Permissions.SkipRequests,
402			Debug:   existing.cfg.Options.Debug,
403			DataDir: existing.cfg.Options.DataDirectory,
404		})
405		return
406	}
407
408	cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
409	if err != nil {
410		c.logError(r, "failed to initialize config", "error", err)
411		jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
412		return
413	}
414
415	if cfg.Permissions == nil {
416		cfg.Permissions = &config.Permissions{}
417	}
418	cfg.Permissions.SkipRequests = args.YOLO
419
420	if err := createDotCrushDir(args.DataDir); err != nil {
421		c.logError(r, "failed to create data directory", "error", err)
422		jsonError(w, http.StatusInternalServerError, "failed to create data directory")
423		return
424	}
425
426	// Connect to DB; this will also run migrations.
427	conn, err := db.Connect(ctx, args.DataDir)
428	if err != nil {
429		c.logError(r, "failed to connect to database", "error", err)
430		jsonError(w, http.StatusInternalServerError, "failed to connect to database")
431		return
432	}
433
434	appInstance, err := app.New(ctx, conn, cfg)
435	if err != nil {
436		slog.Error("failed to create app instance", "error", err)
437		jsonError(w, http.StatusInternalServerError, "failed to create app instance")
438		return
439	}
440
441	ins := &Instance{
442		App:   appInstance,
443		State: InstanceStateCreated,
444		id:    id,
445		path:  args.Path,
446		cfg:   cfg,
447	}
448
449	c.instances.Set(id, ins)
450	jsonEncode(w, proto.Instance{
451		ID:   id,
452		Path: args.Path,
453		YOLO: cfg.Permissions.SkipRequests,
454	})
455}
456
457func createDotCrushDir(dir string) error {
458	if err := os.MkdirAll(dir, 0o700); err != nil {
459		return fmt.Errorf("failed to create data directory: %q %w", dir, err)
460	}
461
462	gitIgnorePath := filepath.Join(dir, ".gitignore")
463	if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
464		if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
465			return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
466		}
467	}
468
469	return nil
470}
471
472func jsonEncode(w http.ResponseWriter, v any) {
473	w.Header().Set("Content-Type", "application/json")
474	_ = json.NewEncoder(w).Encode(v)
475}
476
477func jsonError(w http.ResponseWriter, status int, message string) {
478	w.Header().Set("Content-Type", "application/json")
479	w.WriteHeader(status)
480	_ = json.NewEncoder(w).Encode(proto.Error{Message: message})
481}