diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-03 21:15:50 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-03 21:15:50 +0000 |
| commit | 74cc740398cf2d90804ab19db728c844c2e056b7 (patch) | |
| tree | e8532d1da9273e1613beb7b762b16134da0de286 /internal/api/server.go | |
| parent | f527972f4d8311a09e639ede6c4da4ca669cfd5e (diff) | |
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 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/server.go')
| -rw-r--r-- | internal/api/server.go | 91 |
1 files changed, 63 insertions, 28 deletions
diff --git a/internal/api/server.go b/internal/api/server.go index 94095cb..315b64b 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -17,20 +17,25 @@ import ( // Server provides the REST API and WebSocket endpoint for Claudomator. type Server struct { - store *storage.DB - pool *executor.Pool - hub *Hub - logger *slog.Logger - mux *http.ServeMux + store *storage.DB + logStore logStore // injectable for tests; defaults to store + pool *executor.Pool + hub *Hub + logger *slog.Logger + mux *http.ServeMux + claudeBinPath string // path to claude binary; defaults to "claude" + elaborateCmdPath string // overrides claudeBinPath; used in tests } -func NewServer(store *storage.DB, pool *executor.Pool, logger *slog.Logger) *Server { +func NewServer(store *storage.DB, pool *executor.Pool, logger *slog.Logger, claudeBinPath string) *Server { s := &Server{ - store: store, - pool: pool, - hub: NewHub(), - logger: logger, - mux: http.NewServeMux(), + store: store, + logStore: store, + pool: pool, + hub: NewHub(), + logger: logger, + mux: http.NewServeMux(), + claudeBinPath: claudeBinPath, } s.routes() return s @@ -46,12 +51,20 @@ func (s *Server) StartHub() { } func (s *Server) routes() { + s.mux.HandleFunc("POST /api/tasks/elaborate", s.handleElaborateTask) s.mux.HandleFunc("POST /api/tasks", s.handleCreateTask) s.mux.HandleFunc("GET /api/tasks", s.handleListTasks) s.mux.HandleFunc("GET /api/tasks/{id}", s.handleGetTask) s.mux.HandleFunc("POST /api/tasks/{id}/run", s.handleRunTask) + s.mux.HandleFunc("GET /api/tasks/{id}/subtasks", s.handleListSubtasks) s.mux.HandleFunc("GET /api/tasks/{id}/executions", s.handleListExecutions) s.mux.HandleFunc("GET /api/executions/{id}", s.handleGetExecution) + s.mux.HandleFunc("GET /api/executions/{id}/logs/stream", s.handleStreamLogs) + s.mux.HandleFunc("GET /api/templates", s.handleListTemplates) + s.mux.HandleFunc("POST /api/templates", s.handleCreateTemplate) + s.mux.HandleFunc("GET /api/templates/{id}", s.handleGetTemplate) + s.mux.HandleFunc("PUT /api/templates/{id}", s.handleUpdateTemplate) + s.mux.HandleFunc("DELETE /api/templates/{id}", s.handleDeleteTemplate) s.mux.HandleFunc("GET /api/ws", s.handleWebSocket) s.mux.HandleFunc("GET /api/health", s.handleHealth) s.mux.Handle("GET /", http.FileServerFS(webui.Files)) @@ -80,12 +93,13 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { func (s *Server) handleCreateTask(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"` + 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"` + ParentTaskID string `json:"parent_task_id"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) @@ -94,17 +108,18 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) { now := time.Now().UTC() t := &task.Task{ - ID: uuid.New().String(), - Name: input.Name, - Description: input.Description, - Claude: input.Claude, - Priority: task.Priority(input.Priority), - Tags: input.Tags, - DependsOn: []string{}, - Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, - State: task.StatePending, - CreatedAt: now, - UpdatedAt: now, + ID: uuid.New().String(), + Name: input.Name, + Description: input.Description, + Claude: input.Claude, + Priority: task.Priority(input.Priority), + Tags: input.Tags, + DependsOn: []string{}, + Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, + State: task.StatePending, + CreatedAt: now, + UpdatedAt: now, + ParentTaskID: input.ParentTaskID, } if t.Priority == "" { t.Priority = task.PriorityNormal @@ -167,6 +182,13 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) { return } + if !task.ValidTransition(t.State, task.StateQueued) { + writeJSON(w, http.StatusConflict, map[string]string{ + "error": fmt.Sprintf("task cannot be queued from state %s", t.State), + }) + return + } + if err := s.store.UpdateTaskState(id, task.StateQueued); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return @@ -184,6 +206,19 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) { }) } +func (s *Server) handleListSubtasks(w http.ResponseWriter, r *http.Request) { + parentID := r.PathValue("id") + tasks, err := s.store.ListSubtasks(parentID) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + if tasks == nil { + tasks = []*task.Task{} + } + writeJSON(w, http.StatusOK, tasks) +} + func (s *Server) handleListExecutions(w http.ResponseWriter, r *http.Request) { taskID := r.PathValue("id") execs, err := s.store.ListExecutions(taskID) |
