From 74cc740398cf2d90804ab19db728c844c2e056b7 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Tue, 3 Mar 2026 21:15:50 +0000 Subject: Add elaborate, logs-stream, templates, and subtask-list endpoints - POST /api/tasks/elaborate: calls claude to draft a task config from a natural-language prompt - GET /api/executions/{id}/logs/stream: SSE tail of stdout.log - CRUD /api/templates: create/list/get/update/delete reusable task configs - GET /api/tasks/{id}/subtasks: list child tasks - Server.NewServer accepts claudeBinPath for elaborate; injectable elaborateCmdPath and logStore for test isolation - Valid-transition guard added to POST /api/tasks/{id}/run - CLI passes claude binary path through to the server Co-Authored-By: Claude Sonnet 4.6 --- internal/api/templates.go | 144 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 internal/api/templates.go (limited to 'internal/api/templates.go') diff --git a/internal/api/templates.go b/internal/api/templates.go new file mode 100644 index 0000000..0139895 --- /dev/null +++ b/internal/api/templates.go @@ -0,0 +1,144 @@ +package api + +import ( + "encoding/json" + "errors" + "net/http" + "time" + + "github.com/google/uuid" + "github.com/thepeterstone/claudomator/internal/storage" + "github.com/thepeterstone/claudomator/internal/task" +) + +func (s *Server) handleListTemplates(w http.ResponseWriter, r *http.Request) { + templates, err := s.store.ListTemplates() + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + if templates == nil { + templates = []*storage.Template{} + } + writeJSON(w, http.StatusOK, templates) +} + +func (s *Server) handleCreateTemplate(w http.ResponseWriter, r *http.Request) { + var input struct { + Name string `json:"name"` + Description string `json:"description"` + Claude task.ClaudeConfig `json:"claude"` + Timeout string `json:"timeout"` + Priority string `json:"priority"` + Tags []string `json:"tags"` + } + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) + return + } + if input.Name == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) + return + } + + now := time.Now().UTC() + tmpl := &storage.Template{ + ID: uuid.New().String(), + Name: input.Name, + Description: input.Description, + Claude: input.Claude, + Timeout: input.Timeout, + Priority: input.Priority, + Tags: input.Tags, + CreatedAt: now, + UpdatedAt: now, + } + if tmpl.Priority == "" { + tmpl.Priority = "normal" + } + if tmpl.Tags == nil { + tmpl.Tags = []string{} + } + + if err := s.store.CreateTemplate(tmpl); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusCreated, tmpl) +} + +func (s *Server) handleGetTemplate(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + tmpl, err := s.store.GetTemplate(id) + if err != nil { + if errors.Is(err, storage.ErrTemplateNotFound) { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "template not found"}) + return + } + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, tmpl) +} + +// handleUpdateTemplate fully replaces all fields of the template identified by {id}. +// All fields from the request body overwrite existing values; name is required. +func (s *Server) handleUpdateTemplate(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + existing, err := s.store.GetTemplate(id) + if err != nil { + if errors.Is(err, storage.ErrTemplateNotFound) { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "template not found"}) + return + } + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + var input struct { + Name string `json:"name"` + Description string `json:"description"` + Claude task.ClaudeConfig `json:"claude"` + Timeout string `json:"timeout"` + Priority string `json:"priority"` + Tags []string `json:"tags"` + } + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) + return + } + if input.Name == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) + return + } + + existing.Name = input.Name + existing.Description = input.Description + existing.Claude = input.Claude + existing.Timeout = input.Timeout + existing.Priority = input.Priority + if input.Tags != nil { + existing.Tags = input.Tags + } + existing.UpdatedAt = time.Now().UTC() + + if err := s.store.UpdateTemplate(existing); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, existing) +} + +func (s *Server) handleDeleteTemplate(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + err := s.store.DeleteTemplate(id) + if err != nil { + if errors.Is(err, storage.ErrTemplateNotFound) { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "template not found"}) + return + } + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + w.WriteHeader(http.StatusNoContent) +} -- cgit v1.2.3