proto.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. package client
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "log/slog"
  11. "net/http"
  12. "net/url"
  13. "time"
  14. "github.com/charmbracelet/crush/internal/config"
  15. "github.com/charmbracelet/crush/internal/message"
  16. "github.com/charmbracelet/crush/internal/proto"
  17. "github.com/charmbracelet/crush/internal/pubsub"
  18. "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
  19. )
  20. // ListWorkspaces retrieves all workspaces from the server.
  21. func (c *Client) ListWorkspaces(ctx context.Context) ([]proto.Workspace, error) {
  22. rsp, err := c.get(ctx, "/workspaces", nil, nil)
  23. if err != nil {
  24. return nil, fmt.Errorf("failed to list workspaces: %w", err)
  25. }
  26. defer rsp.Body.Close()
  27. if rsp.StatusCode != http.StatusOK {
  28. return nil, fmt.Errorf("failed to list workspaces: status code %d", rsp.StatusCode)
  29. }
  30. var workspaces []proto.Workspace
  31. if err := json.NewDecoder(rsp.Body).Decode(&workspaces); err != nil {
  32. return nil, fmt.Errorf("failed to decode workspaces: %w", err)
  33. }
  34. return workspaces, nil
  35. }
  36. // CreateWorkspace creates a new workspace on the server.
  37. func (c *Client) CreateWorkspace(ctx context.Context, ws proto.Workspace) (*proto.Workspace, error) {
  38. rsp, err := c.post(ctx, "/workspaces", nil, jsonBody(ws), http.Header{"Content-Type": []string{"application/json"}})
  39. if err != nil {
  40. return nil, fmt.Errorf("failed to create workspace: %w", err)
  41. }
  42. defer rsp.Body.Close()
  43. if rsp.StatusCode != http.StatusOK {
  44. return nil, fmt.Errorf("failed to create workspace: status code %d", rsp.StatusCode)
  45. }
  46. var created proto.Workspace
  47. if err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {
  48. return nil, fmt.Errorf("failed to decode workspace: %w", err)
  49. }
  50. return &created, nil
  51. }
  52. // GetWorkspace retrieves a workspace from the server.
  53. func (c *Client) GetWorkspace(ctx context.Context, id string) (*proto.Workspace, error) {
  54. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s", id), nil, nil)
  55. if err != nil {
  56. return nil, fmt.Errorf("failed to get workspace: %w", err)
  57. }
  58. defer rsp.Body.Close()
  59. if rsp.StatusCode != http.StatusOK {
  60. return nil, fmt.Errorf("failed to get workspace: status code %d", rsp.StatusCode)
  61. }
  62. var ws proto.Workspace
  63. if err := json.NewDecoder(rsp.Body).Decode(&ws); err != nil {
  64. return nil, fmt.Errorf("failed to decode workspace: %w", err)
  65. }
  66. return &ws, nil
  67. }
  68. // DeleteWorkspace deletes a workspace on the server.
  69. func (c *Client) DeleteWorkspace(ctx context.Context, id string) error {
  70. rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s", id), nil, nil)
  71. if err != nil {
  72. return fmt.Errorf("failed to delete workspace: %w", err)
  73. }
  74. defer rsp.Body.Close()
  75. if rsp.StatusCode != http.StatusOK {
  76. return fmt.Errorf("failed to delete workspace: status code %d", rsp.StatusCode)
  77. }
  78. return nil
  79. }
  80. // SubscribeEvents subscribes to server-sent events for a workspace.
  81. func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, error) {
  82. events := make(chan any, 100)
  83. //nolint:bodyclose
  84. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/events", id), nil, http.Header{
  85. "Accept": []string{"text/event-stream"},
  86. "Cache-Control": []string{"no-cache"},
  87. "Connection": []string{"keep-alive"},
  88. })
  89. if err != nil {
  90. return nil, fmt.Errorf("failed to subscribe to events: %w", err)
  91. }
  92. if rsp.StatusCode != http.StatusOK {
  93. rsp.Body.Close()
  94. return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
  95. }
  96. go func() {
  97. defer rsp.Body.Close()
  98. scr := bufio.NewReader(rsp.Body)
  99. for {
  100. line, err := scr.ReadBytes('\n')
  101. if errors.Is(err, io.EOF) {
  102. break
  103. }
  104. if err != nil {
  105. slog.Error("Reading from events stream", "error", err)
  106. time.Sleep(time.Second * 2)
  107. continue
  108. }
  109. line = bytes.TrimSpace(line)
  110. if len(line) == 0 {
  111. continue
  112. }
  113. data, ok := bytes.CutPrefix(line, []byte("data:"))
  114. if !ok {
  115. slog.Warn("Invalid event format", "line", string(line))
  116. continue
  117. }
  118. data = bytes.TrimSpace(data)
  119. var p pubsub.Payload
  120. if err := json.Unmarshal(data, &p); err != nil {
  121. slog.Error("Unmarshaling event envelope", "error", err)
  122. continue
  123. }
  124. switch p.Type {
  125. case pubsub.PayloadTypeLSPEvent:
  126. var e pubsub.Event[proto.LSPEvent]
  127. _ = json.Unmarshal(p.Payload, &e)
  128. sendEvent(ctx, events, e)
  129. case pubsub.PayloadTypeMCPEvent:
  130. var e pubsub.Event[proto.MCPEvent]
  131. _ = json.Unmarshal(p.Payload, &e)
  132. sendEvent(ctx, events, e)
  133. case pubsub.PayloadTypePermissionRequest:
  134. var e pubsub.Event[proto.PermissionRequest]
  135. _ = json.Unmarshal(p.Payload, &e)
  136. sendEvent(ctx, events, e)
  137. case pubsub.PayloadTypePermissionNotification:
  138. var e pubsub.Event[proto.PermissionNotification]
  139. _ = json.Unmarshal(p.Payload, &e)
  140. sendEvent(ctx, events, e)
  141. case pubsub.PayloadTypeMessage:
  142. var e pubsub.Event[proto.Message]
  143. _ = json.Unmarshal(p.Payload, &e)
  144. sendEvent(ctx, events, e)
  145. case pubsub.PayloadTypeSession:
  146. var e pubsub.Event[proto.Session]
  147. _ = json.Unmarshal(p.Payload, &e)
  148. sendEvent(ctx, events, e)
  149. case pubsub.PayloadTypeFile:
  150. var e pubsub.Event[proto.File]
  151. _ = json.Unmarshal(p.Payload, &e)
  152. sendEvent(ctx, events, e)
  153. case pubsub.PayloadTypeAgentEvent:
  154. var e pubsub.Event[proto.AgentEvent]
  155. _ = json.Unmarshal(p.Payload, &e)
  156. sendEvent(ctx, events, e)
  157. default:
  158. slog.Warn("Unknown event type", "type", p.Type)
  159. continue
  160. }
  161. }
  162. }()
  163. return events, nil
  164. }
  165. func sendEvent(ctx context.Context, evc chan any, ev any) {
  166. slog.Info("Event received", "event", fmt.Sprintf("%T %+v", ev, ev))
  167. select {
  168. case evc <- ev:
  169. case <-ctx.Done():
  170. close(evc)
  171. return
  172. }
  173. }
  174. // GetLSPDiagnostics retrieves LSP diagnostics for a specific LSP client.
  175. func (c *Client) GetLSPDiagnostics(ctx context.Context, id string, lspName string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) {
  176. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps/%s/diagnostics", id, lspName), nil, nil)
  177. if err != nil {
  178. return nil, fmt.Errorf("failed to get LSP diagnostics: %w", err)
  179. }
  180. defer rsp.Body.Close()
  181. if rsp.StatusCode != http.StatusOK {
  182. return nil, fmt.Errorf("failed to get LSP diagnostics: status code %d", rsp.StatusCode)
  183. }
  184. var diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
  185. if err := json.NewDecoder(rsp.Body).Decode(&diagnostics); err != nil {
  186. return nil, fmt.Errorf("failed to decode LSP diagnostics: %w", err)
  187. }
  188. return diagnostics, nil
  189. }
  190. // GetLSPs retrieves the LSP client states for a workspace.
  191. func (c *Client) GetLSPs(ctx context.Context, id string) (map[string]proto.LSPClientInfo, error) {
  192. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/lsps", id), nil, nil)
  193. if err != nil {
  194. return nil, fmt.Errorf("failed to get LSPs: %w", err)
  195. }
  196. defer rsp.Body.Close()
  197. if rsp.StatusCode != http.StatusOK {
  198. return nil, fmt.Errorf("failed to get LSPs: status code %d", rsp.StatusCode)
  199. }
  200. var lsps map[string]proto.LSPClientInfo
  201. if err := json.NewDecoder(rsp.Body).Decode(&lsps); err != nil {
  202. return nil, fmt.Errorf("failed to decode LSPs: %w", err)
  203. }
  204. return lsps, nil
  205. }
  206. // MCPGetStates retrieves the MCP client states for a workspace.
  207. func (c *Client) MCPGetStates(ctx context.Context, id string) (map[string]proto.MCPClientInfo, error) {
  208. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/mcp/states", id), nil, nil)
  209. if err != nil {
  210. return nil, fmt.Errorf("failed to get MCP states: %w", err)
  211. }
  212. defer rsp.Body.Close()
  213. if rsp.StatusCode != http.StatusOK {
  214. return nil, fmt.Errorf("failed to get MCP states: status code %d", rsp.StatusCode)
  215. }
  216. var states map[string]proto.MCPClientInfo
  217. if err := json.NewDecoder(rsp.Body).Decode(&states); err != nil {
  218. return nil, fmt.Errorf("failed to decode MCP states: %w", err)
  219. }
  220. return states, nil
  221. }
  222. // MCPRefreshPrompts refreshes prompts for a named MCP client.
  223. func (c *Client) MCPRefreshPrompts(ctx context.Context, id, name string) error {
  224. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/refresh-prompts", id), nil,
  225. jsonBody(struct {
  226. Name string `json:"name"`
  227. }{Name: name}),
  228. http.Header{"Content-Type": []string{"application/json"}})
  229. if err != nil {
  230. return fmt.Errorf("failed to refresh MCP prompts: %w", err)
  231. }
  232. defer rsp.Body.Close()
  233. if rsp.StatusCode != http.StatusOK {
  234. return fmt.Errorf("failed to refresh MCP prompts: status code %d", rsp.StatusCode)
  235. }
  236. return nil
  237. }
  238. // MCPRefreshResources refreshes resources for a named MCP client.
  239. func (c *Client) MCPRefreshResources(ctx context.Context, id, name string) error {
  240. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/refresh-resources", id), nil,
  241. jsonBody(struct {
  242. Name string `json:"name"`
  243. }{Name: name}),
  244. http.Header{"Content-Type": []string{"application/json"}})
  245. if err != nil {
  246. return fmt.Errorf("failed to refresh MCP resources: %w", err)
  247. }
  248. defer rsp.Body.Close()
  249. if rsp.StatusCode != http.StatusOK {
  250. return fmt.Errorf("failed to refresh MCP resources: status code %d", rsp.StatusCode)
  251. }
  252. return nil
  253. }
  254. // GetAgentSessionQueuedPrompts retrieves the number of queued prompts for a
  255. // session.
  256. func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) (int, error) {
  257. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/queued", id, sessionID), nil, nil)
  258. if err != nil {
  259. return 0, fmt.Errorf("failed to get session agent queued prompts: %w", err)
  260. }
  261. defer rsp.Body.Close()
  262. if rsp.StatusCode != http.StatusOK {
  263. return 0, fmt.Errorf("failed to get session agent queued prompts: status code %d", rsp.StatusCode)
  264. }
  265. var count int
  266. if err := json.NewDecoder(rsp.Body).Decode(&count); err != nil {
  267. return 0, fmt.Errorf("failed to decode session agent queued prompts: %w", err)
  268. }
  269. return count, nil
  270. }
  271. // ClearAgentSessionQueuedPrompts clears the queued prompts for a session.
  272. func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) error {
  273. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/clear", id, sessionID), nil, nil, nil)
  274. if err != nil {
  275. return fmt.Errorf("failed to clear session agent queued prompts: %w", err)
  276. }
  277. defer rsp.Body.Close()
  278. if rsp.StatusCode != http.StatusOK {
  279. return fmt.Errorf("failed to clear session agent queued prompts: status code %d", rsp.StatusCode)
  280. }
  281. return nil
  282. }
  283. // GetAgentInfo retrieves the agent status for a workspace.
  284. func (c *Client) GetAgentInfo(ctx context.Context, id string) (*proto.AgentInfo, error) {
  285. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, nil)
  286. if err != nil {
  287. return nil, fmt.Errorf("failed to get agent status: %w", err)
  288. }
  289. defer rsp.Body.Close()
  290. if rsp.StatusCode != http.StatusOK {
  291. return nil, fmt.Errorf("failed to get agent status: status code %d", rsp.StatusCode)
  292. }
  293. var info proto.AgentInfo
  294. if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
  295. return nil, fmt.Errorf("failed to decode agent status: %w", err)
  296. }
  297. return &info, nil
  298. }
  299. // UpdateAgent triggers an agent model update on the server.
  300. func (c *Client) UpdateAgent(ctx context.Context, id string) error {
  301. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/update", id), nil, nil, nil)
  302. if err != nil {
  303. return fmt.Errorf("failed to update agent: %w", err)
  304. }
  305. defer rsp.Body.Close()
  306. if rsp.StatusCode != http.StatusOK {
  307. return fmt.Errorf("failed to update agent: status code %d", rsp.StatusCode)
  308. }
  309. return nil
  310. }
  311. // SendMessage sends a message to the agent for a workspace.
  312. func (c *Client) SendMessage(ctx context.Context, id string, sessionID, prompt string, attachments ...message.Attachment) error {
  313. protoAttachments := make([]proto.Attachment, len(attachments))
  314. for i, a := range attachments {
  315. protoAttachments[i] = proto.Attachment{
  316. FilePath: a.FilePath,
  317. FileName: a.FileName,
  318. MimeType: a.MimeType,
  319. Content: a.Content,
  320. }
  321. }
  322. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, jsonBody(proto.AgentMessage{
  323. SessionID: sessionID,
  324. Prompt: prompt,
  325. Attachments: protoAttachments,
  326. }), http.Header{"Content-Type": []string{"application/json"}})
  327. if err != nil {
  328. return fmt.Errorf("failed to send message to agent: %w", err)
  329. }
  330. defer rsp.Body.Close()
  331. if rsp.StatusCode != http.StatusOK {
  332. return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
  333. }
  334. return nil
  335. }
  336. // GetAgentSessionInfo retrieves the agent session info for a workspace.
  337. func (c *Client) GetAgentSessionInfo(ctx context.Context, id string, sessionID string) (*proto.AgentSession, error) {
  338. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s", id, sessionID), nil, nil)
  339. if err != nil {
  340. return nil, fmt.Errorf("failed to get session agent info: %w", err)
  341. }
  342. defer rsp.Body.Close()
  343. if rsp.StatusCode != http.StatusOK {
  344. return nil, fmt.Errorf("failed to get session agent info: status code %d", rsp.StatusCode)
  345. }
  346. var info proto.AgentSession
  347. if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
  348. return nil, fmt.Errorf("failed to decode session agent info: %w", err)
  349. }
  350. return &info, nil
  351. }
  352. // AgentSummarizeSession requests a session summarization.
  353. func (c *Client) AgentSummarizeSession(ctx context.Context, id string, sessionID string) error {
  354. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/summarize", id, sessionID), nil, nil, nil)
  355. if err != nil {
  356. return fmt.Errorf("failed to summarize session: %w", err)
  357. }
  358. defer rsp.Body.Close()
  359. if rsp.StatusCode != http.StatusOK {
  360. return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
  361. }
  362. return nil
  363. }
  364. // InitiateAgentProcessing triggers agent initialization on the server.
  365. func (c *Client) InitiateAgentProcessing(ctx context.Context, id string) error {
  366. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/init", id), nil, nil, nil)
  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. // ListMessages retrieves all messages for a session as proto types.
  377. func (c *Client) ListMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
  378. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages", id, sessionID), nil, nil)
  379. if err != nil {
  380. return nil, fmt.Errorf("failed to get messages: %w", err)
  381. }
  382. defer rsp.Body.Close()
  383. if rsp.StatusCode != http.StatusOK {
  384. return nil, fmt.Errorf("failed to get messages: status code %d", rsp.StatusCode)
  385. }
  386. var msgs []proto.Message
  387. if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
  388. return nil, fmt.Errorf("failed to decode messages: %w", err)
  389. }
  390. return msgs, nil
  391. }
  392. // GetSession retrieves a specific session as a proto type.
  393. func (c *Client) GetSession(ctx context.Context, id string, sessionID string) (*proto.Session, error) {
  394. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
  395. if err != nil {
  396. return nil, fmt.Errorf("failed to get session: %w", err)
  397. }
  398. defer rsp.Body.Close()
  399. if rsp.StatusCode != http.StatusOK {
  400. return nil, fmt.Errorf("failed to get session: status code %d", rsp.StatusCode)
  401. }
  402. var sess proto.Session
  403. if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
  404. return nil, fmt.Errorf("failed to decode session: %w", err)
  405. }
  406. return &sess, nil
  407. }
  408. // ListSessionHistoryFiles retrieves history files for a session as proto types.
  409. func (c *Client) ListSessionHistoryFiles(ctx context.Context, id string, sessionID string) ([]proto.File, error) {
  410. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/history", id, sessionID), nil, nil)
  411. if err != nil {
  412. return nil, fmt.Errorf("failed to get session history files: %w", err)
  413. }
  414. defer rsp.Body.Close()
  415. if rsp.StatusCode != http.StatusOK {
  416. return nil, fmt.Errorf("failed to get session history files: status code %d", rsp.StatusCode)
  417. }
  418. var files []proto.File
  419. if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
  420. return nil, fmt.Errorf("failed to decode session history files: %w", err)
  421. }
  422. return files, nil
  423. }
  424. // CreateSession creates a new session in a workspace as a proto type.
  425. func (c *Client) CreateSession(ctx context.Context, id string, title string) (*proto.Session, error) {
  426. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, jsonBody(proto.Session{Title: title}), http.Header{"Content-Type": []string{"application/json"}})
  427. if err != nil {
  428. return nil, fmt.Errorf("failed to create session: %w", err)
  429. }
  430. defer rsp.Body.Close()
  431. if rsp.StatusCode != http.StatusOK {
  432. return nil, fmt.Errorf("failed to create session: status code %d", rsp.StatusCode)
  433. }
  434. var sess proto.Session
  435. if err := json.NewDecoder(rsp.Body).Decode(&sess); err != nil {
  436. return nil, fmt.Errorf("failed to decode session: %w", err)
  437. }
  438. return &sess, nil
  439. }
  440. // ListSessions lists all sessions in a workspace as proto types.
  441. func (c *Client) ListSessions(ctx context.Context, id string) ([]proto.Session, error) {
  442. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions", id), nil, nil)
  443. if err != nil {
  444. return nil, fmt.Errorf("failed to get sessions: %w", err)
  445. }
  446. defer rsp.Body.Close()
  447. if rsp.StatusCode != http.StatusOK {
  448. return nil, fmt.Errorf("failed to get sessions: status code %d", rsp.StatusCode)
  449. }
  450. var sessions []proto.Session
  451. if err := json.NewDecoder(rsp.Body).Decode(&sessions); err != nil {
  452. return nil, fmt.Errorf("failed to decode sessions: %w", err)
  453. }
  454. return sessions, nil
  455. }
  456. // GrantPermission grants a permission on a workspace.
  457. func (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) error {
  458. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/grant", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}})
  459. if err != nil {
  460. return fmt.Errorf("failed to grant permission: %w", err)
  461. }
  462. defer rsp.Body.Close()
  463. if rsp.StatusCode != http.StatusOK {
  464. return fmt.Errorf("failed to grant permission: status code %d", rsp.StatusCode)
  465. }
  466. return nil
  467. }
  468. // SetPermissionsSkipRequests sets the skip-requests flag for a workspace.
  469. func (c *Client) SetPermissionsSkipRequests(ctx context.Context, id string, skip bool) error {
  470. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/permissions/skip", id), nil, jsonBody(proto.PermissionSkipRequest{Skip: skip}), http.Header{"Content-Type": []string{"application/json"}})
  471. if err != nil {
  472. return fmt.Errorf("failed to set permissions skip requests: %w", err)
  473. }
  474. defer rsp.Body.Close()
  475. if rsp.StatusCode != http.StatusOK {
  476. return fmt.Errorf("failed to set permissions skip requests: status code %d", rsp.StatusCode)
  477. }
  478. return nil
  479. }
  480. // GetPermissionsSkipRequests retrieves the skip-requests flag for a workspace.
  481. func (c *Client) GetPermissionsSkipRequests(ctx context.Context, id string) (bool, error) {
  482. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/permissions/skip", id), nil, nil)
  483. if err != nil {
  484. return false, fmt.Errorf("failed to get permissions skip requests: %w", err)
  485. }
  486. defer rsp.Body.Close()
  487. if rsp.StatusCode != http.StatusOK {
  488. return false, fmt.Errorf("failed to get permissions skip requests: status code %d", rsp.StatusCode)
  489. }
  490. var skip proto.PermissionSkipRequest
  491. if err := json.NewDecoder(rsp.Body).Decode(&skip); err != nil {
  492. return false, fmt.Errorf("failed to decode permissions skip requests: %w", err)
  493. }
  494. return skip.Skip, nil
  495. }
  496. // GetConfig retrieves the workspace-specific configuration.
  497. func (c *Client) GetConfig(ctx context.Context, id string) (*config.Config, error) {
  498. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/config", id), nil, nil)
  499. if err != nil {
  500. return nil, fmt.Errorf("failed to get config: %w", err)
  501. }
  502. defer rsp.Body.Close()
  503. if rsp.StatusCode != http.StatusOK {
  504. return nil, fmt.Errorf("failed to get config: status code %d", rsp.StatusCode)
  505. }
  506. var cfg config.Config
  507. if err := json.NewDecoder(rsp.Body).Decode(&cfg); err != nil {
  508. return nil, fmt.Errorf("failed to decode config: %w", err)
  509. }
  510. return &cfg, nil
  511. }
  512. func jsonBody(v any) *bytes.Buffer {
  513. b := new(bytes.Buffer)
  514. m, _ := json.Marshal(v)
  515. b.Write(m)
  516. return b
  517. }
  518. // SaveSession updates a session in a workspace, returning a proto type.
  519. func (c *Client) SaveSession(ctx context.Context, id string, sess proto.Session) (*proto.Session, error) {
  520. rsp, err := c.put(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sess.ID), nil, jsonBody(sess), http.Header{"Content-Type": []string{"application/json"}})
  521. if err != nil {
  522. return nil, fmt.Errorf("failed to save session: %w", err)
  523. }
  524. defer rsp.Body.Close()
  525. if rsp.StatusCode != http.StatusOK {
  526. return nil, fmt.Errorf("failed to save session: status code %d", rsp.StatusCode)
  527. }
  528. var saved proto.Session
  529. if err := json.NewDecoder(rsp.Body).Decode(&saved); err != nil {
  530. return nil, fmt.Errorf("failed to decode session: %w", err)
  531. }
  532. return &saved, nil
  533. }
  534. // DeleteSession deletes a session from a workspace.
  535. func (c *Client) DeleteSession(ctx context.Context, id string, sessionID string) error {
  536. rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s", id, sessionID), nil, nil)
  537. if err != nil {
  538. return fmt.Errorf("failed to delete session: %w", err)
  539. }
  540. defer rsp.Body.Close()
  541. if rsp.StatusCode != http.StatusOK {
  542. return fmt.Errorf("failed to delete session: status code %d", rsp.StatusCode)
  543. }
  544. return nil
  545. }
  546. // ListUserMessages retrieves user-role messages for a session as proto types.
  547. func (c *Client) ListUserMessages(ctx context.Context, id string, sessionID string) ([]proto.Message, error) {
  548. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/messages/user", id, sessionID), nil, nil)
  549. if err != nil {
  550. return nil, fmt.Errorf("failed to get user messages: %w", err)
  551. }
  552. defer rsp.Body.Close()
  553. if rsp.StatusCode != http.StatusOK {
  554. return nil, fmt.Errorf("failed to get user messages: status code %d", rsp.StatusCode)
  555. }
  556. var msgs []proto.Message
  557. if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
  558. return nil, fmt.Errorf("failed to decode user messages: %w", err)
  559. }
  560. return msgs, nil
  561. }
  562. // ListAllUserMessages retrieves all user-role messages across sessions as proto types.
  563. func (c *Client) ListAllUserMessages(ctx context.Context, id string) ([]proto.Message, error) {
  564. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/messages/user", id), nil, nil)
  565. if err != nil {
  566. return nil, fmt.Errorf("failed to get all user messages: %w", err)
  567. }
  568. defer rsp.Body.Close()
  569. if rsp.StatusCode != http.StatusOK {
  570. return nil, fmt.Errorf("failed to get all user messages: status code %d", rsp.StatusCode)
  571. }
  572. var msgs []proto.Message
  573. if err := json.NewDecoder(rsp.Body).Decode(&msgs); err != nil && !errors.Is(err, io.EOF) {
  574. return nil, fmt.Errorf("failed to decode all user messages: %w", err)
  575. }
  576. return msgs, nil
  577. }
  578. // CancelAgentSession cancels an ongoing agent operation for a session.
  579. func (c *Client) CancelAgentSession(ctx context.Context, id string, sessionID string) error {
  580. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/cancel", id, sessionID), nil, nil, nil)
  581. if err != nil {
  582. return fmt.Errorf("failed to cancel agent session: %w", err)
  583. }
  584. defer rsp.Body.Close()
  585. if rsp.StatusCode != http.StatusOK {
  586. return fmt.Errorf("failed to cancel agent session: status code %d", rsp.StatusCode)
  587. }
  588. return nil
  589. }
  590. // GetAgentSessionQueuedPromptsList retrieves the list of queued prompt
  591. // strings for a session.
  592. func (c *Client) GetAgentSessionQueuedPromptsList(ctx context.Context, id string, sessionID string) ([]string, error) {
  593. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/prompts/list", id, sessionID), nil, nil)
  594. if err != nil {
  595. return nil, fmt.Errorf("failed to get queued prompts list: %w", err)
  596. }
  597. defer rsp.Body.Close()
  598. if rsp.StatusCode != http.StatusOK {
  599. return nil, fmt.Errorf("failed to get queued prompts list: status code %d", rsp.StatusCode)
  600. }
  601. var prompts []string
  602. if err := json.NewDecoder(rsp.Body).Decode(&prompts); err != nil {
  603. return nil, fmt.Errorf("failed to decode queued prompts list: %w", err)
  604. }
  605. return prompts, nil
  606. }
  607. // GetDefaultSmallModel retrieves the default small model for a provider.
  608. func (c *Client) GetDefaultSmallModel(ctx context.Context, id string, providerID string) (*config.SelectedModel, error) {
  609. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent/default-small-model", id), url.Values{"provider_id": []string{providerID}}, nil)
  610. if err != nil {
  611. return nil, fmt.Errorf("failed to get default small model: %w", err)
  612. }
  613. defer rsp.Body.Close()
  614. if rsp.StatusCode != http.StatusOK {
  615. return nil, fmt.Errorf("failed to get default small model: status code %d", rsp.StatusCode)
  616. }
  617. var model config.SelectedModel
  618. if err := json.NewDecoder(rsp.Body).Decode(&model); err != nil {
  619. return nil, fmt.Errorf("failed to decode default small model: %w", err)
  620. }
  621. return &model, nil
  622. }
  623. // FileTrackerRecordRead records a file read for a session.
  624. func (c *Client) FileTrackerRecordRead(ctx context.Context, id string, sessionID, path string) error {
  625. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/filetracker/read", id), nil, jsonBody(struct {
  626. SessionID string `json:"session_id"`
  627. Path string `json:"path"`
  628. }{SessionID: sessionID, Path: path}), http.Header{"Content-Type": []string{"application/json"}})
  629. if err != nil {
  630. return fmt.Errorf("failed to record file read: %w", err)
  631. }
  632. defer rsp.Body.Close()
  633. if rsp.StatusCode != http.StatusOK {
  634. return fmt.Errorf("failed to record file read: status code %d", rsp.StatusCode)
  635. }
  636. return nil
  637. }
  638. // FileTrackerLastReadTime returns the last read time for a file in a
  639. // session.
  640. func (c *Client) FileTrackerLastReadTime(ctx context.Context, id string, sessionID, path string) (time.Time, error) {
  641. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/filetracker/lastread", id), url.Values{
  642. "session_id": []string{sessionID},
  643. "path": []string{path},
  644. }, nil)
  645. if err != nil {
  646. return time.Time{}, fmt.Errorf("failed to get last read time: %w", err)
  647. }
  648. defer rsp.Body.Close()
  649. if rsp.StatusCode != http.StatusOK {
  650. return time.Time{}, fmt.Errorf("failed to get last read time: status code %d", rsp.StatusCode)
  651. }
  652. var t time.Time
  653. if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil {
  654. return time.Time{}, fmt.Errorf("failed to decode last read time: %w", err)
  655. }
  656. return t, nil
  657. }
  658. // FileTrackerListReadFiles returns the list of read files for a session.
  659. func (c *Client) FileTrackerListReadFiles(ctx context.Context, id string, sessionID string) ([]string, error) {
  660. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/sessions/%s/filetracker/files", id, sessionID), nil, nil)
  661. if err != nil {
  662. return nil, fmt.Errorf("failed to get read files: %w", err)
  663. }
  664. defer rsp.Body.Close()
  665. if rsp.StatusCode != http.StatusOK {
  666. return nil, fmt.Errorf("failed to get read files: status code %d", rsp.StatusCode)
  667. }
  668. var files []string
  669. if err := json.NewDecoder(rsp.Body).Decode(&files); err != nil {
  670. return nil, fmt.Errorf("failed to decode read files: %w", err)
  671. }
  672. return files, nil
  673. }
  674. // LSPStart starts an LSP server for a path.
  675. func (c *Client) LSPStart(ctx context.Context, id string, path string) error {
  676. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/lsps/start", id), nil, jsonBody(struct {
  677. Path string `json:"path"`
  678. }{Path: path}), http.Header{"Content-Type": []string{"application/json"}})
  679. if err != nil {
  680. return fmt.Errorf("failed to start LSP: %w", err)
  681. }
  682. defer rsp.Body.Close()
  683. if rsp.StatusCode != http.StatusOK {
  684. return fmt.Errorf("failed to start LSP: status code %d", rsp.StatusCode)
  685. }
  686. return nil
  687. }
  688. // LSPStopAll stops all LSP servers for a workspace.
  689. func (c *Client) LSPStopAll(ctx context.Context, id string) error {
  690. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/lsps/stop", id), nil, nil, nil)
  691. if err != nil {
  692. return fmt.Errorf("failed to stop LSPs: %w", err)
  693. }
  694. defer rsp.Body.Close()
  695. if rsp.StatusCode != http.StatusOK {
  696. return fmt.Errorf("failed to stop LSPs: status code %d", rsp.StatusCode)
  697. }
  698. return nil
  699. }