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