config.go

  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}