clockify

package
v1.3.0-rc.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrCircuitBreakerOpen = errors.New("clockify upstream circuit breaker open")

Functions

func CentsFromReportNumber

func CentsFromReportNumber(v float64) int64

CentsFromReportNumber converts a JSON number coming from the reports host (always integer-cents-as-float, per findings/rates-and-reports.md) to int64 cents using banker's rounding. The raw float is preserved by the caller for audit.

func ListAll

func ListAll[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string) ([]T, error)

ListAll fetches every page into a single slice, bounded by the default pagination row cap.

func ListAllFunc added in v1.2.1

func ListAllFunc[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string, onPage func([]T) error) error

ListAllFunc scans pages and invokes onPage for each non-empty page without retaining earlier pages.

func ListAllFuncWithOptions

func ListAllFuncWithOptions[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string, opts ListAllOptions, onPage func([]T) error) error

ListAllFuncWithOptions scans pages using opts and invokes onPage for each non-empty page without retaining earlier pages.

func ListAllWithOptions

func ListAllWithOptions[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string, opts ListAllOptions) ([]T, error)

ListAllWithOptions fetches every page into a single slice using opts.

func ParseRateAmount added in v1.2.2

func ParseRateAmount(s string) (int64, error)

ParseRateAmount accepts the JSON number form that Clockify uses in rate-update request bodies ("amount":1234 or "amount":1234.0) and returns it as int64 cents. Used by tools that need to round-trip a caller's rate amount through the Clockify HTTP client.

Types

type APIError

type APIError struct {
	Method      string
	Path        string
	StatusCode  int
	Status      string
	Body        string
	RetryAfter  time.Duration
	Translation *ErrorTranslation
}

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) ErrorTranslation

func (e *APIError) ErrorTranslation() any

func (*APIError) Sanitized

func (e *APIError) Sanitized() string

Sanitized returns the error string without the upstream response body, suitable for hosted/shared deployments where the body might leak per-tenant information across tenants. Server-side logs still call Error() so operators can see the full diagnostic; the sanitised form is only what we hand to the MCP client.

type CircuitBreaker

type CircuitBreaker struct {
	// contains filtered or unexported fields
}

func NewCircuitBreaker

func NewCircuitBreaker(cfg CircuitBreakerConfig) *CircuitBreaker

func (*CircuitBreaker) After

func (b *CircuitBreaker) After(endpoint, method string, upstream5xx bool)

func (*CircuitBreaker) Before

func (b *CircuitBreaker) Before(endpoint, method string) error

func (*CircuitBreaker) StateValue

func (b *CircuitBreaker) StateValue(endpoint, method string) float64

type CircuitBreakerConfig

type CircuitBreakerConfig struct {
	Enabled          bool
	FailureThreshold int
	OpenDuration     time.Duration
	HalfOpenProbes   int
}

type CircuitBreakerOpenError

type CircuitBreakerOpenError struct {
	Endpoint   string
	Method     string
	RetryAfter time.Duration
}

func (*CircuitBreakerOpenError) Error

func (e *CircuitBreakerOpenError) Error() string

func (*CircuitBreakerOpenError) Is

func (e *CircuitBreakerOpenError) Is(target error) bool

type Client

type Client struct {
	// contains filtered or unexported fields
}

func NewClient

func NewClient(apiKey, baseURL string, timeout time.Duration, maxRetries int) *Client

func (*Client) Close

func (c *Client) Close()

Close releases idle connections held by the client.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, path string) error

func (*Client) DeleteReports

func (c *Client) DeleteReports(ctx context.Context, path string) error

DeleteReports performs a DELETE against the reports host.

func (*Client) DeleteWithQuery

func (c *Client) DeleteWithQuery(ctx context.Context, path string, query map[string]string) error

func (*Client) Get

func (c *Client) Get(ctx context.Context, path string, query map[string]string, out any) error

func (*Client) GetReports

func (c *Client) GetReports(ctx context.Context, path string, query map[string]string, out any) error

GetReports performs a GET against the reports host. Use this for shared-report endpoints; everything else should stay on Get.

func (*Client) GetReportsRaw added in v1.2.1

func (c *Client) GetReportsRaw(ctx context.Context, path string, query map[string]string) (*RawResponse, error)

GetReportsRaw is the binary-aware sibling of GetReports. It runs the same retry/tracing/metrics pipeline but skips the JSON unmarshal, exposing the raw body bytes and response headers so callers can inspect Content-Type and Content-Disposition. Use for export endpoints; use GetReports for JSON-shaped reads.

func (*Client) GetValues

func (c *Client) GetValues(ctx context.Context, path string, query url.Values, out any) error

func (*Client) Patch

func (c *Client) Patch(ctx context.Context, path string, body any, out any) error

func (*Client) Post

func (c *Client) Post(ctx context.Context, path string, body any, out any) error

func (*Client) PostMultipart

func (c *Client) PostMultipart(ctx context.Context, path string, form url.Values, out any) error

PostMultipart performs a POST with a multipart/form-data body. Each form key maps to one or more values; multi-value keys are written as repeated form fields under the same name.

func (*Client) PostMultipartWithFiles

func (c *Client) PostMultipartWithFiles(ctx context.Context, path string, form url.Values, files []MultipartFile, out any) error

PostMultipartWithFiles performs a POST with form fields plus binary file parts. File payloads are bounded to keep MCP requests from becoming an unbounded memory sink.

func (*Client) PostReports

func (c *Client) PostReports(ctx context.Context, path string, body any, out any) error

PostReports performs a POST against the reports host.

func (*Client) PostReportsRaw

func (c *Client) PostReportsRaw(ctx context.Context, path string, body any) (*RawResponse, error)

PostReportsRaw is the binary-aware sibling of PostReports. It is used for report exportType values such as PDF/CSV/XLSX/ZIP where the reports API may return a file body rather than JSON.

func (*Client) PostWithQuery

func (c *Client) PostWithQuery(ctx context.Context, path string, query map[string]string, body any, out any) error

func (*Client) Put

func (c *Client) Put(ctx context.Context, path string, body any, out any) error

func (*Client) PutMultipart

func (c *Client) PutMultipart(ctx context.Context, path string, form url.Values, out any) error

PutMultipart performs a PUT with a multipart/form-data body. Same encoding rules as PostMultipart.

func (*Client) PutMultipartWithFiles

func (c *Client) PutMultipartWithFiles(ctx context.Context, path string, form url.Values, files []MultipartFile, out any) error

PutMultipartWithFiles performs a PUT with form fields plus binary file parts.

func (*Client) PutReports

func (c *Client) PutReports(ctx context.Context, path string, body any, out any) error

PutReports performs a PUT against the reports host.

func (*Client) PutWithQuery

func (c *Client) PutWithQuery(ctx context.Context, path string, query map[string]string, body any, out any) error

func (*Client) ReportsBaseURL

func (c *Client) ReportsBaseURL() string

ReportsBaseURL returns the base URL for endpoints that live on Clockify's reports host (reports.api.clockify.me/v1). The reports API is a separate host from api.clockify.me — hitting the reports paths on the primary host returns 404 ("No static resource"), and hitting them on the reports host with the /api/v1 prefix is also 404. See findings/shared-reports.md.

Derivation: when the primary base URL matches the canonical production host, swap to reports.api.clockify.me and drop the /api segment. Otherwise (test stubs, custom proxies, on-prem mirrors), return the primary base URL unchanged so existing fixtures continue to work without per-test wiring.

func (*Client) RequestJSONValues added in v1.2.2

func (c *Client) RequestJSONValues(ctx context.Context, reportsHost bool, method, path string, query url.Values, body any, out any) error

RequestJSONValues is the generic JSON transport used by the allowlisted documented-API MCP surface. It deliberately stays below the domain helpers: callers must supply a validated method/path and already-normalized query.

func (*Client) RequestMultipartValues added in v1.2.2

func (c *Client) RequestMultipartValues(ctx context.Context, reportsHost bool, method, path string, query url.Values, form url.Values, out any) error

RequestMultipartValues sends multipart form fields to a validated documented endpoint. File streaming is intentionally not exposed here; the probe-lab docs currently accept form fields for the covered MCP use cases.

func (*Client) RequestMultipartValuesWithFiles

func (c *Client) RequestMultipartValuesWithFiles(ctx context.Context, reportsHost bool, method, path string, query url.Values, form url.Values, files []MultipartFile, out any) error

RequestMultipartValuesWithFiles sends multipart form fields plus file parts to a validated documented endpoint.

func (*Client) RequestRawValues

func (c *Client) RequestRawValues(ctx context.Context, reportsHost bool, method, path string, query url.Values, body any) (*RawResponse, error)

RequestRawValues is the binary-aware sibling of RequestJSONValues.

func (*Client) SetCircuitBreaker

func (c *Client) SetCircuitBreaker(cfg CircuitBreakerConfig)

func (*Client) SetUserAgent

func (c *Client) SetUserAgent(ua string)

SetUserAgent sets the User-Agent string sent with every request.

type ClientEntity

type ClientEntity struct {
	ID           string `json:"id"`
	Name         string `json:"name"`
	Address      string `json:"address,omitempty"`
	Archived     bool   `json:"archived,omitempty"`
	CCEmails     any    `json:"ccEmails,omitempty"`
	CurrencyCode string `json:"currencyCode,omitempty"`
	CurrencyID   string `json:"currencyId,omitempty"`
	Email        string `json:"email,omitempty"`
	Note         string `json:"note,omitempty"`
	WorkspaceID  string `json:"workspaceId,omitempty"`
}

type ErrorTranslation

type ErrorTranslation struct {
	Message     string   `json:"message"`
	Remediation string   `json:"remediation,omitempty"`
	Refs        []string `json:"refs,omitempty"`
}

func TranslateAPIError

func TranslateAPIError(statusCode int, body string) ErrorTranslation

type ListAllOptions

type ListAllOptions struct {
	// MaxRows bounds total decoded rows before ListAll/ListAllFunc fail
	// closed. 0 uses the default production cap.
	MaxRows int
}

type MultipartFile

type MultipartFile struct {
	FieldName   string
	Filename    string
	ContentType string
	Data        []byte
}

MultipartFile is a single file part for multipart/form-data requests. Data is already decoded by the caller; callers must not pass filesystem paths because MCP clients should not grant arbitrary server file access.

type PaginationCapError

type PaginationCapError struct {
	Path        string
	RowsScanned int
	MaxRows     int
	Page        int
	PageSize    int
}

PaginationCapError reports that a paginated scan exceeded its row cap.

func (*PaginationCapError) Error

func (e *PaginationCapError) Error() string

type Project

type Project struct {
	ID             string              `json:"id"`
	Name           string              `json:"name"`
	ClientID       string              `json:"clientId,omitempty"`
	ClientName     string              `json:"clientName,omitempty"`
	Client         any                 `json:"client,omitempty"`
	Color          string              `json:"color,omitempty"`
	Archived       bool                `json:"archived"`
	Billable       bool                `json:"billable,omitempty"`
	BudgetEstimate any                 `json:"budgetEstimate,omitempty"`
	CostRate       *Rate               `json:"costRate,omitempty"`
	Currency       any                 `json:"currency,omitempty"`
	CustomFields   any                 `json:"customFields,omitempty"`
	Duration       string              `json:"duration,omitempty"`
	Estimate       any                 `json:"estimate,omitempty"`
	EstimateReset  any                 `json:"estimateReset,omitempty"`
	Expenses       any                 `json:"expenses,omitempty"`
	Favorite       bool                `json:"favorite,omitempty"`
	HourlyRate     *Rate               `json:"hourlyRate,omitempty"`
	Memberships    []ProjectMembership `json:"memberships,omitempty"`
	Note           string              `json:"note,omitempty"`
	Public         bool                `json:"public,omitempty"`
	Tasks          []Task              `json:"tasks,omitempty"`
	Template       bool                `json:"template,omitempty"`
	TimeEstimate   any                 `json:"timeEstimate,omitempty"`
	WorkspaceID    string              `json:"workspaceId,omitempty"`
}

type ProjectMembership

type ProjectMembership struct {
	UserID           string `json:"userId"`
	TargetID         string `json:"targetId,omitempty"`
	MembershipType   string `json:"membershipType,omitempty"`
	MembershipStatus string `json:"membershipStatus,omitempty"`
	HourlyRate       *Rate  `json:"hourlyRate,omitempty"`
	CostRate         *Rate  `json:"costRate,omitempty"`
}

ProjectMembership models the `memberships[]` element returned on projects, tasks, workspaces, and users. Per-member rates here override their parent scope; nil means "fall through to parent."

type Rate

type Rate struct {
	Amount    int64  `json:"amount"`
	Currency  string `json:"currency,omitempty"`
	Inherited bool   `json:"inherited,omitempty"`
}

Rate is Clockify's RateDtoV1 / HourlyRateDtoV1. The Amount is always in the minor unit of the currency (cents for EUR/USD/GBP, etc.), as an integer. e.g. {amount: 4321, currency: "EUR"} represents €43.21.

Clockify can also serialise a rate as the literal string "##default", meaning "inherit from the parent scope (workspace/project/task)." We preserve that as Inherited=true with Amount=0 so callers can render it as "(inherited)" rather than dropping it.

func (*Rate) DecimalString

func (r *Rate) DecimalString() string

DecimalString renders Amount/100 with exactly two decimal places. Negative amounts are preserved verbatim. e.g. 4321 -> "43.21", 100 -> "1.00", -250 -> "-2.50".

func (*Rate) Display

func (r *Rate) Display() string

Display returns the rate in "<symbol><decimal> <CCY>" form for the few currencies we know symbols for, falling back to "<decimal> <CCY>". Examples: 4321 EUR -> "€43.21 EUR"; 4321 USD -> "$43.21 USD"; 4321 XYZ -> "43.21 XYZ".

func (*Rate) MarshalJSON

func (r *Rate) MarshalJSON() ([]byte, error)

func (*Rate) UnmarshalJSON

func (r *Rate) UnmarshalJSON(b []byte) error

type RawResponse

type RawResponse struct {
	Header http.Header
	Body   []byte
}

RawResponse is the binary-aware envelope returned by GetReportsRaw — the raw response bytes plus enough header context for callers to recover Content-Type and Content-Disposition (filename). Used by the shared-reports export endpoint where the upstream returns binary (PDF/XLSX) or non-JSON text (CSV).

type Tag

type Tag struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Archived    bool   `json:"archived"`
	WorkspaceID string `json:"workspaceId,omitempty"`
}

type Task

type Task struct {
	ID             string   `json:"id"`
	Name           string   `json:"name"`
	ProjectID      string   `json:"projectId"`
	AssigneeID     string   `json:"assigneeId,omitempty"`
	AssigneeIDs    []string `json:"assigneeIds,omitempty"`
	Billable       bool     `json:"billable"`
	BudgetEstimate int64    `json:"budgetEstimate,omitempty"`
	CostRate       *Rate    `json:"costRate,omitempty"`
	Duration       string   `json:"duration,omitempty"`
	Estimate       string   `json:"estimate,omitempty"`
	HourlyRate     *Rate    `json:"hourlyRate,omitempty"`
	Status         string   `json:"status,omitempty"`
	UserGroupIDs   []string `json:"userGroupIds,omitempty"`
}

type TimeEntry

type TimeEntry struct {
	ID                string       `json:"id"`
	Description       string       `json:"description"`
	ProjectID         string       `json:"projectId"`
	ProjectName       string       `json:"projectName,omitempty"`
	TaskID            string       `json:"taskId,omitempty"`
	TagIDs            []string     `json:"tagIds,omitempty"`
	Billable          bool         `json:"billable,omitempty"`
	BillablePresent   bool         `json:"-"`
	CostRate          *Rate        `json:"costRate,omitempty"`
	CustomFieldValues any          `json:"customFieldValues,omitempty"`
	HourlyRate        *Rate        `json:"hourlyRate,omitempty"`
	IsLocked          bool         `json:"isLocked,omitempty"`
	KioskID           string       `json:"kioskId,omitempty"`
	Type              string       `json:"type,omitempty"`
	UserID            string       `json:"userId,omitempty"`
	WorkspaceID       string       `json:"workspaceId,omitempty"`
	TimeInterval      TimeInterval `json:"timeInterval"`
}

func (TimeEntry) DurationSeconds

func (e TimeEntry) DurationSeconds() int64

func (TimeEntry) EndTime

func (e TimeEntry) EndTime() (time.Time, error)

func (TimeEntry) IsRunning

func (e TimeEntry) IsRunning() bool

func (TimeEntry) StartTime

func (e TimeEntry) StartTime() (time.Time, error)

func (*TimeEntry) UnmarshalJSON

func (e *TimeEntry) UnmarshalJSON(data []byte) error

type TimeInterval

type TimeInterval struct {
	Start    string `json:"start"`
	End      string `json:"end,omitempty"`
	Duration string `json:"duration,omitempty"`
}

type User

type User struct {
	ID               string `json:"id"`
	Name             string `json:"name"`
	Email            string `json:"email"`
	ActiveWorkspace  string `json:"activeWorkspace,omitempty"`
	CustomFields     any    `json:"customFields,omitempty"`
	DefaultWorkspace string `json:"defaultWorkspace,omitempty"`
	Memberships      any    `json:"memberships,omitempty"`
	ProfilePicture   string `json:"profilePicture,omitempty"`
	Settings         any    `json:"settings,omitempty"`
	Status           string `json:"status,omitempty"`
}

type Workspace

type Workspace struct {
	ID                      string              `json:"id"`
	Name                    string              `json:"name"`
	CakeOrganizationID      string              `json:"cakeOrganizationId,omitempty"`
	CostRate                *Rate               `json:"costRate,omitempty"`
	Currencies              any                 `json:"currencies,omitempty"`
	FeatureSubscriptionType string              `json:"featureSubscriptionType,omitempty"`
	Features                []string            `json:"features,omitempty"`
	HourlyRate              *Rate               `json:"hourlyRate,omitempty"`
	ImageURL                string              `json:"imageUrl,omitempty"`
	Memberships             []ProjectMembership `json:"memberships,omitempty"`
	Subdomain               any                 `json:"subdomain,omitempty"`
	WorkspaceSettings       any                 `json:"workspaceSettings,omitempty"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL