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}