// Package backend is the typed HTTP client for Dialectic.Backend's API. // Endpoint shapes mirror Dialectic.OpenclawPlugin/plugin/src/backend-client.ts // so the two plugins drop into the same backend without per-plugin // adapters. package backend import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "time" ) type Client struct { BaseURL string APIKey string HTTP *http.Client } func New(baseURL, apiKey string) *Client { return &Client{ BaseURL: strings.TrimRight(baseURL, "/"), APIKey: apiKey, HTTP: &http.Client{Timeout: 15 * time.Second}, } } func (c *Client) Get(ctx context.Context, path string) (json.RawMessage, error) { return c.do(ctx, http.MethodGet, path, nil) } func (c *Client) Post(ctx context.Context, path string, body any) (json.RawMessage, error) { return c.do(ctx, http.MethodPost, path, body) } func (c *Client) Put(ctx context.Context, path string, body any) (json.RawMessage, error) { return c.do(ctx, http.MethodPut, path, body) } func (c *Client) do(ctx context.Context, method, path string, body any) (json.RawMessage, error) { if c.APIKey == "" { return nil, errors.New("dialectic api key not configured — set plugins.dialectic.config.apiKey or agentKeys[]") } url := c.BaseURL + ensureLeadingSlash(path) var rdr io.Reader if body != nil { raw, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal %s %s: %w", method, path, err) } rdr = bytes.NewReader(raw) } req, err := http.NewRequestWithContext(ctx, method, url, rdr) if err != nil { return nil, err } if body != nil { req.Header.Set("Content-Type", "application/json") } req.Header.Set("Authorization", "Bearer "+c.APIKey) req.Header.Set("Accept", "application/json") res, err := c.HTTP.Do(req) if err != nil { return nil, fmt.Errorf("%s %s: %w", method, path, err) } defer res.Body.Close() raw, _ := io.ReadAll(res.Body) if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("%s %s → %d: %s", method, path, res.StatusCode, truncate(raw, 500)) } if len(raw) == 0 { return nil, nil } return raw, nil } func ensureLeadingSlash(p string) string { if strings.HasPrefix(p, "/") { return p } return "/" + p } func truncate(b []byte, n int) string { if len(b) <= n { return string(b) } return string(b[:n]) + "…" } // TopicDetail is the minimal shape the signup tool needs (debate window // for HF pre-check). The Dialectic backend returns more fields — they // pass through untouched in the raw JSON the tools surface to agents. type TopicDetail struct { ID string `json:"id"` DebateStartAt string `json:"debate_start_at"` DebateEndAt string `json:"debate_end_at"` }