1package server
2
3import (
4 "encoding/json"
5 "net/http"
6
7 "github.com/charmbracelet/crush/internal/proto"
8)
9
10// handlePostWorkspaceConfigSet sets a configuration field.
11//
12// @Summary Set a config field
13// @Tags config
14// @Accept json
15// @Param id path string true "Workspace ID"
16// @Param request body proto.ConfigSetRequest true "Config set request"
17// @Success 200
18// @Failure 400 {object} proto.Error
19// @Failure 404 {object} proto.Error
20// @Failure 500 {object} proto.Error
21// @Router /workspaces/{id}/config/set [post]
22func (c *controllerV1) handlePostWorkspaceConfigSet(w http.ResponseWriter, r *http.Request) {
23 id := r.PathValue("id")
24
25 var req proto.ConfigSetRequest
26 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
27 c.server.logError(r, "Failed to decode request", "error", err)
28 jsonError(w, http.StatusBadRequest, "failed to decode request")
29 return
30 }
31
32 if err := c.backend.SetConfigField(id, req.Scope, req.Key, req.Value); err != nil {
33 c.handleError(w, r, err)
34 return
35 }
36 w.WriteHeader(http.StatusOK)
37}
38
39// handlePostWorkspaceConfigRemove removes a configuration field.
40//
41// @Summary Remove a config field
42// @Tags config
43// @Accept json
44// @Param id path string true "Workspace ID"
45// @Param request body proto.ConfigRemoveRequest true "Config remove request"
46// @Success 200
47// @Failure 400 {object} proto.Error
48// @Failure 404 {object} proto.Error
49// @Failure 500 {object} proto.Error
50// @Router /workspaces/{id}/config/remove [post]
51func (c *controllerV1) handlePostWorkspaceConfigRemove(w http.ResponseWriter, r *http.Request) {
52 id := r.PathValue("id")
53
54 var req proto.ConfigRemoveRequest
55 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
56 c.server.logError(r, "Failed to decode request", "error", err)
57 jsonError(w, http.StatusBadRequest, "failed to decode request")
58 return
59 }
60
61 if err := c.backend.RemoveConfigField(id, req.Scope, req.Key); err != nil {
62 c.handleError(w, r, err)
63 return
64 }
65 w.WriteHeader(http.StatusOK)
66}
67
68// handlePostWorkspaceConfigModel updates the preferred model.
69//
70// @Summary Set the preferred model
71// @Tags config
72// @Accept json
73// @Param id path string true "Workspace ID"
74// @Param request body proto.ConfigModelRequest true "Config model request"
75// @Success 200
76// @Failure 400 {object} proto.Error
77// @Failure 404 {object} proto.Error
78// @Failure 500 {object} proto.Error
79// @Router /workspaces/{id}/config/model [post]
80func (c *controllerV1) handlePostWorkspaceConfigModel(w http.ResponseWriter, r *http.Request) {
81 id := r.PathValue("id")
82
83 var req proto.ConfigModelRequest
84 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
85 c.server.logError(r, "Failed to decode request", "error", err)
86 jsonError(w, http.StatusBadRequest, "failed to decode request")
87 return
88 }
89
90 if err := c.backend.UpdatePreferredModel(id, req.Scope, req.ModelType, req.Model); err != nil {
91 c.handleError(w, r, err)
92 return
93 }
94 w.WriteHeader(http.StatusOK)
95}
96
97// handlePostWorkspaceConfigCompact sets compact mode.
98//
99// @Summary Set compact mode
100// @Tags config
101// @Accept json
102// @Param id path string true "Workspace ID"
103// @Param request body proto.ConfigCompactRequest true "Config compact request"
104// @Success 200
105// @Failure 400 {object} proto.Error
106// @Failure 404 {object} proto.Error
107// @Failure 500 {object} proto.Error
108// @Router /workspaces/{id}/config/compact [post]
109func (c *controllerV1) handlePostWorkspaceConfigCompact(w http.ResponseWriter, r *http.Request) {
110 id := r.PathValue("id")
111
112 var req proto.ConfigCompactRequest
113 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
114 c.server.logError(r, "Failed to decode request", "error", err)
115 jsonError(w, http.StatusBadRequest, "failed to decode request")
116 return
117 }
118
119 if err := c.backend.SetCompactMode(id, req.Scope, req.Enabled); err != nil {
120 c.handleError(w, r, err)
121 return
122 }
123 w.WriteHeader(http.StatusOK)
124}
125
126// handlePostWorkspaceConfigProviderKey sets a provider API key.
127//
128// @Summary Set provider API key
129// @Tags config
130// @Accept json
131// @Param id path string true "Workspace ID"
132// @Param request body proto.ConfigProviderKeyRequest true "Config provider key request"
133// @Success 200
134// @Failure 400 {object} proto.Error
135// @Failure 404 {object} proto.Error
136// @Failure 500 {object} proto.Error
137// @Router /workspaces/{id}/config/provider-key [post]
138func (c *controllerV1) handlePostWorkspaceConfigProviderKey(w http.ResponseWriter, r *http.Request) {
139 id := r.PathValue("id")
140
141 var req proto.ConfigProviderKeyRequest
142 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
143 c.server.logError(r, "Failed to decode request", "error", err)
144 jsonError(w, http.StatusBadRequest, "failed to decode request")
145 return
146 }
147
148 if err := c.backend.SetProviderAPIKey(id, req.Scope, req.ProviderID, req.APIKey); err != nil {
149 c.handleError(w, r, err)
150 return
151 }
152 w.WriteHeader(http.StatusOK)
153}
154
155// handlePostWorkspaceConfigImportCopilot imports Copilot credentials.
156//
157// @Summary Import Copilot credentials
158// @Tags config
159// @Produce json
160// @Param id path string true "Workspace ID"
161// @Success 200 {object} proto.ImportCopilotResponse
162// @Failure 404 {object} proto.Error
163// @Failure 500 {object} proto.Error
164// @Router /workspaces/{id}/config/import-copilot [post]
165func (c *controllerV1) handlePostWorkspaceConfigImportCopilot(w http.ResponseWriter, r *http.Request) {
166 id := r.PathValue("id")
167 token, ok, err := c.backend.ImportCopilot(id)
168 if err != nil {
169 c.handleError(w, r, err)
170 return
171 }
172 jsonEncode(w, proto.ImportCopilotResponse{Token: token, Success: ok})
173}
174
175// handlePostWorkspaceConfigRefreshOAuth refreshes an OAuth token for a provider.
176//
177// @Summary Refresh OAuth token
178// @Tags config
179// @Accept json
180// @Param id path string true "Workspace ID"
181// @Param request body proto.ConfigRefreshOAuthRequest true "Refresh OAuth request"
182// @Success 200
183// @Failure 400 {object} proto.Error
184// @Failure 404 {object} proto.Error
185// @Failure 500 {object} proto.Error
186// @Router /workspaces/{id}/config/refresh-oauth [post]
187func (c *controllerV1) handlePostWorkspaceConfigRefreshOAuth(w http.ResponseWriter, r *http.Request) {
188 id := r.PathValue("id")
189
190 var req proto.ConfigRefreshOAuthRequest
191 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
192 c.server.logError(r, "Failed to decode request", "error", err)
193 jsonError(w, http.StatusBadRequest, "failed to decode request")
194 return
195 }
196
197 if err := c.backend.RefreshOAuthToken(r.Context(), id, req.Scope, req.ProviderID); err != nil {
198 c.handleError(w, r, err)
199 return
200 }
201 w.WriteHeader(http.StatusOK)
202}
203
204// handleGetWorkspaceProjectNeedsInit reports whether a project needs initialization.
205//
206// @Summary Check if project needs initialization
207// @Tags project
208// @Produce json
209// @Param id path string true "Workspace ID"
210// @Success 200 {object} proto.ProjectNeedsInitResponse
211// @Failure 404 {object} proto.Error
212// @Failure 500 {object} proto.Error
213// @Router /workspaces/{id}/project/needs-init [get]
214func (c *controllerV1) handleGetWorkspaceProjectNeedsInit(w http.ResponseWriter, r *http.Request) {
215 id := r.PathValue("id")
216 needs, err := c.backend.ProjectNeedsInitialization(id)
217 if err != nil {
218 c.handleError(w, r, err)
219 return
220 }
221 jsonEncode(w, proto.ProjectNeedsInitResponse{NeedsInit: needs})
222}
223
224// handlePostWorkspaceProjectInit marks the project as initialized.
225//
226// @Summary Mark project as initialized
227// @Tags project
228// @Param id path string true "Workspace ID"
229// @Success 200
230// @Failure 404 {object} proto.Error
231// @Failure 500 {object} proto.Error
232// @Router /workspaces/{id}/project/init [post]
233func (c *controllerV1) handlePostWorkspaceProjectInit(w http.ResponseWriter, r *http.Request) {
234 id := r.PathValue("id")
235 if err := c.backend.MarkProjectInitialized(id); err != nil {
236 c.handleError(w, r, err)
237 return
238 }
239 w.WriteHeader(http.StatusOK)
240}
241
242// handleGetWorkspaceProjectInitPrompt returns the project initialization prompt.
243//
244// @Summary Get project initialization prompt
245// @Tags project
246// @Produce json
247// @Param id path string true "Workspace ID"
248// @Success 200 {object} proto.ProjectInitPromptResponse
249// @Failure 404 {object} proto.Error
250// @Failure 500 {object} proto.Error
251// @Router /workspaces/{id}/project/init-prompt [get]
252func (c *controllerV1) handleGetWorkspaceProjectInitPrompt(w http.ResponseWriter, r *http.Request) {
253 id := r.PathValue("id")
254 prompt, err := c.backend.InitializePrompt(id)
255 if err != nil {
256 c.handleError(w, r, err)
257 return
258 }
259 jsonEncode(w, proto.ProjectInitPromptResponse{Prompt: prompt})
260}
261
262// handlePostWorkspaceMCPEnableDocker enables the Docker MCP server.
263//
264// @Summary Enable Docker MCP
265// @Tags mcp
266// @Param id path string true "Workspace ID"
267// @Success 200
268// @Failure 404 {object} proto.Error
269// @Failure 500 {object} proto.Error
270// @Router /workspaces/{id}/mcp/docker/enable [post]
271func (c *controllerV1) handlePostWorkspaceMCPEnableDocker(w http.ResponseWriter, r *http.Request) {
272 id := r.PathValue("id")
273 if err := c.backend.EnableDockerMCP(r.Context(), id); err != nil {
274 c.handleError(w, r, err)
275 return
276 }
277 w.WriteHeader(http.StatusOK)
278}
279
280// handlePostWorkspaceMCPDisableDocker disables the Docker MCP server.
281//
282// @Summary Disable Docker MCP
283// @Tags mcp
284// @Param id path string true "Workspace ID"
285// @Success 200
286// @Failure 404 {object} proto.Error
287// @Failure 500 {object} proto.Error
288// @Router /workspaces/{id}/mcp/docker/disable [post]
289func (c *controllerV1) handlePostWorkspaceMCPDisableDocker(w http.ResponseWriter, r *http.Request) {
290 id := r.PathValue("id")
291 if err := c.backend.DisableDockerMCP(id); err != nil {
292 c.handleError(w, r, err)
293 return
294 }
295 w.WriteHeader(http.StatusOK)
296}
297
298// handlePostWorkspaceMCPRefreshTools refreshes tools for a named MCP server.
299//
300// @Summary Refresh MCP tools
301// @Tags mcp
302// @Accept json
303// @Param id path string true "Workspace ID"
304// @Param request body proto.MCPNameRequest true "MCP name request"
305// @Success 200
306// @Failure 400 {object} proto.Error
307// @Failure 404 {object} proto.Error
308// @Failure 500 {object} proto.Error
309// @Router /workspaces/{id}/mcp/refresh-tools [post]
310func (c *controllerV1) handlePostWorkspaceMCPRefreshTools(w http.ResponseWriter, r *http.Request) {
311 id := r.PathValue("id")
312
313 var req proto.MCPNameRequest
314 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
315 c.server.logError(r, "Failed to decode request", "error", err)
316 jsonError(w, http.StatusBadRequest, "failed to decode request")
317 return
318 }
319
320 if err := c.backend.RefreshMCPTools(r.Context(), id, req.Name); err != nil {
321 c.handleError(w, r, err)
322 return
323 }
324 w.WriteHeader(http.StatusOK)
325}
326
327// handlePostWorkspaceMCPReadResource reads a resource from an MCP server.
328//
329// @Summary Read MCP resource
330// @Tags mcp
331// @Accept json
332// @Produce json
333// @Param id path string true "Workspace ID"
334// @Param request body proto.MCPReadResourceRequest true "MCP read resource request"
335// @Success 200 {object} object
336// @Failure 400 {object} proto.Error
337// @Failure 404 {object} proto.Error
338// @Failure 500 {object} proto.Error
339// @Router /workspaces/{id}/mcp/read-resource [post]
340func (c *controllerV1) handlePostWorkspaceMCPReadResource(w http.ResponseWriter, r *http.Request) {
341 id := r.PathValue("id")
342
343 var req proto.MCPReadResourceRequest
344 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
345 c.server.logError(r, "Failed to decode request", "error", err)
346 jsonError(w, http.StatusBadRequest, "failed to decode request")
347 return
348 }
349
350 contents, err := c.backend.ReadMCPResource(r.Context(), id, req.Name, req.URI)
351 if err != nil {
352 c.handleError(w, r, err)
353 return
354 }
355 jsonEncode(w, contents)
356}
357
358// handlePostWorkspaceMCPGetPrompt retrieves a prompt from an MCP server.
359//
360// @Summary Get MCP prompt
361// @Tags mcp
362// @Accept json
363// @Produce json
364// @Param id path string true "Workspace ID"
365// @Param request body proto.MCPGetPromptRequest true "MCP get prompt request"
366// @Success 200 {object} proto.MCPGetPromptResponse
367// @Failure 400 {object} proto.Error
368// @Failure 404 {object} proto.Error
369// @Failure 500 {object} proto.Error
370// @Router /workspaces/{id}/mcp/get-prompt [post]
371func (c *controllerV1) handlePostWorkspaceMCPGetPrompt(w http.ResponseWriter, r *http.Request) {
372 id := r.PathValue("id")
373
374 var req proto.MCPGetPromptRequest
375 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
376 c.server.logError(r, "Failed to decode request", "error", err)
377 jsonError(w, http.StatusBadRequest, "failed to decode request")
378 return
379 }
380
381 prompt, err := c.backend.GetMCPPrompt(id, req.ClientID, req.PromptID, req.Args)
382 if err != nil {
383 c.handleError(w, r, err)
384 return
385 }
386 jsonEncode(w, proto.MCPGetPromptResponse{Prompt: prompt})
387}
388
389// handleGetWorkspaceMCPStates returns the state of all MCP clients.
390//
391// @Summary Get MCP client states
392// @Tags mcp
393// @Produce json
394// @Param id path string true "Workspace ID"
395// @Success 200 {object} map[string]proto.MCPClientInfo
396// @Failure 404 {object} proto.Error
397// @Failure 500 {object} proto.Error
398// @Router /workspaces/{id}/mcp/states [get]
399func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *http.Request) {
400 id := r.PathValue("id")
401 states := c.backend.MCPGetStates(id)
402 result := make(map[string]proto.MCPClientInfo, len(states))
403 for k, v := range states {
404 result[k] = proto.MCPClientInfo{
405 Name: v.Name,
406 State: proto.MCPState(v.State),
407 Error: v.Error,
408 ToolCount: v.Counts.Tools,
409 PromptCount: v.Counts.Prompts,
410 ResourceCount: v.Counts.Resources,
411 ConnectedAt: v.ConnectedAt,
412 }
413 }
414 jsonEncode(w, result)
415}
416
417// handlePostWorkspaceMCPRefreshPrompts refreshes prompts for a named MCP server.
418//
419// @Summary Refresh MCP prompts
420// @Tags mcp
421// @Accept json
422// @Param id path string true "Workspace ID"
423// @Param request body proto.MCPNameRequest true "MCP name request"
424// @Success 200
425// @Failure 400 {object} proto.Error
426// @Failure 404 {object} proto.Error
427// @Failure 500 {object} proto.Error
428// @Router /workspaces/{id}/mcp/refresh-prompts [post]
429func (c *controllerV1) handlePostWorkspaceMCPRefreshPrompts(w http.ResponseWriter, r *http.Request) {
430 id := r.PathValue("id")
431
432 var req proto.MCPNameRequest
433 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
434 c.server.logError(r, "Failed to decode request", "error", err)
435 jsonError(w, http.StatusBadRequest, "failed to decode request")
436 return
437 }
438
439 c.backend.MCPRefreshPrompts(r.Context(), id, req.Name)
440 w.WriteHeader(http.StatusOK)
441}
442
443// handlePostWorkspaceMCPRefreshResources refreshes resources for a named MCP server.
444//
445// @Summary Refresh MCP resources
446// @Tags mcp
447// @Accept json
448// @Param id path string true "Workspace ID"
449// @Param request body proto.MCPNameRequest true "MCP name request"
450// @Success 200
451// @Failure 400 {object} proto.Error
452// @Failure 404 {object} proto.Error
453// @Failure 500 {object} proto.Error
454// @Router /workspaces/{id}/mcp/refresh-resources [post]
455func (c *controllerV1) handlePostWorkspaceMCPRefreshResources(w http.ResponseWriter, r *http.Request) {
456 id := r.PathValue("id")
457
458 var req proto.MCPNameRequest
459 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
460 c.server.logError(r, "Failed to decode request", "error", err)
461 jsonError(w, http.StatusBadRequest, "failed to decode request")
462 return
463 }
464
465 c.backend.MCPRefreshResources(r.Context(), id, req.Name)
466 w.WriteHeader(http.StatusOK)
467}