Files
blackroad-sdk/go/client.go
Your Name 753f61a1e6 feat: @blackroad/sdk - Official TypeScript SDK
- GraphQL Client: queries, mutations, convenience methods
- Webhooks Client: create, list, test, trigger events
- Email Client: send templated emails, preview templates
- Full TypeScript type definitions
- Zero dependencies (native fetch)
- Connects to live APIs: GraphQL, Webhooks, Email
2026-02-14 22:27:31 -06:00

250 lines
6.0 KiB
Go

package blackroad
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
const (
defaultBaseURL = "https://api.blackroad.io/v1"
defaultTimeout = 30 * time.Second
defaultMaxRetries = 3
)
// ClientConfig contains configuration options for the client.
type ClientConfig struct {
// APIKey is the BlackRoad API key. If empty, reads from BLACKROAD_API_KEY env var.
APIKey string
// BaseURL is the API base URL. Defaults to https://api.blackroad.io/v1
BaseURL string
// Timeout is the request timeout. Defaults to 30 seconds.
Timeout time.Duration
// MaxRetries is the maximum number of retry attempts. Defaults to 3.
MaxRetries int
// HTTPClient allows providing a custom HTTP client.
HTTPClient *http.Client
}
// Client is the BlackRoad API client.
type Client struct {
apiKey string
baseURL string
timeout time.Duration
maxRetries int
httpClient *http.Client
// API modules
Agents *AgentAPI
Tasks *TaskAPI
Memory *MemoryAPI
}
// NewClient creates a new BlackRoad client.
//
// Example:
//
// client, err := blackroad.NewClient(&blackroad.ClientConfig{
// APIKey: "your-api-key",
// })
// if err != nil {
// log.Fatal(err)
// }
//
// agents, err := client.Agents.List(context.Background(), nil)
func NewClient(config *ClientConfig) (*Client, error) {
if config == nil {
config = &ClientConfig{}
}
apiKey := config.APIKey
if apiKey == "" {
apiKey = os.Getenv("BLACKROAD_API_KEY")
}
if apiKey == "" {
return nil, NewAuthenticationError("API key required. Set BLACKROAD_API_KEY environment variable or pass APIKey in config.")
}
baseURL := config.BaseURL
if baseURL == "" {
baseURL = os.Getenv("BLACKROAD_API_URL")
}
if baseURL == "" {
baseURL = defaultBaseURL
}
baseURL = strings.TrimSuffix(baseURL, "/")
timeout := config.Timeout
if timeout == 0 {
timeout = defaultTimeout
}
maxRetries := config.MaxRetries
if maxRetries == 0 {
maxRetries = defaultMaxRetries
}
httpClient := config.HTTPClient
if httpClient == nil {
httpClient = &http.Client{
Timeout: timeout,
}
}
c := &Client{
apiKey: apiKey,
baseURL: baseURL,
timeout: timeout,
maxRetries: maxRetries,
httpClient: httpClient,
}
// Initialize API modules
c.Agents = &AgentAPI{client: c}
c.Tasks = &TaskAPI{client: c}
c.Memory = &MemoryAPI{client: c}
return c, nil
}
// request makes an HTTP request to the API.
func (c *Client) request(ctx context.Context, method, endpoint string, body interface{}, params url.Values) ([]byte, error) {
fullURL := fmt.Sprintf("%s/%s", c.baseURL, strings.TrimPrefix(endpoint, "/"))
if len(params) > 0 {
fullURL = fmt.Sprintf("%s?%s", fullURL, params.Encode())
}
var bodyReader io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, NewConnectionError("failed to marshal request body", err)
}
bodyReader = bytes.NewReader(jsonBody)
}
var lastErr error
for attempt := 0; attempt < c.maxRetries; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
if err != nil {
return nil, NewConnectionError("failed to create request", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "blackroad-go/1.0.0")
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = NewConnectionError("request failed", err)
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
lastErr = NewConnectionError("failed to read response", err)
continue
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return respBody, nil
}
switch resp.StatusCode {
case 401:
return nil, NewAuthenticationError("invalid API key")
case 404:
return nil, NewNotFoundError(endpoint)
case 422:
return nil, NewValidationError(string(respBody))
case 429:
retryAfter := 1
if ra := resp.Header.Get("Retry-After"); ra != "" {
if parsed, err := strconv.Atoi(ra); err == nil {
retryAfter = parsed
}
}
if attempt < c.maxRetries-1 {
time.Sleep(time.Duration(retryAfter) * time.Second)
continue
}
return nil, NewRateLimitError(retryAfter)
default:
lastErr = &Error{
Message: fmt.Sprintf("API error: %s", string(respBody)),
Code: "API_ERROR",
StatusCode: resp.StatusCode,
}
}
}
if lastErr != nil {
return nil, lastErr
}
return nil, &Error{Message: "max retries exceeded"}
}
// Get makes a GET request.
func (c *Client) Get(ctx context.Context, endpoint string, params url.Values) ([]byte, error) {
return c.request(ctx, http.MethodGet, endpoint, nil, params)
}
// Post makes a POST request.
func (c *Client) Post(ctx context.Context, endpoint string, body interface{}) ([]byte, error) {
return c.request(ctx, http.MethodPost, endpoint, body, nil)
}
// Put makes a PUT request.
func (c *Client) Put(ctx context.Context, endpoint string, body interface{}) ([]byte, error) {
return c.request(ctx, http.MethodPut, endpoint, body, nil)
}
// Delete makes a DELETE request.
func (c *Client) Delete(ctx context.Context, endpoint string) ([]byte, error) {
return c.request(ctx, http.MethodDelete, endpoint, nil, nil)
}
// Health checks the API health status.
func (c *Client) Health(ctx context.Context) (*HealthStatus, error) {
resp, err := c.Get(ctx, "/health", nil)
if err != nil {
return nil, err
}
var health HealthStatus
if err := json.Unmarshal(resp, &health); err != nil {
return nil, NewConnectionError("failed to parse health response", err)
}
return &health, nil
}
// Version returns the API version.
func (c *Client) Version(ctx context.Context) (string, error) {
resp, err := c.Get(ctx, "/version", nil)
if err != nil {
return "", err
}
var result struct {
Version string `json:"version"`
}
if err := json.Unmarshal(resp, &result); err != nil {
return "", NewConnectionError("failed to parse version response", err)
}
return result.Version, nil
}