// Package aw is a thin ActivityWatch REST client: it creates buckets and reads // and writes events. It holds no Keel concepts, so it stays a leaf package. package aw import ( "bytes" "context" "encoding/json" "fmt" "net/http" "os" "strings" "time" ) // Event is one ActivityWatch event. Duration is seconds; for Keel's discrete // derived events it is 0. type Event struct { Timestamp time.Time Duration float64 Data map[string]any } // Client talks to an aw-server over its /api/0 REST surface. type Client struct { baseURL string hc *http.Client } // New builds a client for the given base URL (e.g. http://localhost:5600). func New(baseURL string) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), hc: &http.Client{Timeout: 5 * time.Second}, } } // EnsureBucket creates the bucket if absent. It is idempotent: an already-exists // response (304) is success. func (c *Client) EnsureBucket(ctx context.Context, id, eventType, client string) error { body, _ := json.Marshal(map[string]string{ "client": client, "type": eventType, "hostname": hostname(), }) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/0/buckets/"+id, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := c.hc.Do(req) if err != nil { return err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusNotModified: return nil default: return fmt.Errorf("aw: create bucket %s: status %d", id, resp.StatusCode) } } func hostname() string { if h, err := os.Hostname(); err == nil && h != "" { return h } return "keel" }