// Drop-in Go client library for the Production Board HTTP API. // // Save this file alongside your code as `prod_client.go` (or in its // own subpackage) and use the Client type: // // import "yourproject/prod_client" // // c := prod_client.New("pat_...") // rows, err := c.AccountList(&prod_client.ListOpts{Limit: 20, Sort: "-created_at"}) // // Every endpoint exposed by the HTTP API is wrapped as a // `` method on Client. List endpoints take *ListOpts; // get/update/delete endpoints take the row id as their first argument. // // Provided as-is, with no warranty. Vendor freely; modify as needed. // Targets Go 1.21+; uses only the standard library. // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. // Local edits will be overwritten by the once-per-day version check. package prod_client import ( "bytes" "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" ) // ── Identity (substituted at generation time) ─────────────────────── const ( AppSlug = "prod" AppName = "Production Board" ModuleName = "prod_client" ClientVersion = "0.3.12" Language = "go" defaultBase = "https://qtssystem.com" ) // TypesJSON is the per-type metadata baked at generation time. // Available at runtime when calling code needs to know the legal // filters / sort columns / max_limit for a model without a second // round-trip. const TypesJSON = `{"board":{"ops":["list","read","create","update","delete"],"create_fields":["name","description","accent","settings","tags","columns"],"update_fields":["name","description","accent","settings","tags","columns"],"allowed_filters":["data__name","data__accent","data__tags","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__name"],"default_sort":"created_at","max_limit":50,"fields":[{"name":"name","type":"string","max_len":200},{"name":"tags","type":"tags"},{"name":"accent","type":"enum","values":["slate","gray","blue","indigo","violet","fuchsia","amber","orange","emerald","green","rose","red"]},{"name":"settings","type":"dict"},{"name":"description","type":"string","max_len":2000}]},"card":{"ops":["list","read","create","update","delete"],"create_fields":["title","description","status","position","priority","tags","assignee","due_date","board_id"],"update_fields":["title","description","status","position","priority","tags","assignee","due_date","board_id"],"allowed_filters":["data__status","data__priority","data__tags","data__assignee","data__board_id","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__position","data__status","data__priority","data__due_date"],"default_sort":"data__position","max_limit":200,"fields":[{"name":"tags","type":"tags"},{"name":"title","type":"string","max_len":200},{"name":"status","type":"string","max_len":64},{"name":"assignee","type":"string","max_len":64},{"name":"board_id","type":"string","max_len":64,"ref":{"type":"board","owned":true,"optional":true}},{"name":"due_date","type":"string","max_len":32},{"name":"position","type":"number"},{"name":"priority","type":"enum","values":["low","medium","high","critical"]},{"name":"description","type":"string","max_len":4000}]}}` // ── Configuration ────────────────────────────────────────────────── // ListOpts mirrors the standard query parameters the list endpoints // accept. Filters carries arbitrary additional ?key=value pairs. type ListOpts struct { Limit int Offset int Sort string Q string Filters map[string]any } // Client is the per-app HTTP client. Reuse across requests; safe for // concurrent use. type Client struct { HTTPClient *http.Client BaseURL string Token string once sync.Once deviceID string sessID string } // New returns a Client wired to the host this library was generated // against. Pass a personal access token; an empty string falls back to // the XCLIENT_TOKEN environment variable. func New(token string) *Client { base := os.Getenv("XCLIENT_BASE_URL") if base == "" { base = defaultBase } if token == "" { token = os.Getenv("XCLIENT_TOKEN") } return &Client{ HTTPClient: &http.Client{Timeout: 30 * time.Second}, BaseURL: strings.TrimRight(base, "/"), Token: token, } } func (c *Client) initIDs() { c.once.Do(func() { c.deviceID = loadOrMintDeviceID() c.sessID = mintUUID() }) } // ── Identifier persistence ───────────────────────────────────────── func stateDir() string { home, err := os.UserHomeDir() if err != nil || home == "" { return "" } d := filepath.Join(home, "."+ModuleName) _ = os.MkdirAll(d, 0o700) return d } func mintUUID() string { var b [16]byte _, _ = rand.Read(b[:]) b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 h := hex.EncodeToString(b[:]) return h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:32] } func loadOrMintDeviceID() string { dir := stateDir() if dir == "" { return mintUUID() } f := filepath.Join(dir, "device.json") if raw, err := os.ReadFile(f); err == nil { var blob struct { DeviceID string `json:"device_id"` } if json.Unmarshal(raw, &blob) == nil && len(blob.DeviceID) >= 32 { return blob.DeviceID } } id := mintUUID() body, _ := json.Marshal(map[string]string{"device_id": id}) _ = os.WriteFile(f, body, 0o600) return id } // ── Telemetry toggles ────────────────────────────────────────────── func autoupdateEnabled() bool { v := strings.ToLower(os.Getenv("XCLIENT_NO_AUTOUPDATE")) return v != "1" && v != "true" && v != "yes" } // ── Editor / runtime fingerprint ─────────────────────────────────── func fingerprint() map[string]any { out := map[string]any{ "go_version": runtime.Version(), "os": runtime.GOOS, "arch": runtime.GOARCH, } out["term_program"] = os.Getenv("TERM_PROGRAM") out["editor_env"] = os.Getenv("EDITOR") out["ci"] = os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" out["claude_code"] = os.Getenv("CLAUDECODE") != "" || os.Getenv("CLAUDE_CODE_ENTRYPOINT") != "" out["codex"] = os.Getenv("CODEX_HOME") != "" tp := strings.ToLower(os.Getenv("TERM_PROGRAM")) out["vscode"] = tp == "vscode" && os.Getenv("CURSOR_TRACE_ID") == "" out["cursor"] = os.Getenv("CURSOR_TRACE_ID") != "" out["antigravity"] = os.Getenv("ANTIGRAVITY_TRACE_ID") != "" out["jetbrains"] = strings.Contains(tp, "jetbrains") return out } // ── HTTP transport ───────────────────────────────────────────────── // APIError wraps a non-2xx response. type APIError struct { Status int Message string Body any } func (e *APIError) Error() string { return fmt.Sprintf("HTTP %d: %s", e.Status, e.Message) } var retryableStatus = map[int]struct{}{ 408: {}, 425: {}, 429: {}, 500: {}, 502: {}, 503: {}, 504: {}, } func backoff(attempt int, retryAfterSec float64) time.Duration { if retryAfterSec >= 0 { if retryAfterSec > 60 { retryAfterSec = 60 } return time.Duration(retryAfterSec * float64(time.Second)) } delay := float64(int(1) << uint(attempt)) if delay > 60 { delay = 60 } return time.Duration(delay * float64(time.Second)) } func (c *Client) userAgent() string { return fmt.Sprintf("%s/%s (lib/%s; go/%s; %s)", ModuleName, ClientVersion, Language, runtime.Version(), runtime.GOOS) } // requestJSON fires one method+path against the API, JSON in / JSON // out. Pass nil body for read-only verbs. func (c *Client) requestJSON(method, path string, body any) (map[string]any, error) { c.maybeAutoupdate() c.initIDs() u := c.BaseURL + path var data []byte if body != nil { var err error data, err = json.Marshal(body) if err != nil { return nil, err } } const maxRetries = 3 var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { var bodyReader io.Reader if data != nil { bodyReader = bytes.NewReader(data) } req, err := http.NewRequest(method, u, bodyReader) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", c.userAgent()) req.Header.Set("X-Client-Channel", "client_"+Language) req.Header.Set("X-Client-Version", ClientVersion) req.Header.Set("X-Analytics-Device-Id", c.deviceID) req.Header.Set("X-Analytics-Session-Id", c.sessID) if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } if data != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.HTTPClient.Do(req) if err != nil { lastErr = err if attempt+1 < maxRetries { time.Sleep(backoff(attempt, -1)) continue } c.emitCallEvent(method, path, 0, false) return nil, &APIError{Status: 0, Message: err.Error()} } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() c.maybePersistRefresh(resp.Header) if _, retry := retryableStatus[resp.StatusCode]; retry && attempt+1 < maxRetries { ra := -1.0 if v := resp.Header.Get("Retry-After"); v != "" { if f, err2 := strconv.ParseFloat(v, 64); err2 == nil { ra = f } } time.Sleep(backoff(attempt, ra)) continue } if resp.StatusCode >= 400 { var parsed any _ = json.Unmarshal(raw, &parsed) msg := http.StatusText(resp.StatusCode) if m, ok := parsed.(map[string]any); ok { if d, ok2 := m["detail"].(string); ok2 { msg = d } else if d, ok2 := m["message"].(string); ok2 { msg = d } } c.emitCallEvent(method, path, resp.StatusCode, false) return nil, &APIError{Status: resp.StatusCode, Message: msg, Body: parsed} } c.emitCallEvent(method, path, resp.StatusCode, true) if len(raw) == 0 { return nil, nil } var out map[string]any if err := json.Unmarshal(raw, &out); err != nil { return nil, err } return out, nil } if lastErr != nil { c.emitCallEvent(method, path, 0, false) return nil, &APIError{Status: 0, Message: lastErr.Error()} } return nil, errors.New("request failed") } // requestList wraps requestJSON for list endpoints, lifting *ListOpts // into the query string. func (c *Client) requestList(path string, opts *ListOpts) (map[string]any, error) { q := url.Values{} if opts != nil { if opts.Limit > 0 { q.Set("limit", strconv.Itoa(opts.Limit)) } if opts.Offset > 0 { q.Set("offset", strconv.Itoa(opts.Offset)) } if opts.Sort != "" { q.Set("sort", opts.Sort) } if opts.Q != "" { q.Set("q", opts.Q) } for k, v := range opts.Filters { if v == nil { continue } q.Set(k, fmt.Sprint(v)) } } if encoded := q.Encode(); encoded != "" { path = path + "?" + encoded } return c.requestJSON("GET", path, nil) } func (c *Client) maybePersistRefresh(h http.Header) { if v := h.Get("x-auth-refresh-token"); v != "" { c.Token = v } } // ── Analytics ────────────────────────────────────────────────────── var metaSentOnce sync.Once func (c *Client) emitCallEvent(method, pathStr string, status int, ok bool) { go func() { defer func() { _ = recover() }() meta := map[string]any{ "channel": "client_" + Language, "client_version": ClientVersion, "module_name": ModuleName, "language": Language, "os": runtime.GOOS, "go_version": runtime.Version(), } var addEnv bool metaSentOnce.Do(func() { addEnv = true }) if addEnv { meta["env"] = fingerprint() } evt := map[string]any{ "type": "client.call", "ts_client": time.Now().Unix(), "meta": map[string]any{ "method": strings.ToUpper(method), "path": strings.SplitN(pathStr, "?", 2)[0], "status": status, "ok": ok, }, } body := map[string]any{ "device_id": c.deviceID, "session_id": c.sessID, "events": []any{evt}, "meta": meta, } raw, _ := json.Marshal(body) req, err := http.NewRequest("POST", c.BaseURL+"/xapi2/analytics/track", bytes.NewReader(raw)) if err != nil { return } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", c.userAgent()) hc := &http.Client{Timeout: 4 * time.Second} resp, err := hc.Do(req) if err != nil { return } _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() }() } // ── Auto-update ──────────────────────────────────────────────────── var autoupdateOnce sync.Once func (c *Client) maybeAutoupdate() { autoupdateOnce.Do(func() { if !autoupdateEnabled() { return } go c.runAutoupdate() }) } func (c *Client) runAutoupdate() { defer func() { _ = recover() }() dir := stateDir() if dir == "" { return } stamp := filepath.Join(dir, "update_check.json") if raw, err := os.ReadFile(stamp); err == nil { var blob struct { CheckedAt int64 `json:"checked_at"` } if json.Unmarshal(raw, &blob) == nil { if time.Now().Unix()-blob.CheckedAt < 86400 { return } } } hc := &http.Client{Timeout: 6 * time.Second} resp, err := hc.Get(c.BaseURL + "/xapi2/clients/version") if err != nil { return } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() var payload struct { Version string `json:"version"` } if json.Unmarshal(raw, &payload) != nil { return } stampBody, _ := json.Marshal(map[string]any{"checked_at": time.Now().Unix()}) _ = os.WriteFile(stamp, stampBody, 0o600) if payload.Version == "" || payload.Version == ClientVersion { return } // Source replacement is intentionally a no-op in Go - the user is // running a compiled binary, the .go file on disk is just a record // of the version they vendored. Surface the new version through // the next build. } // BoardList lists board rows. Pass nil opts for defaults. func (c *Client) BoardList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/board", opts) } // BoardGet fetches one board row by id. func (c *Client) BoardGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/board/"+id, nil) } // BoardCreate creates a new board row. func (c *Client) BoardCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/board", data) } // BoardUpdate patches an existing board row. func (c *Client) BoardUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/board/"+id, data) } // BoardDelete deletes a board row. func (c *Client) BoardDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/board/"+id, nil) return err } // CardList lists card rows. Pass nil opts for defaults. func (c *Client) CardList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/card", opts) } // CardGet fetches one card row by id. func (c *Client) CardGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/card/"+id, nil) } // CardCreate creates a new card row. func (c *Client) CardCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/card", data) } // CardUpdate patches an existing card row. func (c *Client) CardUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/card/"+id, data) } // CardDelete deletes a card row. func (c *Client) CardDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/card/"+id, nil) return err }