config.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package client
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "github.com/charmbracelet/crush/internal/config"
  8. "github.com/charmbracelet/crush/internal/oauth"
  9. )
  10. // SetConfigField sets a config key/value pair on the server.
  11. func (c *Client) SetConfigField(ctx context.Context, id string, scope config.Scope, key string, value any) error {
  12. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/config/set", id), nil, jsonBody(struct {
  13. Scope config.Scope `json:"scope"`
  14. Key string `json:"key"`
  15. Value any `json:"value"`
  16. }{Scope: scope, Key: key, Value: value}), http.Header{"Content-Type": []string{"application/json"}})
  17. if err != nil {
  18. return fmt.Errorf("failed to set config field: %w", err)
  19. }
  20. defer rsp.Body.Close()
  21. if rsp.StatusCode != http.StatusOK {
  22. return fmt.Errorf("failed to set config field: status code %d", rsp.StatusCode)
  23. }
  24. return nil
  25. }
  26. // RemoveConfigField removes a config key on the server.
  27. func (c *Client) RemoveConfigField(ctx context.Context, id string, scope config.Scope, key string) error {
  28. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/config/remove", id), nil, jsonBody(struct {
  29. Scope config.Scope `json:"scope"`
  30. Key string `json:"key"`
  31. }{Scope: scope, Key: key}), http.Header{"Content-Type": []string{"application/json"}})
  32. if err != nil {
  33. return fmt.Errorf("failed to remove config field: %w", err)
  34. }
  35. defer rsp.Body.Close()
  36. if rsp.StatusCode != http.StatusOK {
  37. return fmt.Errorf("failed to remove config field: status code %d", rsp.StatusCode)
  38. }
  39. return nil
  40. }
  41. // UpdatePreferredModel updates the preferred model on the server.
  42. func (c *Client) UpdatePreferredModel(ctx context.Context, id string, scope config.Scope, modelType config.SelectedModelType, model config.SelectedModel) error {
  43. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/config/model", id), nil, jsonBody(struct {
  44. Scope config.Scope `json:"scope"`
  45. ModelType config.SelectedModelType `json:"model_type"`
  46. Model config.SelectedModel `json:"model"`
  47. }{Scope: scope, ModelType: modelType, Model: model}), http.Header{"Content-Type": []string{"application/json"}})
  48. if err != nil {
  49. return fmt.Errorf("failed to update preferred model: %w", err)
  50. }
  51. defer rsp.Body.Close()
  52. if rsp.StatusCode != http.StatusOK {
  53. return fmt.Errorf("failed to update preferred model: status code %d", rsp.StatusCode)
  54. }
  55. return nil
  56. }
  57. // SetCompactMode sets compact mode on the server.
  58. func (c *Client) SetCompactMode(ctx context.Context, id string, scope config.Scope, enabled bool) error {
  59. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/config/compact", id), nil, jsonBody(struct {
  60. Scope config.Scope `json:"scope"`
  61. Enabled bool `json:"enabled"`
  62. }{Scope: scope, Enabled: enabled}), http.Header{"Content-Type": []string{"application/json"}})
  63. if err != nil {
  64. return fmt.Errorf("failed to set compact mode: %w", err)
  65. }
  66. defer rsp.Body.Close()
  67. if rsp.StatusCode != http.StatusOK {
  68. return fmt.Errorf("failed to set compact mode: status code %d", rsp.StatusCode)
  69. }
  70. return nil
  71. }
  72. // SetProviderAPIKey sets a provider API key on the server.
  73. func (c *Client) SetProviderAPIKey(ctx context.Context, id string, scope config.Scope, providerID string, apiKey any) error {
  74. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/config/provider-key", id), nil, jsonBody(struct {
  75. Scope config.Scope `json:"scope"`
  76. ProviderID string `json:"provider_id"`
  77. APIKey any `json:"api_key"`
  78. }{Scope: scope, ProviderID: providerID, APIKey: apiKey}), http.Header{"Content-Type": []string{"application/json"}})
  79. if err != nil {
  80. return fmt.Errorf("failed to set provider API key: %w", err)
  81. }
  82. defer rsp.Body.Close()
  83. if rsp.StatusCode != http.StatusOK {
  84. return fmt.Errorf("failed to set provider API key: status code %d", rsp.StatusCode)
  85. }
  86. return nil
  87. }
  88. // ImportCopilot attempts to import a GitHub Copilot token on the
  89. // server.
  90. func (c *Client) ImportCopilot(ctx context.Context, id string) (*oauth.Token, bool, error) {
  91. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/config/import-copilot", id), nil, nil, nil)
  92. if err != nil {
  93. return nil, false, fmt.Errorf("failed to import copilot: %w", err)
  94. }
  95. defer rsp.Body.Close()
  96. if rsp.StatusCode != http.StatusOK {
  97. return nil, false, fmt.Errorf("failed to import copilot: status code %d", rsp.StatusCode)
  98. }
  99. var result struct {
  100. Token *oauth.Token `json:"token"`
  101. Success bool `json:"success"`
  102. }
  103. if err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {
  104. return nil, false, fmt.Errorf("failed to decode import copilot response: %w", err)
  105. }
  106. return result.Token, result.Success, nil
  107. }
  108. // RefreshOAuthToken refreshes an OAuth token for a provider on the
  109. // server.
  110. func (c *Client) RefreshOAuthToken(ctx context.Context, id string, scope config.Scope, providerID string) error {
  111. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/config/refresh-oauth", id), nil, jsonBody(struct {
  112. Scope config.Scope `json:"scope"`
  113. ProviderID string `json:"provider_id"`
  114. }{Scope: scope, ProviderID: providerID}), http.Header{"Content-Type": []string{"application/json"}})
  115. if err != nil {
  116. return fmt.Errorf("failed to refresh OAuth token: %w", err)
  117. }
  118. defer rsp.Body.Close()
  119. if rsp.StatusCode != http.StatusOK {
  120. return fmt.Errorf("failed to refresh OAuth token: status code %d", rsp.StatusCode)
  121. }
  122. return nil
  123. }
  124. // ProjectNeedsInitialization checks if the project needs
  125. // initialization.
  126. func (c *Client) ProjectNeedsInitialization(ctx context.Context, id string) (bool, error) {
  127. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/project/needs-init", id), nil, nil)
  128. if err != nil {
  129. return false, fmt.Errorf("failed to check project init: %w", err)
  130. }
  131. defer rsp.Body.Close()
  132. if rsp.StatusCode != http.StatusOK {
  133. return false, fmt.Errorf("failed to check project init: status code %d", rsp.StatusCode)
  134. }
  135. var result struct {
  136. NeedsInit bool `json:"needs_init"`
  137. }
  138. if err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {
  139. return false, fmt.Errorf("failed to decode project init response: %w", err)
  140. }
  141. return result.NeedsInit, nil
  142. }
  143. // MarkProjectInitialized marks the project as initialized on the
  144. // server.
  145. func (c *Client) MarkProjectInitialized(ctx context.Context, id string) error {
  146. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/project/init", id), nil, nil, nil)
  147. if err != nil {
  148. return fmt.Errorf("failed to mark project initialized: %w", err)
  149. }
  150. defer rsp.Body.Close()
  151. if rsp.StatusCode != http.StatusOK {
  152. return fmt.Errorf("failed to mark project initialized: status code %d", rsp.StatusCode)
  153. }
  154. return nil
  155. }
  156. // GetInitializePrompt retrieves the initialization prompt from the
  157. // server.
  158. func (c *Client) GetInitializePrompt(ctx context.Context, id string) (string, error) {
  159. rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/project/init-prompt", id), nil, nil)
  160. if err != nil {
  161. return "", fmt.Errorf("failed to get init prompt: %w", err)
  162. }
  163. defer rsp.Body.Close()
  164. if rsp.StatusCode != http.StatusOK {
  165. return "", fmt.Errorf("failed to get init prompt: status code %d", rsp.StatusCode)
  166. }
  167. var result struct {
  168. Prompt string `json:"prompt"`
  169. }
  170. if err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {
  171. return "", fmt.Errorf("failed to decode init prompt response: %w", err)
  172. }
  173. return result.Prompt, nil
  174. }
  175. // MCPResourceContents holds the contents of an MCP resource.
  176. type MCPResourceContents struct {
  177. URI string `json:"uri"`
  178. MIMEType string `json:"mime_type,omitempty"`
  179. Text string `json:"text,omitempty"`
  180. Blob []byte `json:"blob,omitempty"`
  181. }
  182. // EnableDockerMCP enables the Docker MCP server on the workspace.
  183. func (c *Client) EnableDockerMCP(ctx context.Context, id string) error {
  184. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/docker/enable", id), nil, nil, nil)
  185. if err != nil {
  186. return fmt.Errorf("failed to enable docker MCP: %w", err)
  187. }
  188. defer rsp.Body.Close()
  189. if rsp.StatusCode != http.StatusOK {
  190. return fmt.Errorf("failed to enable docker MCP: status code %d", rsp.StatusCode)
  191. }
  192. return nil
  193. }
  194. // DisableDockerMCP disables the Docker MCP server on the workspace.
  195. func (c *Client) DisableDockerMCP(ctx context.Context, id string) error {
  196. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/docker/disable", id), nil, nil, nil)
  197. if err != nil {
  198. return fmt.Errorf("failed to disable docker MCP: %w", err)
  199. }
  200. defer rsp.Body.Close()
  201. if rsp.StatusCode != http.StatusOK {
  202. return fmt.Errorf("failed to disable docker MCP: status code %d", rsp.StatusCode)
  203. }
  204. return nil
  205. }
  206. // RefreshMCPTools refreshes tools for a named MCP server.
  207. func (c *Client) RefreshMCPTools(ctx context.Context, id, name string) error {
  208. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/refresh-tools", id), nil, jsonBody(struct {
  209. Name string `json:"name"`
  210. }{Name: name}), http.Header{"Content-Type": []string{"application/json"}})
  211. if err != nil {
  212. return fmt.Errorf("failed to refresh MCP tools: %w", err)
  213. }
  214. defer rsp.Body.Close()
  215. if rsp.StatusCode != http.StatusOK {
  216. return fmt.Errorf("failed to refresh MCP tools: status code %d", rsp.StatusCode)
  217. }
  218. return nil
  219. }
  220. // ReadMCPResource reads a resource from a named MCP server.
  221. func (c *Client) ReadMCPResource(ctx context.Context, id, name, uri string) ([]MCPResourceContents, error) {
  222. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/read-resource", id), nil, jsonBody(struct {
  223. Name string `json:"name"`
  224. URI string `json:"uri"`
  225. }{Name: name, URI: uri}), http.Header{"Content-Type": []string{"application/json"}})
  226. if err != nil {
  227. return nil, fmt.Errorf("failed to read MCP resource: %w", err)
  228. }
  229. defer rsp.Body.Close()
  230. if rsp.StatusCode != http.StatusOK {
  231. return nil, fmt.Errorf("failed to read MCP resource: status code %d", rsp.StatusCode)
  232. }
  233. var contents []MCPResourceContents
  234. if err := json.NewDecoder(rsp.Body).Decode(&contents); err != nil {
  235. return nil, fmt.Errorf("failed to decode MCP resource: %w", err)
  236. }
  237. return contents, nil
  238. }
  239. // GetMCPPrompt retrieves a prompt from a named MCP server.
  240. func (c *Client) GetMCPPrompt(ctx context.Context, id, clientID, promptID string, args map[string]string) (string, error) {
  241. rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/get-prompt", id), nil, jsonBody(struct {
  242. ClientID string `json:"client_id"`
  243. PromptID string `json:"prompt_id"`
  244. Args map[string]string `json:"args"`
  245. }{ClientID: clientID, PromptID: promptID, Args: args}), http.Header{"Content-Type": []string{"application/json"}})
  246. if err != nil {
  247. return "", fmt.Errorf("failed to get MCP prompt: %w", err)
  248. }
  249. defer rsp.Body.Close()
  250. if rsp.StatusCode != http.StatusOK {
  251. return "", fmt.Errorf("failed to get MCP prompt: status code %d", rsp.StatusCode)
  252. }
  253. var result struct {
  254. Prompt string `json:"prompt"`
  255. }
  256. if err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {
  257. return "", fmt.Errorf("failed to decode MCP prompt response: %w", err)
  258. }
  259. return result.Prompt, nil
  260. }