1package client
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "log/slog"
12 "net/http"
13 "time"
14
15 "github.com/charmbracelet/crush/internal/app"
16 "github.com/charmbracelet/crush/internal/config"
17 "github.com/charmbracelet/crush/internal/history"
18 "github.com/charmbracelet/crush/internal/message"
19 "github.com/charmbracelet/crush/internal/proto"
20 "github.com/charmbracelet/crush/internal/pubsub"
21 "github.com/charmbracelet/crush/internal/session"
22 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
23)
24
25func (c *Client) SubscribeEvents(ctx context.Context) (<-chan any, error) {
26 events := make(chan any, 100)
27 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/events", c.id), nil)
28 if err != nil {
29 return nil, fmt.Errorf("failed to create request: %w", err)
30 }
31
32 r.Header.Set("Accept", "text/event-stream")
33 r.Header.Set("Cache-Control", "no-cache")
34 r.Header.Set("Connection", "keep-alive")
35
36 go func() {
37 rsp, err := c.h.Do(r)
38 if err != nil {
39 slog.Error("subscribing to events", "error", err)
40 return
41 }
42
43 defer rsp.Body.Close()
44
45 if rsp.StatusCode != http.StatusOK {
46 slog.Error("subscribing to events", "status_code", rsp.StatusCode)
47 return
48 }
49
50 scr := bufio.NewReader(rsp.Body)
51 for {
52 line, err := scr.ReadBytes('\n')
53 if errors.Is(err, io.EOF) {
54 break
55 }
56 if err != nil {
57 slog.Error("reading from events stream", "error", err)
58 time.Sleep(time.Second * 2)
59 continue
60 }
61 line = bytes.TrimSpace(line)
62 if len(line) == 0 {
63 // End of an event
64 continue
65 }
66
67 data, ok := bytes.CutPrefix(line, []byte("data:"))
68 if !ok {
69 slog.Warn("invalid event format", "line", string(line))
70 continue
71 }
72
73 data = bytes.TrimSpace(data)
74
75 var event pubsub.Event[any]
76 if err := json.Unmarshal(data, &event); err != nil {
77 slog.Error("unmarshaling event", "error", err)
78 continue
79 }
80
81 type alias pubsub.Event[any]
82 aux := &struct {
83 Payload json.RawMessage `json:"payload"`
84 *alias
85 }{
86 alias: (*alias)(&event),
87 }
88
89 if err := json.Unmarshal(data, &aux); err != nil {
90 slog.Error("unmarshaling event payload", "error", err)
91 continue
92 }
93
94 var p pubsub.Payload
95 if err := json.Unmarshal(aux.Payload, &p); err != nil {
96 slog.Error("unmarshaling event payload", "error", err)
97 continue
98 }
99
100 switch p.Type {
101 case pubsub.PayloadTypeLSPEvent:
102 var e pubsub.Event[proto.LSPEvent]
103 _ = json.Unmarshal(data, &e)
104 sendEvent(ctx, events, e)
105 case pubsub.PayloadTypeMCPEvent:
106 var e pubsub.Event[proto.MCPEvent]
107 _ = json.Unmarshal(data, &e)
108 sendEvent(ctx, events, e)
109 case pubsub.PayloadTypePermissionRequest:
110 var e pubsub.Event[proto.PermissionRequest]
111 _ = json.Unmarshal(data, &e)
112 sendEvent(ctx, events, e)
113 case pubsub.PayloadTypePermissionNotification:
114 var e pubsub.Event[proto.PermissionNotification]
115 _ = json.Unmarshal(data, &e)
116 sendEvent(ctx, events, e)
117 case pubsub.PayloadTypeMessage:
118 var e pubsub.Event[proto.Message]
119 _ = json.Unmarshal(data, &e)
120 sendEvent(ctx, events, e)
121 case pubsub.PayloadTypeSession:
122 var e pubsub.Event[proto.Session]
123 _ = json.Unmarshal(data, &e)
124 sendEvent(ctx, events, e)
125 case pubsub.PayloadTypeFile:
126 var e pubsub.Event[proto.File]
127 _ = json.Unmarshal(data, &e)
128 sendEvent(ctx, events, e)
129 case pubsub.PayloadTypeAgentEvent:
130 var e pubsub.Event[proto.AgentEvent]
131 _ = json.Unmarshal(data, &e)
132 sendEvent(ctx, events, e)
133 default:
134 slog.Warn("unknown event type", "type", p.Type)
135 continue
136 }
137 }
138 }()
139
140 return events, nil
141}
142
143func sendEvent(ctx context.Context, evc chan any, ev any) {
144 slog.Info("event received", "event", fmt.Sprintf("%T %+v", ev, ev))
145 select {
146 case evc <- ev:
147 case <-ctx.Done():
148 close(evc)
149 return
150 }
151}
152
153func (c *Client) GetLSPDiagnostics(ctx context.Context, lsp string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) {
154 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/lsps/%s/diagnostics", c.id, lsp), nil)
155 if err != nil {
156 return nil, fmt.Errorf("failed to create request: %w", err)
157 }
158 rsp, err := c.h.Do(r)
159 if err != nil {
160 return nil, fmt.Errorf("failed to get LSP diagnostics: %w", err)
161 }
162 defer rsp.Body.Close()
163 if rsp.StatusCode != http.StatusOK {
164 return nil, fmt.Errorf("failed to get LSP diagnostics: status code %d", rsp.StatusCode)
165 }
166 var diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
167 if err := json.NewDecoder(rsp.Body).Decode(&diagnostics); err != nil {
168 return nil, fmt.Errorf("failed to decode LSP diagnostics: %w", err)
169 }
170 return diagnostics, nil
171}
172
173func (c *Client) GetLSPs(ctx context.Context) (map[string]app.LSPClientInfo, error) {
174 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/lsps", c.id), nil)
175 if err != nil {
176 return nil, fmt.Errorf("failed to create request: %w", err)
177 }
178 rsp, err := c.h.Do(r)
179 if err != nil {
180 return nil, fmt.Errorf("failed to get LSPs: %w", err)
181 }
182 defer rsp.Body.Close()
183 if rsp.StatusCode != http.StatusOK {
184 return nil, fmt.Errorf("failed to get LSPs: status code %d", rsp.StatusCode)
185 }
186 var lsps map[string]app.LSPClientInfo
187 if err := json.NewDecoder(rsp.Body).Decode(&lsps); err != nil {
188 return nil, fmt.Errorf("failed to decode LSPs: %w", err)
189 }
190 return lsps, nil
191}
192
193func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, sessionID string) (int, error) {
194 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/agent/sessions/%s/prompts/queued", c.id, sessionID), nil)
195 if err != nil {
196 return 0, fmt.Errorf("failed to create request: %w", err)
197 }
198 rsp, err := c.h.Do(r)
199 if err != nil {
200 return 0, fmt.Errorf("failed to get session agent queued prompts: %w", err)
201 }
202 defer rsp.Body.Close()
203 if rsp.StatusCode != http.StatusOK {
204 return 0, fmt.Errorf("failed to get session agent queued prompts: status code %d", rsp.StatusCode)
205 }
206 var count int
207 if err := json.NewDecoder(rsp.Body).Decode(&count); err != nil {
208 return 0, fmt.Errorf("failed to decode session agent queued prompts: %w", err)
209 }
210 return count, nil
211}
212
213func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, sessionID string) error {
214 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent/sessions/%s/prompts/clear", c.id, sessionID), nil)
215 if err != nil {
216 return fmt.Errorf("failed to create request: %w", err)
217 }
218 rsp, err := c.h.Do(r)
219 if err != nil {
220 return fmt.Errorf("failed to clear session agent queued prompts: %w", err)
221 }
222 defer rsp.Body.Close()
223 if rsp.StatusCode != http.StatusOK {
224 return fmt.Errorf("failed to clear session agent queued prompts: status code %d", rsp.StatusCode)
225 }
226 return nil
227}
228
229func (c *Client) GetAgentInfo(ctx context.Context) (*proto.AgentInfo, error) {
230 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/agent", c.id), nil)
231 if err != nil {
232 return nil, fmt.Errorf("failed to create request: %w", err)
233 }
234 rsp, err := c.h.Do(r)
235 if err != nil {
236 return nil, fmt.Errorf("failed to get agent status: %w", err)
237 }
238 defer rsp.Body.Close()
239 if rsp.StatusCode != http.StatusOK {
240 return nil, fmt.Errorf("failed to get agent status: status code %d", rsp.StatusCode)
241 }
242 var info proto.AgentInfo
243 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
244 return nil, fmt.Errorf("failed to decode agent status: %w", err)
245 }
246 return &info, nil
247}
248
249func (c *Client) UpdateAgent(ctx context.Context) error {
250 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent/update", c.id), nil)
251 if err != nil {
252 return fmt.Errorf("failed to create request: %w", err)
253 }
254 rsp, err := c.h.Do(r)
255 if err != nil {
256 return fmt.Errorf("failed to update agent: %w", err)
257 }
258 defer rsp.Body.Close()
259 if rsp.StatusCode != http.StatusOK {
260 return fmt.Errorf("failed to update agent: status code %d", rsp.StatusCode)
261 }
262 return nil
263}
264
265func (c *Client) SendMessage(ctx context.Context, sessionID, message string, attchments ...message.Attachment) error {
266 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent", c.id), jsonBody(proto.AgentMessage{
267 SessionID: sessionID,
268 Prompt: message,
269 Attachments: attchments,
270 }))
271 if err != nil {
272 return fmt.Errorf("failed to create request: %w", err)
273 }
274 rsp, err := c.h.Do(r)
275 if err != nil {
276 return fmt.Errorf("failed to send message to agent: %w", err)
277 }
278 defer rsp.Body.Close()
279 if rsp.StatusCode != http.StatusOK {
280 return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
281 }
282 return nil
283}
284
285func (c *Client) GetAgentSessionInfo(ctx context.Context, sessionID string) (*proto.AgentSession, error) {
286 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/agent/sessions/%s", c.id, sessionID), nil)
287 if err != nil {
288 return nil, fmt.Errorf("failed to create request: %w", err)
289 }
290 rsp, err := c.h.Do(r)
291 if err != nil {
292 return nil, fmt.Errorf("failed to get session agent info: %w", err)
293 }
294 defer rsp.Body.Close()
295 if rsp.StatusCode != http.StatusOK {
296 return nil, fmt.Errorf("failed to get session agent info: status code %d", rsp.StatusCode)
297 }
298 var info proto.AgentSession
299 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
300 return nil, fmt.Errorf("failed to decode session agent info: %w", err)
301 }
302 return &info, nil
303}
304
305func (c *Client) AgentSummarizeSession(ctx context.Context, sessionID string) error {
306 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent/sessions/%s/summarize", c.id, sessionID), nil)
307 if err != nil {
308 return fmt.Errorf("failed to create request: %w", err)
309 }
310 rsp, err := c.h.Do(r)
311 if err != nil {
312 return fmt.Errorf("failed to summarize session: %w", err)
313 }
314 defer rsp.Body.Close()
315 if rsp.StatusCode != http.StatusOK {
316 return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
317 }
318 return nil
319}
320
321func (c *Client) ListMessages(ctx context.Context, sessionID string) ([]message.Message, error) {
322 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/sessions/%s/messages", c.id, sessionID), nil)
323 if err != nil {
324 return nil, fmt.Errorf("failed to create request: %w", err)
325 }
326 rsp, err := c.h.Do(r)
327 if err != nil {
328 return nil, fmt.Errorf("failed to get messages: %w", err)
329 }
330 defer rsp.Body.Close()
331 if rsp.StatusCode != http.StatusOK {
332 return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
333 }
334 var messages []message.Message
335 if err := json.NewDecoder(rsp.Body).Decode(&messages); err != nil {
336 return nil, fmt.Errorf("failed to decode messages: %w", err)
337 }
338 return messages, nil
339}
340
341func (c *Client) GetSession(ctx context.Context, sessionID string) (*session.Session, error) {
342 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/sessions/%s", c.id, sessionID), nil)
343 if err != nil {
344 return nil, fmt.Errorf("failed to create request: %w", err)
345 }
346 rsp, err := c.h.Do(r)
347 if err != nil {
348 return nil, fmt.Errorf("failed to get session: %w", err)
349 }
350 defer rsp.Body.Close()
351 if rsp.StatusCode != http.StatusOK {
352 return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
353 }
354 var sess session.Session
355 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
356 return nil, fmt.Errorf("failed to decode session: %w", err)
357 }
358 return &sess, nil
359}
360
361func (c *Client) InitiateAgentProcessing(ctx context.Context) error {
362 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent/init", c.id), nil)
363 if err != nil {
364 return fmt.Errorf("failed to create request: %w", err)
365 }
366 rsp, err := c.h.Do(r)
367 if err != nil {
368 return fmt.Errorf("failed to initiate session agent processing: %w", err)
369 }
370 defer rsp.Body.Close()
371 if rsp.StatusCode != http.StatusOK {
372 return fmt.Errorf("failed to initiate session agent processing: status code %d", rsp.StatusCode)
373 }
374 return nil
375}
376
377func (c *Client) ListSessionHistoryFiles(ctx context.Context, sessionID string) ([]history.File, error) {
378 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/sessions/%s/history", c.id, sessionID), nil)
379 if err != nil {
380 return nil, fmt.Errorf("failed to create request: %w", err)
381 }
382 rsp, err := c.h.Do(r)
383 if err != nil {
384 return nil, fmt.Errorf("failed to get session history files: %w", err)
385 }
386 defer rsp.Body.Close()
387 if rsp.StatusCode != http.StatusOK {
388 return nil, fmt.Errorf("failed to get session history files: status code %d", rsp.StatusCode)
389 }
390 var files []history.File
391 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
392 return nil, fmt.Errorf("failed to decode session history files: %w", err)
393 }
394 return files, nil
395}
396
397func (c *Client) CreateSession(ctx context.Context, title string) (*session.Session, error) {
398 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/sessions", c.id), jsonBody(session.Session{Title: title}))
399 if err != nil {
400 return nil, fmt.Errorf("failed to create request: %w", err)
401 }
402 r.Header.Set("Content-Type", "application/json")
403 rsp, err := c.h.Do(r)
404 if err != nil {
405 return nil, fmt.Errorf("failed to create session: %w", err)
406 }
407 defer rsp.Body.Close()
408 if rsp.StatusCode != http.StatusOK {
409 return nil, fmt.Errorf("failed to create session: status code %d", rsp.StatusCode)
410 }
411 var sess session.Session
412 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
413 return nil, fmt.Errorf("failed to decode session: %w", err)
414 }
415 return &sess, nil
416}
417
418func (c *Client) ListSessions(ctx context.Context) ([]session.Session, error) {
419 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/sessions", c.id), nil)
420 if err != nil {
421 return nil, fmt.Errorf("failed to create request: %w", err)
422 }
423 rsp, err := c.h.Do(r)
424 if err != nil {
425 return nil, fmt.Errorf("failed to get sessions: %w", err)
426 }
427 defer rsp.Body.Close()
428 if rsp.StatusCode != http.StatusOK {
429 return nil, fmt.Errorf("failed to get sessions: status code %d", rsp.StatusCode)
430 }
431 var sessions []session.Session
432 if err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {
433 return nil, fmt.Errorf("failed to decode sessions: %w", err)
434 }
435 return sessions, nil
436}
437
438func (c *Client) GrantPermission(ctx context.Context, req proto.PermissionGrant) error {
439 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/permissions/grant", c.id), jsonBody(req))
440 if err != nil {
441 return fmt.Errorf("failed to create request: %w", err)
442 }
443 r.Header.Set("Content-Type", "application/json")
444 rsp, err := c.h.Do(r)
445 if err != nil {
446 return fmt.Errorf("failed to grant permission: %w", err)
447 }
448 defer rsp.Body.Close()
449 if rsp.StatusCode != http.StatusOK {
450 return fmt.Errorf("failed to grant permission: status code %d", rsp.StatusCode)
451 }
452 return nil
453}
454
455func (c *Client) SetPermissionsSkipRequests(ctx context.Context, skip bool) error {
456 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/permissions/skip", c.id), jsonBody(proto.PermissionSkipRequest{Skip: skip}))
457 if err != nil {
458 return fmt.Errorf("failed to create request: %w", err)
459 }
460 r.Header.Set("Content-Type", "application/json")
461 rsp, err := c.h.Do(r)
462 if err != nil {
463 return fmt.Errorf("failed to set permissions skip requests: %w", err)
464 }
465 defer rsp.Body.Close()
466 if rsp.StatusCode != http.StatusOK {
467 return fmt.Errorf("failed to set permissions skip requests: status code %d", rsp.StatusCode)
468 }
469 return nil
470}
471
472func (c *Client) GetPermissionsSkipRequests(ctx context.Context) (bool, error) {
473 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/permissions/skip", c.id), nil)
474 if err != nil {
475 return false, fmt.Errorf("failed to create request: %w", err)
476 }
477 rsp, err := c.h.Do(r)
478 if err != nil {
479 return false, fmt.Errorf("failed to get permissions skip requests: %w", err)
480 }
481 defer rsp.Body.Close()
482 if rsp.StatusCode != http.StatusOK {
483 return false, fmt.Errorf("failed to get permissions skip requests: status code %d", rsp.StatusCode)
484 }
485 var skip proto.PermissionSkipRequest
486 if err := json.NewDecoder(rsp.Body).Decode(&skip); err != nil {
487 return false, fmt.Errorf("failed to decode permissions skip requests: %w", err)
488 }
489 return skip.Skip, nil
490}
491
492func (c *Client) GetConfig(ctx context.Context) (*config.Config, error) {
493 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/config", c.id), nil)
494 if err != nil {
495 return nil, fmt.Errorf("failed to create request: %w", err)
496 }
497 rsp, err := c.h.Do(r)
498 if err != nil {
499 return nil, fmt.Errorf("failed to get config: %w", err)
500 }
501 defer rsp.Body.Close()
502 if rsp.StatusCode != http.StatusOK {
503 return nil, fmt.Errorf("failed to get config: status code %d", rsp.StatusCode)
504 }
505 var cfg config.Config
506 if err := json.NewDecoder(rsp.Body).Decode(&cfg); err != nil {
507 return nil, fmt.Errorf("failed to decode config: %w", err)
508 }
509 return &cfg, nil
510}
511
512func (c *Client) CreateInstance(ctx context.Context, ins proto.Instance) (*proto.Instance, error) {
513 r, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/v1/instances", jsonBody(ins))
514 if err != nil {
515 return nil, fmt.Errorf("failed to create request: %w", err)
516 }
517
518 r.Header.Set("Content-Type", "application/json")
519 rsp, err := c.h.Do(r)
520 if err != nil {
521 return nil, fmt.Errorf("failed to create instance: %w", err)
522 }
523 defer rsp.Body.Close()
524 if rsp.StatusCode != http.StatusOK {
525 return nil, fmt.Errorf("failed to create instance: status code %d", rsp.StatusCode)
526 }
527 var created proto.Instance
528 if err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {
529 return nil, fmt.Errorf("failed to decode instance: %w", err)
530 }
531 return &created, nil
532}
533
534func (c *Client) DeleteInstance(ctx context.Context, id string) error {
535 r, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("http://localhost/v1/instances/%s", id), nil)
536 if err != nil {
537 return fmt.Errorf("failed to create request: %w", err)
538 }
539 rsp, err := c.h.Do(r)
540 if err != nil {
541 return fmt.Errorf("failed to delete instance: %w", err)
542 }
543 defer rsp.Body.Close()
544 if rsp.StatusCode != http.StatusOK {
545 return fmt.Errorf("failed to delete instance: status code %d", rsp.StatusCode)
546 }
547 return nil
548}
549
550func jsonBody(v any) *bytes.Buffer {
551 b := new(bytes.Buffer)
552 m, _ := json.Marshal(v)
553 b.Write(m)
554 return b
555}