summaryrefslogtreecommitdiff
path: root/internal/storage/templates.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-03 21:15:50 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-03 21:15:50 +0000
commit74cc740398cf2d90804ab19db728c844c2e056b7 (patch)
treee8532d1da9273e1613beb7b762b16134da0de286 /internal/storage/templates.go
parentf527972f4d8311a09e639ede6c4da4ca669cfd5e (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/storage/templates.go')
-rw-r--r--internal/storage/templates.go140
1 files changed, 140 insertions, 0 deletions
diff --git a/internal/storage/templates.go b/internal/storage/templates.go
new file mode 100644
index 0000000..350b4f8
--- /dev/null
+++ b/internal/storage/templates.go
@@ -0,0 +1,140 @@
+package storage
+
+import (
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/thepeterstone/claudomator/internal/task"
+)
+
+// ErrTemplateNotFound is returned when a template ID does not exist.
+var ErrTemplateNotFound = errors.New("template not found")
+
+// Template is a reusable task configuration saved for repeated use.
+type Template struct {
+ ID string `json:"id"`
+ 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"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// CreateTemplate inserts a new template.
+func (s *DB) CreateTemplate(tmpl *Template) error {
+ configJSON, err := json.Marshal(tmpl.Claude)
+ if err != nil {
+ return fmt.Errorf("marshaling config: %w", err)
+ }
+ tagsJSON, err := json.Marshal(tmpl.Tags)
+ if err != nil {
+ return fmt.Errorf("marshaling tags: %w", err)
+ }
+ _, err = s.db.Exec(`
+ INSERT INTO templates (id, name, description, config_json, timeout, priority, tags_json, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ tmpl.ID, tmpl.Name, tmpl.Description, string(configJSON),
+ tmpl.Timeout, tmpl.Priority, string(tagsJSON),
+ tmpl.CreatedAt.UTC(), tmpl.UpdatedAt.UTC(),
+ )
+ return err
+}
+
+// GetTemplate retrieves a template by ID, returning ErrTemplateNotFound if missing.
+func (s *DB) GetTemplate(id string) (*Template, error) {
+ row := s.db.QueryRow(`SELECT id, name, description, config_json, timeout, priority, tags_json, created_at, updated_at FROM templates WHERE id = ?`, id)
+ return scanTemplate(row)
+}
+
+// ListTemplates returns all templates ordered by name.
+func (s *DB) ListTemplates() ([]*Template, error) {
+ rows, err := s.db.Query(`SELECT id, name, description, config_json, timeout, priority, tags_json, created_at, updated_at FROM templates ORDER BY name ASC`)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var templates []*Template
+ for rows.Next() {
+ tmpl, err := scanTemplate(rows)
+ if err != nil {
+ return nil, err
+ }
+ templates = append(templates, tmpl)
+ }
+ return templates, rows.Err()
+}
+
+// UpdateTemplate fully replaces a template's fields. Returns ErrTemplateNotFound if the ID is missing.
+func (s *DB) UpdateTemplate(tmpl *Template) error {
+ configJSON, err := json.Marshal(tmpl.Claude)
+ if err != nil {
+ return fmt.Errorf("marshaling config: %w", err)
+ }
+ tagsJSON, err := json.Marshal(tmpl.Tags)
+ if err != nil {
+ return fmt.Errorf("marshaling tags: %w", err)
+ }
+ result, err := s.db.Exec(`
+ UPDATE templates SET name = ?, description = ?, config_json = ?, timeout = ?, priority = ?, tags_json = ?, updated_at = ?
+ WHERE id = ?`,
+ tmpl.Name, tmpl.Description, string(configJSON), tmpl.Timeout, tmpl.Priority, string(tagsJSON),
+ tmpl.UpdatedAt.UTC(), tmpl.ID,
+ )
+ if err != nil {
+ return err
+ }
+ n, err := result.RowsAffected()
+ if err != nil {
+ return err
+ }
+ if n == 0 {
+ return ErrTemplateNotFound
+ }
+ return nil
+}
+
+// DeleteTemplate removes a template by ID. Returns ErrTemplateNotFound if the ID is missing.
+func (s *DB) DeleteTemplate(id string) error {
+ result, err := s.db.Exec(`DELETE FROM templates WHERE id = ?`, id)
+ if err != nil {
+ return err
+ }
+ n, err := result.RowsAffected()
+ if err != nil {
+ return err
+ }
+ if n == 0 {
+ return ErrTemplateNotFound
+ }
+ return nil
+}
+
+func scanTemplate(row scanner) (*Template, error) {
+ var (
+ tmpl Template
+ configJSON string
+ tagsJSON string
+ )
+ err := row.Scan(&tmpl.ID, &tmpl.Name, &tmpl.Description, &configJSON,
+ &tmpl.Timeout, &tmpl.Priority, &tagsJSON, &tmpl.CreatedAt, &tmpl.UpdatedAt)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, ErrTemplateNotFound
+ }
+ return nil, err
+ }
+ if err := json.Unmarshal([]byte(configJSON), &tmpl.Claude); err != nil {
+ return nil, fmt.Errorf("unmarshaling config: %w", err)
+ }
+ if err := json.Unmarshal([]byte(tagsJSON), &tmpl.Tags); err != nil {
+ return nil, fmt.Errorf("unmarshaling tags: %w", err)
+ }
+ return &tmpl, nil
+}