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// RefreshMCPTools refreshes tools for a named MCP server.
197func (c *Client) RefreshMCPTools(ctx context.Context, id, name string) error {
198 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/refresh-tools", id), nil, jsonBody(struct {
199 Name string `json:"name"`
200 }{Name: name}), http.Header{"Content-Type": []string{"application/json"}})
201 if err != nil {
202 return fmt.Errorf("failed to refresh MCP tools: %w", err)
203 }
204 defer rsp.Body.Close()
205 if rsp.StatusCode != http.StatusOK {
206 return fmt.Errorf("failed to refresh MCP tools: status code %d", rsp.StatusCode)
207 }
208 return nil
209}
210
211// ReadMCPResource reads a resource from a named MCP server.
212func (c *Client) ReadMCPResource(ctx context.Context, id, name, uri string) ([]MCPResourceContents, error) {
213 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/read-resource", id), nil, jsonBody(struct {
214 Name string `json:"name"`
215 URI string `json:"uri"`
216 }{Name: name, URI: uri}), http.Header{"Content-Type": []string{"application/json"}})
217 if err != nil {
218 return nil, fmt.Errorf("failed to read MCP resource: %w", err)
219 }
220 defer rsp.Body.Close()
221 if rsp.StatusCode != http.StatusOK {
222 return nil, fmt.Errorf("failed to read MCP resource: status code %d", rsp.StatusCode)
223 }
224 var contents []MCPResourceContents
225 if err := json.NewDecoder(rsp.Body).Decode(&contents); err != nil {
226 return nil, fmt.Errorf("failed to decode MCP resource: %w", err)
227 }
228 return contents, nil
229}
230
231// GetMCPPrompt retrieves a prompt from a named MCP server.
232func (c *Client) GetMCPPrompt(ctx context.Context, id, clientID, promptID string, args map[string]string) (string, error) {
233 rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/get-prompt", id), nil, jsonBody(struct {
234 ClientID string `json:"client_id"`
235 PromptID string `json:"prompt_id"`
236 Args map[string]string `json:"args"`
237 }{ClientID: clientID, PromptID: promptID, Args: args}), http.Header{"Content-Type": []string{"application/json"}})
238 if err != nil {
239 return "", fmt.Errorf("failed to get MCP prompt: %w", err)
240 }
241 defer rsp.Body.Close()
242 if rsp.StatusCode != http.StatusOK {
243 return "", fmt.Errorf("failed to get MCP prompt: status code %d", rsp.StatusCode)
244 }
245 var result struct {
246 Prompt string `json:"prompt"`
247 }
248 if err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {
249 return "", fmt.Errorf("failed to decode MCP prompt response: %w", err)
250 }
251 return result.Prompt, nil
252}