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 apiKey, err := req.DecodeAPIKey()
149 if err != nil {
150 c.server.logError(r, "Failed to decode api key", "error", err, "kind", req.Kind)
151 jsonError(w, http.StatusBadRequest, err.Error())
152 return
153 }
154
155 if err := c.backend.SetProviderAPIKey(id, req.Scope, req.ProviderID, apiKey); err != nil {
156 c.handleError(w, r, err)
157 return
158 }
159 w.WriteHeader(http.StatusOK)
160}
161
162// handlePostWorkspaceConfigImportCopilot imports Copilot credentials.
163//
164// @Summary Import Copilot credentials
165// @Tags config
166// @Produce json
167// @Param id path string true "Workspace ID"
168// @Success 200 {object} proto.ImportCopilotResponse
169// @Failure 404 {object} proto.Error
170// @Failure 500 {object} proto.Error
171// @Router /workspaces/{id}/config/import-copilot [post]
172func (c *controllerV1) handlePostWorkspaceConfigImportCopilot(w http.ResponseWriter, r *http.Request) {
173 id := r.PathValue("id")
174 token, ok, err := c.backend.ImportCopilot(id)
175 if err != nil {
176 c.handleError(w, r, err)
177 return
178 }
179 jsonEncode(w, proto.ImportCopilotResponse{Token: token, Success: ok})
180}
181
182// handlePostWorkspaceConfigRefreshOAuth refreshes an OAuth token for a provider.
183//
184// @Summary Refresh OAuth token
185// @Tags config
186// @Accept json
187// @Param id path string true "Workspace ID"
188// @Param request body proto.ConfigRefreshOAuthRequest true "Refresh OAuth request"
189// @Success 200
190// @Failure 400 {object} proto.Error
191// @Failure 404 {object} proto.Error
192// @Failure 500 {object} proto.Error
193// @Router /workspaces/{id}/config/refresh-oauth [post]
194func (c *controllerV1) handlePostWorkspaceConfigRefreshOAuth(w http.ResponseWriter, r *http.Request) {
195 id := r.PathValue("id")
196
197 var req proto.ConfigRefreshOAuthRequest
198 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
199 c.server.logError(r, "Failed to decode request", "error", err)
200 jsonError(w, http.StatusBadRequest, "failed to decode request")
201 return
202 }
203
204 if err := c.backend.RefreshOAuthToken(r.Context(), id, req.Scope, req.ProviderID); err != nil {
205 c.handleError(w, r, err)
206 return
207 }
208 w.WriteHeader(http.StatusOK)
209}
210
211// handleGetWorkspaceProjectNeedsInit reports whether a project needs initialization.
212//
213// @Summary Check if project needs initialization
214// @Tags project
215// @Produce json
216// @Param id path string true "Workspace ID"
217// @Success 200 {object} proto.ProjectNeedsInitResponse
218// @Failure 404 {object} proto.Error
219// @Failure 500 {object} proto.Error
220// @Router /workspaces/{id}/project/needs-init [get]
221func (c *controllerV1) handleGetWorkspaceProjectNeedsInit(w http.ResponseWriter, r *http.Request) {
222 id := r.PathValue("id")
223 needs, err := c.backend.ProjectNeedsInitialization(id)
224 if err != nil {
225 c.handleError(w, r, err)
226 return
227 }
228 jsonEncode(w, proto.ProjectNeedsInitResponse{NeedsInit: needs})
229}
230
231// handlePostWorkspaceProjectInit marks the project as initialized.
232//
233// @Summary Mark project as initialized
234// @Tags project
235// @Param id path string true "Workspace ID"
236// @Success 200
237// @Failure 404 {object} proto.Error
238// @Failure 500 {object} proto.Error
239// @Router /workspaces/{id}/project/init [post]
240func (c *controllerV1) handlePostWorkspaceProjectInit(w http.ResponseWriter, r *http.Request) {
241 id := r.PathValue("id")
242 if err := c.backend.MarkProjectInitialized(id); err != nil {
243 c.handleError(w, r, err)
244 return
245 }
246 w.WriteHeader(http.StatusOK)
247}
248
249// handleGetWorkspaceProjectInitPrompt returns the project initialization prompt.
250//
251// @Summary Get project initialization prompt
252// @Tags project
253// @Produce json
254// @Param id path string true "Workspace ID"
255// @Success 200 {object} proto.ProjectInitPromptResponse
256// @Failure 404 {object} proto.Error
257// @Failure 500 {object} proto.Error
258// @Router /workspaces/{id}/project/init-prompt [get]
259func (c *controllerV1) handleGetWorkspaceProjectInitPrompt(w http.ResponseWriter, r *http.Request) {
260 id := r.PathValue("id")
261 prompt, err := c.backend.InitializePrompt(id)
262 if err != nil {
263 c.handleError(w, r, err)
264 return
265 }
266 jsonEncode(w, proto.ProjectInitPromptResponse{Prompt: prompt})
267}
268
269// handleGetWorkspaceSkills returns the effective visible skills for a workspace.
270//
271// @Summary List visible skills
272// @Tags skills
273// @Produce json
274// @Param id path string true "Workspace ID"
275// @Success 200 {array} proto.SkillInfo
276// @Failure 404 {object} proto.Error
277// @Failure 500 {object} proto.Error
278// @Router /workspaces/{id}/skills [get]
279func (c *controllerV1) handleGetWorkspaceSkills(w http.ResponseWriter, r *http.Request) {
280 id := r.PathValue("id")
281 skills, err := c.backend.ListSkills(id)
282 if err != nil {
283 c.handleError(w, r, err)
284 return
285 }
286 jsonEncode(w, skills)
287}
288
289// handlePostWorkspaceSkillRead reads a skill's content by ID.
290//
291// @Summary Read skill content
292// @Tags skills
293// @Accept json
294// @Produce json
295// @Param id path string true "Workspace ID"
296// @Param request body proto.ReadSkillRequest true "Read skill request"
297// @Success 200 {object} proto.ReadSkillResponse
298// @Failure 400 {object} proto.Error
299// @Failure 404 {object} proto.Error
300// @Failure 500 {object} proto.Error
301// @Router /workspaces/{id}/skills/read [post]
302func (c *controllerV1) handlePostWorkspaceSkillRead(w http.ResponseWriter, r *http.Request) {
303 id := r.PathValue("id")
304
305 var req proto.ReadSkillRequest
306 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
307 c.server.logError(r, "Failed to decode request", "error", err)
308 jsonError(w, http.StatusBadRequest, "failed to decode request")
309 return
310 }
311
312 content, result, err := c.backend.ReadSkill(r.Context(), id, req.SkillID)
313 if err != nil {
314 c.handleError(w, r, err)
315 return
316 }
317 jsonEncode(w, proto.ReadSkillResponse{Content: content, Result: result})
318}
319
320// handlePostWorkspaceMCPEnableDocker enables the Docker MCP server.
321//
322// @Summary Enable Docker MCP
323// @Tags mcp
324// @Param id path string true "Workspace ID"
325// @Success 200
326// @Failure 404 {object} proto.Error
327// @Failure 500 {object} proto.Error
328// @Router /workspaces/{id}/mcp/docker/enable [post]
329func (c *controllerV1) handlePostWorkspaceMCPEnableDocker(w http.ResponseWriter, r *http.Request) {
330 id := r.PathValue("id")
331 if err := c.backend.EnableDockerMCP(r.Context(), id); err != nil {
332 c.handleError(w, r, err)
333 return
334 }
335 w.WriteHeader(http.StatusOK)
336}
337
338// handlePostWorkspaceMCPDisableDocker disables the Docker MCP server.
339//
340// @Summary Disable Docker MCP
341// @Tags mcp
342// @Param id path string true "Workspace ID"
343// @Success 200
344// @Failure 404 {object} proto.Error
345// @Failure 500 {object} proto.Error
346// @Router /workspaces/{id}/mcp/docker/disable [post]
347func (c *controllerV1) handlePostWorkspaceMCPDisableDocker(w http.ResponseWriter, r *http.Request) {
348 id := r.PathValue("id")
349 if err := c.backend.DisableDockerMCP(id); err != nil {
350 c.handleError(w, r, err)
351 return
352 }
353 w.WriteHeader(http.StatusOK)
354}
355
356// handlePostWorkspaceMCPRefreshTools refreshes tools for a named MCP server.
357//
358// @Summary Refresh MCP tools
359// @Tags mcp
360// @Accept json
361// @Param id path string true "Workspace ID"
362// @Param request body proto.MCPNameRequest true "MCP name request"
363// @Success 200
364// @Failure 400 {object} proto.Error
365// @Failure 404 {object} proto.Error
366// @Failure 500 {object} proto.Error
367// @Router /workspaces/{id}/mcp/refresh-tools [post]
368func (c *controllerV1) handlePostWorkspaceMCPRefreshTools(w http.ResponseWriter, r *http.Request) {
369 id := r.PathValue("id")
370
371 var req proto.MCPNameRequest
372 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
373 c.server.logError(r, "Failed to decode request", "error", err)
374 jsonError(w, http.StatusBadRequest, "failed to decode request")
375 return
376 }
377
378 if err := c.backend.RefreshMCPTools(r.Context(), id, req.Name); err != nil {
379 c.handleError(w, r, err)
380 return
381 }
382 w.WriteHeader(http.StatusOK)
383}
384
385// handlePostWorkspaceMCPReadResource reads a resource from an MCP server.
386//
387// @Summary Read MCP resource
388// @Tags mcp
389// @Accept json
390// @Produce json
391// @Param id path string true "Workspace ID"
392// @Param request body proto.MCPReadResourceRequest true "MCP read resource request"
393// @Success 200 {object} object
394// @Failure 400 {object} proto.Error
395// @Failure 404 {object} proto.Error
396// @Failure 500 {object} proto.Error
397// @Router /workspaces/{id}/mcp/read-resource [post]
398func (c *controllerV1) handlePostWorkspaceMCPReadResource(w http.ResponseWriter, r *http.Request) {
399 id := r.PathValue("id")
400
401 var req proto.MCPReadResourceRequest
402 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
403 c.server.logError(r, "Failed to decode request", "error", err)
404 jsonError(w, http.StatusBadRequest, "failed to decode request")
405 return
406 }
407
408 contents, err := c.backend.ReadMCPResource(r.Context(), id, req.Name, req.URI)
409 if err != nil {
410 c.handleError(w, r, err)
411 return
412 }
413 jsonEncode(w, contents)
414}
415
416// handlePostWorkspaceMCPGetPrompt retrieves a prompt from an MCP server.
417//
418// @Summary Get MCP prompt
419// @Tags mcp
420// @Accept json
421// @Produce json
422// @Param id path string true "Workspace ID"
423// @Param request body proto.MCPGetPromptRequest true "MCP get prompt request"
424// @Success 200 {object} proto.MCPGetPromptResponse
425// @Failure 400 {object} proto.Error
426// @Failure 404 {object} proto.Error
427// @Failure 500 {object} proto.Error
428// @Router /workspaces/{id}/mcp/get-prompt [post]
429func (c *controllerV1) handlePostWorkspaceMCPGetPrompt(w http.ResponseWriter, r *http.Request) {
430 id := r.PathValue("id")
431
432 var req proto.MCPGetPromptRequest
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 prompt, err := c.backend.GetMCPPrompt(id, req.ClientID, req.PromptID, req.Args)
440 if err != nil {
441 c.handleError(w, r, err)
442 return
443 }
444 jsonEncode(w, proto.MCPGetPromptResponse{Prompt: prompt})
445}
446
447// handleGetWorkspaceMCPStates returns the state of all MCP clients.
448//
449// @Summary Get MCP client states
450// @Tags mcp
451// @Produce json
452// @Param id path string true "Workspace ID"
453// @Success 200 {object} map[string]proto.MCPClientInfo
454// @Failure 404 {object} proto.Error
455// @Failure 500 {object} proto.Error
456// @Router /workspaces/{id}/mcp/states [get]
457func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *http.Request) {
458 id := r.PathValue("id")
459 states := c.backend.MCPGetStates(id)
460 result := make(map[string]proto.MCPClientInfo, len(states))
461 for k, v := range states {
462 result[k] = proto.MCPClientInfo{
463 Name: v.Name,
464 State: proto.MCPState(v.State),
465 Error: v.Error,
466 ToolCount: v.Counts.Tools,
467 PromptCount: v.Counts.Prompts,
468 ResourceCount: v.Counts.Resources,
469 ConnectedAt: v.ConnectedAt,
470 }
471 }
472 jsonEncode(w, result)
473}
474
475// handlePostWorkspaceMCPRefreshPrompts refreshes prompts for a named MCP server.
476//
477// @Summary Refresh MCP prompts
478// @Tags mcp
479// @Accept json
480// @Param id path string true "Workspace ID"
481// @Param request body proto.MCPNameRequest true "MCP name request"
482// @Success 200
483// @Failure 400 {object} proto.Error
484// @Failure 404 {object} proto.Error
485// @Failure 500 {object} proto.Error
486// @Router /workspaces/{id}/mcp/refresh-prompts [post]
487func (c *controllerV1) handlePostWorkspaceMCPRefreshPrompts(w http.ResponseWriter, r *http.Request) {
488 id := r.PathValue("id")
489
490 var req proto.MCPNameRequest
491 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
492 c.server.logError(r, "Failed to decode request", "error", err)
493 jsonError(w, http.StatusBadRequest, "failed to decode request")
494 return
495 }
496
497 c.backend.MCPRefreshPrompts(r.Context(), id, req.Name)
498 w.WriteHeader(http.StatusOK)
499}
500
501// handlePostWorkspaceMCPRefreshResources refreshes resources for a named MCP server.
502//
503// @Summary Refresh MCP resources
504// @Tags mcp
505// @Accept json
506// @Param id path string true "Workspace ID"
507// @Param request body proto.MCPNameRequest true "MCP name request"
508// @Success 200
509// @Failure 400 {object} proto.Error
510// @Failure 404 {object} proto.Error
511// @Failure 500 {object} proto.Error
512// @Router /workspaces/{id}/mcp/refresh-resources [post]
513func (c *controllerV1) handlePostWorkspaceMCPRefreshResources(w http.ResponseWriter, r *http.Request) {
514 id := r.PathValue("id")
515
516 var req proto.MCPNameRequest
517 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
518 c.server.logError(r, "Failed to decode request", "error", err)
519 jsonError(w, http.StatusBadRequest, "failed to decode request")
520 return
521 }
522
523 c.backend.MCPRefreshResources(r.Context(), id, req.Name)
524 w.WriteHeader(http.StatusOK)
525}