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/history"
17 "github.com/charmbracelet/crush/internal/message"
18 "github.com/charmbracelet/crush/internal/proto"
19 "github.com/charmbracelet/crush/internal/session"
20 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
21)
22
23func (c *Client) SubscribeEvents(ctx context.Context) (<-chan any, error) {
24 events := make(chan any, 100)
25 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/events", c.id), nil)
26 if err != nil {
27 return nil, fmt.Errorf("failed to create request: %w", err)
28 }
29 rsp, err := c.h.Do(r)
30 if err != nil {
31 return nil, fmt.Errorf("failed to subscribe to events: %w", err)
32 }
33 if rsp.StatusCode != http.StatusOK {
34 rsp.Body.Close()
35 return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
36 }
37
38 go func() {
39 defer rsp.Body.Close()
40
41 scr := bufio.NewReader(rsp.Body)
42 for {
43 line, err := scr.ReadBytes('\n')
44 if errors.Is(err, io.EOF) {
45 break
46 }
47 if err != nil {
48 slog.Error("reading from events stream", "error", err)
49 time.Sleep(time.Second * 2)
50 continue
51 }
52 line = bytes.TrimSpace(line)
53 if len(line) == 0 {
54 // End of an event
55 continue
56 }
57
58 data, ok := bytes.CutPrefix(line, []byte("data:"))
59 if !ok {
60 slog.Warn("invalid event format", "line", string(line))
61 continue
62 }
63
64 data = bytes.TrimSpace(data)
65
66 var event any
67 if err := json.Unmarshal(data, &event); err != nil {
68 slog.Error("unmarshaling event", "error", err)
69 continue
70 }
71
72 select {
73 case events <- event:
74 case <-ctx.Done():
75 close(events)
76 return
77 }
78 }
79 }()
80
81 return events, nil
82}
83
84func (c *Client) GetLSPDiagnostics(ctx context.Context, lsp string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) {
85 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/lsps/%s/diagnostics", c.id, lsp), nil)
86 if err != nil {
87 return nil, fmt.Errorf("failed to create request: %w", err)
88 }
89 rsp, err := c.h.Do(r)
90 if err != nil {
91 return nil, fmt.Errorf("failed to get LSP diagnostics: %w", err)
92 }
93 defer rsp.Body.Close()
94 if rsp.StatusCode != http.StatusOK {
95 return nil, fmt.Errorf("failed to get LSP diagnostics: status code %d", rsp.StatusCode)
96 }
97 var diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
98 if err := json.NewDecoder(rsp.Body).Decode(&diagnostics); err != nil {
99 return nil, fmt.Errorf("failed to decode LSP diagnostics: %w", err)
100 }
101 return diagnostics, nil
102}
103
104func (c *Client) GetLSPs(ctx context.Context) (map[string]app.LSPClientInfo, error) {
105 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/lsps", c.id), nil)
106 if err != nil {
107 return nil, fmt.Errorf("failed to create request: %w", err)
108 }
109 rsp, err := c.h.Do(r)
110 if err != nil {
111 return nil, fmt.Errorf("failed to get LSPs: %w", err)
112 }
113 defer rsp.Body.Close()
114 if rsp.StatusCode != http.StatusOK {
115 return nil, fmt.Errorf("failed to get LSPs: status code %d", rsp.StatusCode)
116 }
117 var lsps map[string]app.LSPClientInfo
118 if err := json.NewDecoder(rsp.Body).Decode(&lsps); err != nil {
119 return nil, fmt.Errorf("failed to decode LSPs: %w", err)
120 }
121 return lsps, nil
122}
123
124func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, sessionID string) (int, error) {
125 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/agent/sessions/%s/prompts/queued", c.id, sessionID), nil)
126 if err != nil {
127 return 0, fmt.Errorf("failed to create request: %w", err)
128 }
129 rsp, err := c.h.Do(r)
130 if err != nil {
131 return 0, fmt.Errorf("failed to get session agent queued prompts: %w", err)
132 }
133 defer rsp.Body.Close()
134 if rsp.StatusCode != http.StatusOK {
135 return 0, fmt.Errorf("failed to get session agent queued prompts: status code %d", rsp.StatusCode)
136 }
137 var count int
138 if err := json.NewDecoder(rsp.Body).Decode(&count); err != nil {
139 return 0, fmt.Errorf("failed to decode session agent queued prompts: %w", err)
140 }
141 return count, nil
142}
143
144func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, sessionID string) error {
145 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent/sessions/%s/prompts/clear", c.id, sessionID), nil)
146 if err != nil {
147 return fmt.Errorf("failed to create request: %w", err)
148 }
149 rsp, err := c.h.Do(r)
150 if err != nil {
151 return fmt.Errorf("failed to clear session agent queued prompts: %w", err)
152 }
153 defer rsp.Body.Close()
154 if rsp.StatusCode != http.StatusOK {
155 return fmt.Errorf("failed to clear session agent queued prompts: status code %d", rsp.StatusCode)
156 }
157 return nil
158}
159
160func (c *Client) GetAgentInfo(ctx context.Context) (*proto.AgentInfo, error) {
161 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/agent", c.id), nil)
162 if err != nil {
163 return nil, fmt.Errorf("failed to create request: %w", err)
164 }
165 rsp, err := c.h.Do(r)
166 if err != nil {
167 return nil, fmt.Errorf("failed to get agent status: %w", err)
168 }
169 defer rsp.Body.Close()
170 if rsp.StatusCode != http.StatusOK {
171 return nil, fmt.Errorf("failed to get agent status: status code %d", rsp.StatusCode)
172 }
173 var info proto.AgentInfo
174 if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
175 return nil, fmt.Errorf("failed to decode agent status: %w", err)
176 }
177 return &info, nil
178}
179
180func (c *Client) UpdateAgent(ctx context.Context) error {
181 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent/update", c.id), nil)
182 if err != nil {
183 return fmt.Errorf("failed to create request: %w", err)
184 }
185 rsp, err := c.h.Do(r)
186 if err != nil {
187 return fmt.Errorf("failed to update agent: %w", err)
188 }
189 defer rsp.Body.Close()
190 if rsp.StatusCode != http.StatusOK {
191 return fmt.Errorf("failed to update agent: status code %d", rsp.StatusCode)
192 }
193 return nil
194}
195
196func (c *Client) SendMessage(ctx context.Context, sessionID, message string, attchments ...message.Attachment) error {
197 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent", c.id), jsonBody(proto.AgentMessage{
198 SessionID: sessionID,
199 Prompt: message,
200 Attachments: attchments,
201 }))
202 if err != nil {
203 return fmt.Errorf("failed to create request: %w", err)
204 }
205 rsp, err := c.h.Do(r)
206 if err != nil {
207 return fmt.Errorf("failed to send message to agent: %w", err)
208 }
209 defer rsp.Body.Close()
210 if rsp.StatusCode != http.StatusOK {
211 return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
212 }
213 return nil
214}
215
216func (c *Client) AgentSummarizeSession(ctx context.Context, sessionID string) error {
217 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent/sessions/%s/summarize", c.id, sessionID), nil)
218 if err != nil {
219 return fmt.Errorf("failed to create request: %w", err)
220 }
221 rsp, err := c.h.Do(r)
222 if err != nil {
223 return fmt.Errorf("failed to summarize session: %w", err)
224 }
225 defer rsp.Body.Close()
226 if rsp.StatusCode != http.StatusOK {
227 return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
228 }
229 return nil
230}
231
232func (c *Client) ListMessages(ctx context.Context, sessionID string) ([]message.Message, error) {
233 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/sessions/%s/messages", c.id, sessionID), nil)
234 if err != nil {
235 return nil, fmt.Errorf("failed to create request: %w", err)
236 }
237 rsp, err := c.h.Do(r)
238 if err != nil {
239 return nil, fmt.Errorf("failed to get messages: %w", err)
240 }
241 defer rsp.Body.Close()
242 if rsp.StatusCode != http.StatusOK {
243 return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
244 }
245 var messages []message.Message
246 if err := json.NewDecoder(rsp.Body).Decode(&messages); err != nil {
247 return nil, fmt.Errorf("failed to decode messages: %w", err)
248 }
249 return messages, nil
250}
251
252func (c *Client) GetSession(ctx context.Context, sessionID string) (*session.Session, error) {
253 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/sessions/%s", c.id, sessionID), nil)
254 if err != nil {
255 return nil, fmt.Errorf("failed to create request: %w", err)
256 }
257 rsp, err := c.h.Do(r)
258 if err != nil {
259 return nil, fmt.Errorf("failed to get session: %w", err)
260 }
261 defer rsp.Body.Close()
262 if rsp.StatusCode != http.StatusOK {
263 return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
264 }
265 var sess session.Session
266 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
267 return nil, fmt.Errorf("failed to decode session: %w", err)
268 }
269 return &sess, nil
270}
271
272func (c *Client) InitiateAgentProcessing(ctx context.Context) error {
273 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/agent/init", c.id), nil)
274 if err != nil {
275 return fmt.Errorf("failed to create request: %w", err)
276 }
277 rsp, err := c.h.Do(r)
278 if err != nil {
279 return fmt.Errorf("failed to initiate session agent processing: %w", err)
280 }
281 defer rsp.Body.Close()
282 if rsp.StatusCode != http.StatusOK {
283 return fmt.Errorf("failed to initiate session agent processing: status code %d", rsp.StatusCode)
284 }
285 return nil
286}
287
288func (c *Client) ListSessionHistoryFiles(ctx context.Context, sessionID string) ([]history.File, error) {
289 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/sessions/%s/history", c.id, sessionID), nil)
290 if err != nil {
291 return nil, fmt.Errorf("failed to create request: %w", err)
292 }
293 rsp, err := c.h.Do(r)
294 if err != nil {
295 return nil, fmt.Errorf("failed to get session history files: %w", err)
296 }
297 defer rsp.Body.Close()
298 if rsp.StatusCode != http.StatusOK {
299 return nil, fmt.Errorf("failed to get session history files: status code %d", rsp.StatusCode)
300 }
301 var files []history.File
302 if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
303 return nil, fmt.Errorf("failed to decode session history files: %w", err)
304 }
305 return files, nil
306}
307
308func (c *Client) CreateSession(ctx context.Context, title string) (*session.Session, error) {
309 r, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://localhost/v1/instances/%s/sessions", c.id), jsonBody(session.Session{Title: title}))
310 if err != nil {
311 return nil, fmt.Errorf("failed to create request: %w", err)
312 }
313 r.Header.Set("Content-Type", "application/json")
314 rsp, err := c.h.Do(r)
315 if err != nil {
316 return nil, fmt.Errorf("failed to create session: %w", err)
317 }
318 defer rsp.Body.Close()
319 if rsp.StatusCode != http.StatusOK {
320 return nil, fmt.Errorf("failed to create session: status code %d", rsp.StatusCode)
321 }
322 var sess session.Session
323 if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
324 return nil, fmt.Errorf("failed to decode session: %w", err)
325 }
326 return &sess, nil
327}
328
329func (c *Client) ListSessions(ctx context.Context) ([]session.Session, error) {
330 r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost/v1/instances/%s/sessions", c.id), nil)
331 if err != nil {
332 return nil, fmt.Errorf("failed to create request: %w", err)
333 }
334 rsp, err := c.h.Do(r)
335 if err != nil {
336 return nil, fmt.Errorf("failed to get sessions: %w", err)
337 }
338 defer rsp.Body.Close()
339 if rsp.StatusCode != http.StatusOK {
340 return nil, fmt.Errorf("failed to get sessions: status code %d", rsp.StatusCode)
341 }
342 var sessions []session.Session
343 if err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {
344 return nil, fmt.Errorf("failed to decode sessions: %w", err)
345 }
346 return sessions, nil
347}
348
349func (c *Client) CreateInstance(ctx context.Context, ins proto.Instance) (*proto.Instance, error) {
350 r, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/v1/instances", jsonBody(ins))
351 if err != nil {
352 return nil, fmt.Errorf("failed to create request: %w", err)
353 }
354
355 r.Header.Set("Content-Type", "application/json")
356 rsp, err := c.h.Do(r)
357 if err != nil {
358 return nil, fmt.Errorf("failed to create instance: %w", err)
359 }
360 defer rsp.Body.Close()
361 if rsp.StatusCode != http.StatusOK {
362 return nil, fmt.Errorf("failed to create instance: status code %d", rsp.StatusCode)
363 }
364 var created proto.Instance
365 if err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {
366 return nil, fmt.Errorf("failed to decode instance: %w", err)
367 }
368 return &created, nil
369}
370
371func (c *Client) DeleteInstance(ctx context.Context, id string) error {
372 r, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("http://localhost/v1/instances/%s", id), nil)
373 if err != nil {
374 return fmt.Errorf("failed to create request: %w", err)
375 }
376 rsp, err := c.h.Do(r)
377 if err != nil {
378 return fmt.Errorf("failed to delete instance: %w", err)
379 }
380 defer rsp.Body.Close()
381 if rsp.StatusCode != http.StatusOK {
382 return fmt.Errorf("failed to delete instance: status code %d", rsp.StatusCode)
383 }
384 return nil
385}
386
387func (c *Client) DeleteInstances(ctx context.Context, ids []string) error {
388 r, err := http.NewRequestWithContext(ctx, "DELETE", "http://localhost/v1/instances", jsonBody(ids))
389 if err != nil {
390 return fmt.Errorf("failed to create request: %w", err)
391 }
392 r.Header.Set("Content-Type", "application/json")
393 rsp, err := c.h.Do(r)
394 if err != nil {
395 return fmt.Errorf("failed to delete instances: %w", err)
396 }
397 defer rsp.Body.Close()
398 if rsp.StatusCode != http.StatusOK {
399 return fmt.Errorf("failed to delete instances: status code %d", rsp.StatusCode)
400 }
401 return nil
402}
403
404func jsonBody(v any) *bytes.Buffer {
405 b := new(bytes.Buffer)
406 m, _ := json.Marshal(v)
407 b.Write(m)
408 return b
409}