// Package froog is a client for the froog API (https://froogcorp.com/docs/).
//
// Generated from https://froogcorp.com/openapi.json by deploy/build-sdks.py; don't edit.
// API version 0.12.0. Go 1.21+, standard library only.
//
//	c := froog.New("")                        // FROOG_API_KEY from the environment
//	sb, err := c.Sandboxes.Create(ctx, froog.CreateSandbox{Size: froog.Ptr(froog.SizeSmall)})
//	if err != nil { ... }
//	defer sb.Delete(context.Background())
//	r, err := sb.Exec(ctx, "uname -a", nil)
//	fmt.Print(r.Stdout)
//
// One method per operation on Client (c.Exec, c.CreateSandboxes, c.ReadCommandLogs, ...), plus WaitUntil,
// ExecStream and the SandboxHandle object layer.
package froog

import (
	"bufio"
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
)

type Account struct {
	AllowXlarge bool `json:"allow_xlarge"`
	// Closed (`POST /account/close`): unix seconds. Reopen before `erase_at`.
	ClosedAt      *int64         `json:"closed_at,omitempty"`
	DataRetention *DataRetention `json:"data_retention,omitempty"`
	// The closing's deletion operation (`dop_…`, `GET /deletion-operations/{id}`).
	DeletionOperation *string `json:"deletion_operation,omitempty"`
	Email             string  `json:"email"`
	// When a closed account's data is erased for good (closed_at + 30 days).
	EraseAt      *int64 `json:"erase_at,omitempty"`
	ID           string `json:"id"`
	MaxSandboxes uint32 `json:"max_sandboxes"`
	// The organization this request acted in (`X-Froog-Org`, or the active one); absent in your personal workspace. `id` and `email` stay yours.
	Org *OrgSummary `json:"org,omitempty"`
}

// `POST /v1/account/close` and `POST /v1/account/reopen`.
type AccountClosing struct {
	Account   Account           `json:"account"`
	Operation DeletionOperation `json:"operation"`
}

// Add an account to a profile (`POST /v1/ai/profiles/{profile}/credentials`). A provider that already has one gets another, used when the first hits a rate or usage limit.
type AddAiCredential struct {
	// Custom APIs: an https:// URL with a public DNS name. Bedrock: the AWS region (default us-east-1) goes in `region`.
	BaseURL *string `json:"base_url,omitempty"`
	// Custom APIs: the variable name (A-Z, 0-9, _; not a provider's).
	Name *string `json:"name,omitempty"`
	// anthropic, openai, gemini, deepseek, mistral, openrouter, xai, bedrock or custom.
	Provider string `json:"provider"`
	// Bedrock: the AWS region, e.g. us-west-2.
	Region *string `json:"region,omitempty"`
	// Custom APIs: the header the proxy puts the secret in, and only there (default `Authorization` with `Bearer `). The placeholder anywhere else in the request is removed.
	SecretHeader *string `json:"secret_header,omitempty"`
	// Custom APIs: what comes before the secret in the header, e.g. `Bearer ` or `Token ` (default `Bearer ` when neither `secret_header` nor `secret_query` is given, else none).
	SecretPrefix *string `json:"secret_prefix,omitempty"`
	// Custom APIs: put the secret in this query parameter instead of a header (e.g. `api_key`).
	SecretQuery *string `json:"secret_query,omitempty"`
	// The key: one token of up to 8192 printable characters. Bedrock: a Bedrock API key, or `ACCESS_KEY_ID:SECRET_ACCESS_KEY[:SESSION_TOKEN]`.
	Value string `json:"value"`
}

type AddSshKey struct {
	Name      string `json:"name"`
	PublicKey string `json:"public_key"`
}

// One event of the agent's work.
//
// `type`: `status` (data: status, error), `text` (content, partial), `tool_call` (name, input, id), `tool_result` (output, is_error, id), `steer` (message, native, mode), `log` (a line the harness printed that isn't an event), `error` (message).
type AgentEvent struct {
	// Unix milliseconds.
	At             int64  `json:"at"`
	ConversationID string `json:"conversation_id"`
	Data           any    `json:"data"`
	// Increasing; pass the last one back as `cursor`.
	ID       int64  `json:"id"`
	PromptID string `json:"prompt_id"`
	Type     string `json:"type"`
}

// One account of a provider in a profile. Never the secret.
type AiCredential struct {
	// The account the proxy uses first for this provider (the first one not resting).
	Active bool `json:"active"`
	// Custom APIs: where the proxy sends the requests.
	BaseURL   *string `json:"base_url,omitempty"`
	CreatedAt int64   `json:"created_at"`
	// `cred_…`
	ID string `json:"id"`
	// The last characters of the key, e.g. `…7f3a`.
	Label         string  `json:"label"`
	LimitedReason *string `json:"limited_reason,omitempty"`
	// Hit a rate or usage limit: the proxy skips it until then (unix seconds).
	LimitedUntil *int64 `json:"limited_until,omitempty"`
	// Custom APIs: the variable name the sandbox gets (e.g. `STRIPE_KEY`).
	Name *string `json:"name,omitempty"`
	// anthropic, openai, gemini, deepseek, mistral, openrouter, xai, bedrock or custom.
	Provider string `json:"provider"`
	// Custom APIs: the header the proxy puts the secret in (e.g. `Authorization`), after `secret_prefix`. Absent when it goes in the query string (`secret_query`).
	SecretHeader *string `json:"secret_header,omitempty"`
	// Custom APIs: what comes before the secret in `secret_header` (e.g. `Bearer `).
	SecretPrefix *string `json:"secret_prefix,omitempty"`
	// Custom APIs: the query parameter the proxy puts the secret in, instead of a header.
	SecretQuery *string `json:"secret_query,omitempty"`
}

// A credential profile: a named set of AI credentials.
type AiProfile struct {
	CreatedAt int64 `json:"created_at"`
	// In switching order per provider.
	Credentials []AiCredential `json:"credentials"`
	// Sandboxes get the default profile unless their environment or the sandbox names another.
	Default bool `json:"default"`
	// Environments whose sandboxes get this profile.
	Environments []string `json:"environments"`
	// `aip_…`
	ID   string `json:"id"`
	Name string `json:"name"`
}

// The account's AI proxy settings (`GET /v1/ai/proxy`).
type AiProxy struct {
	// Every proxied sandbox of the account is refused (403 `ai_blocked`).
	Blocked bool `json:"blocked"`
	// On: sandboxes started from now on get placeholder keys and base URLs pointing at the proxy; the real keys stay on froog. Default off.
	Enabled bool `json:"enabled"`
	// The base URL sandboxes send their AI requests to.
	URL string `json:"url"`
	// With the proxy on, keep subscription sign-ins (Claude, ChatGPT) out of sandboxes too. They are never proxied. Default false: they pass as without the proxy.
	WithholdSubscriptions bool `json:"withhold_subscriptions"`
}

// Usage of one sandbox in the account view.
type AiSandboxUsage struct {
	Blocked bool `json:"blocked"`
	// Its name (or its id once deleted).
	Sandbox   string   `json:"sandbox"`
	SandboxID string   `json:"sandbox_id"`
	Totals    AiTotals `json:"totals"`
}

// Tokens and requests over a period.
type AiTotals struct {
	CacheReadTokens  uint64 `json:"cache_read_tokens"`
	CacheWriteTokens uint64 `json:"cache_write_tokens"`
	// Estimated provider cost in USD, from list prices built into froog. Not a froog charge.
	CostUSD float64 `json:"cost_usd"`
	// Answers with status 400 or above (including the proxy's own refusals upstream).
	Errors uint64 `json:"errors"`
	// Input tokens not read from a cache.
	InputTokens  uint64 `json:"input_tokens"`
	OutputTokens uint64 `json:"output_tokens"`
	Requests     uint64 `json:"requests"`
}

// The account's AI usage (`GET /v1/ai/usage`).
type AiUsage struct {
	Days uint32 `json:"days"`
	// Most expensive first.
	Sandboxes []AiSandboxUsage `json:"sandboxes"`
	Totals    AiTotals         `json:"totals"`
}

// One row: a UTC day, provider and model.
type AiUsageRow struct {
	CacheReadTokens  uint64 `json:"cache_read_tokens"`
	CacheWriteTokens uint64 `json:"cache_write_tokens"`
	// Estimated provider cost in USD, from list prices built into froog. Not a froog charge.
	CostUSD float64 `json:"cost_usd"`
	// YYYY-MM-DD (UTC).
	Day string `json:"day"`
	// Answers with status 400 or above (including the proxy's own refusals upstream).
	Errors uint64 `json:"errors"`
	// Input tokens not read from a cache.
	InputTokens  uint64 `json:"input_tokens"`
	Model        string `json:"model"`
	OutputTokens uint64 `json:"output_tokens"`
	Provider     string `json:"provider"`
	Requests     uint64 `json:"requests"`
}

// `GET /v1/account/alerts`: which emails the account gets, and whether an errored sandbox is rebuilt from its last snapshot by itself.
type AlertSettings struct {
	// Rebuild a sandbox whose VM was lost (state `error`) from its last snapshot, and email (default on).
	AutoRebuild bool         `json:"auto_rebuild"`
	Emails      []EmailAlert `json:"emails"`
}

// An API key as listed (the key itself is shown only once, at creation).
type ApiKeyInfo struct {
	// What the key may do (`scopes::ACTIONS`); `["*"]` = everything.
	Actions   []string `json:"actions,omitempty"`
	CreatedAt int64    `json:"created_at"`
	// An organization key: the email of the member who made it (it acts as that member).
	CreatedBy *string `json:"created_by,omitempty"`
	// Limited to sandboxes of these environments (names); None = any environment.
	Environments []string `json:"environments,omitempty"`
	// None = never expires.
	ExpiresAt *int64 `json:"expires_at,omitempty"`
	ID        string `json:"id"`
	Label     string `json:"label"`
	// Limited to sandboxes that have all of these labels; None = any labels.
	Labels     map[string]string `json:"labels,omitempty"`
	LastUsedAt *int64            `json:"last_used_at,omitempty"`
	// The preset its actions match, if any (read-only, full-sandbox, ci, admin).
	Preset *string `json:"preset,omitempty"`
	// Requests made with it in the last 30 days.
	Requests30d uint64 `json:"requests_30d,omitempty"`
	// The sandboxes it may touch (names), besides those it created; None = all.
	Sandboxes []string `json:"sandboxes,omitempty"`
	// Sandboxes this key created that still exist.
	SandboxesCreated uint32 `json:"sandboxes_created,omitempty"`
}

// One entry of the account's audit log (`GET /audit`): who changed what, from where. Kept 90 days.
type AuditEvent struct {
	// What happened, e.g. sandbox.create, api_key.revoke, webhook.update, billing.plan.
	Action string `json:"action"`
	// Who did it: an API key id (`key_…`), `session` (the dashboard), or `public-url` (an auto-start by a visitor of a public URL).
	Actor string `json:"actor"`
	// The API key's name, or the account email for a dashboard session.
	ActorLabel *string `json:"actor_label,omitempty"`
	// Unix seconds.
	At int64 `json:"at"`
	// Short extra facts, e.g. the sandboxes a create or fork made.
	Detail *string `json:"detail,omitempty"`
	// `aud_…`
	ID string `json:"id"`
	// The client's IP address.
	IP *string `json:"ip,omitempty"`
	// The request's `X-Request-Id`.
	RequestID *string `json:"request_id,omitempty"`
	// What it happened to: a sandbox, key, webhook or variable name or id (never a secret).
	Target *string `json:"target,omitempty"`
}

type AuditList struct {
	// Newest first.
	Events []AuditEvent `json:"events"`
	// Pass as `cursor` for the next (older) page; None at the end.
	NextCursor *string `json:"next_cursor,omitempty"`
}

// `PUT /v1/billing/wallet/auto-topup`: charge this much whenever the plan's usage plus the wallet drops below 10% of it (at least $5). 0 turns it off.
type AutoTopup struct {
	AmountUSD uint32 `json:"amount_usd"`
}

// Payment status of an account (`GET /v1/billing`).
type Billing struct {
	// Declined auto top-ups in a row (3 turn auto top-up off).
	AutoTopupFailures uint32 `json:"auto_topup_failures,omitempty"`
	// Auto top-up amount in dollars; 0 = off.
	AutoTopupUSD uint32 `json:"auto_topup_usd,omitempty"`
	// A payment method is on file.
	CardOnFile bool `json:"card_on_file"`
	// Payments are set up on this froog server.
	Enabled bool `json:"enabled"`
	// The account isn't billed (e.g. the operator's own account).
	Exempt bool `json:"exempt"`
	// Paying from the wallet for starts past the start limits.
	ExtraStarts *ExtraStarts `json:"extra_starts,omitempty"`
	// Out of money: running sandboxes are stopped at this time (unix seconds) unless money is added.
	GraceUntil *int64 `json:"grace_until,omitempty"`
	// This period: what the plan covers, what was used, and what's left (plan plus wallet).
	IncludedUSD float64 `json:"included_usd,omitempty"`
	// The plan was canceled with `keep_disks`: once it ends, every sandbox stops, and stopped disks and saved snapshots are kept free until this time (unix seconds).
	KeepDisksUntil *int64 `json:"keep_disks_until,omitempty"`
	// Most the card can be charged for the wallet in 24 hours (payments and auto top-ups).
	MaxWalletPerDayUSD uint32 `json:"max_wallet_per_day_usd,omitempty"`
	// Smallest wallet payment, dollars.
	MinTopupUSD uint32 `json:"min_topup_usd,omitempty"`
	// The account's plan; None until one is chosen.
	Plan *CurrentPlan `json:"plan,omitempty"`
	// The plans on offer, cheapest first.
	Plans []PlanInfo `json:"plans,omitempty"`
	// Per-second rates, quoted per hour and per 730-hour month.
	Rates        []Rate  `json:"rates,omitempty"`
	RemainingUSD float64 `json:"remaining_usd,omitempty"`
	// Out of money: the plan's usage and the wallet ran out and the grace period ended, so sandboxes were stopped. Add money to start them again.
	Suspended bool    `json:"suspended"`
	UsedUSD   float64 `json:"used_usd,omitempty"`
	// The bonus added to wallet payments and auto top-ups by amount; None = no bonus.
	WalletBonus *WalletBonus `json:"wallet_bonus,omitempty"`
	// The wallet: money paid in minus usage past the plan. Negative = owed.
	WalletUSD float64 `json:"wallet_usd,omitempty"`
}

// A Stripe-hosted page to open in a browser.
type BillingUrl struct {
	URL string `json:"url"`
}

// `POST /sandboxes/{name}/browser`: open the browser view (only Chrome).
type BrowserRequest struct {
	// Chrome's user data directory on the sandbox: an absolute path outside the system directories, created if missing. Default: `~/.config/froog-browser`.
	Profile *string `json:"profile,omitempty"`
}

// `POST /v1/billing/plan`: choose or change the plan.
type ChoosePlan struct {
	// Dollars a month: one of `Billing.plans`.
	Plan uint32 `json:"plan"`
}

// `GET /v1/ci/analytics?days=&repo=`: what the account's jobs add up to.
type CiAnalytics struct {
	Days uint32 `json:"days"`
	// Tests that both failed and passed on the same commit.
	FlakyTests []CiFlakyTest `json:"flaky_tests"`
	// One row per repository, workflow and job name; most job time first.
	Jobs []CiJobStats `json:"jobs"`
	// Most job time first.
	Repos []CiRepoStats `json:"repos"`
	Since int64         `json:"since"`
}

type CiCache struct {
	// The name in `/cache=<name>`.
	Name string `json:"name"`
	// When the last default-branch job saved it.
	SavedAt int64 `json:"saved_at"`
	// What it adds to the base image, in GB.
	SizeGB *float64 `json:"size_gb,omitempty"`
	// Its saved snapshot (`cache-<name>`): `DELETE /v1/saved-snapshots/{snapshot}` starts it over.
	Snapshot string `json:"snapshot"`
}

type CiFacet struct {
	Name string `json:"name"`
	Runs uint64 `json:"runs"`
}

type CiFlakyTest struct {
	// Commits it both failed and passed on.
	Commits      uint32  `json:"commits"`
	Failed       uint32  `json:"failed"`
	LastFailedAt int64   `json:"last_failed_at"`
	Message      *string `json:"message,omitempty"`
	Name         string  `json:"name"`
	Passed       uint32  `json:"passed"`
	Repo         string  `json:"repo"`
	Suite        *string `json:"suite,omitempty"`
}

// One GitHub Actions job (`GET /v1/ci`, `GET /v1/ci/jobs/{id}`).
type CiJob struct {
	// With `/cache=`: whether its VM started from the saved cache (false: a cold start).
	CacheHit *bool `json:"cache_hit,omitempty"`
	// GitHub's conclusion once completed: success | failure | cancelled | …
	Conclusion *string `json:"conclusion,omitempty"`
	// What its job time costs at the CI price (so far, while it runs).
	CostUSD    *float64 `json:"cost_usd,omitempty"`
	FinishedAt *int64   `json:"finished_at,omitempty"`
	// GitHub's job id.
	GithubJobID int64 `json:"github_job_id"`
	// The branch the run is for (GitHub's `head_branch`).
	HeadBranch *string `json:"head_branch,omitempty"`
	// The commit the run is for.
	HeadSha *string `json:"head_sha,omitempty"`
	HtmlURL *string `json:"html_url,omitempty"`
	// `ci_…`
	ID      string `json:"id"`
	JobName string `json:"job_name"`
	// The job's `runs-on` labels.
	Labels   []string `json:"labels"`
	QueuedAt int64    `json:"queued_at"`
	// Why it waits, or why it didn't run.
	Reason *string `json:"reason,omitempty"`
	// owner/name
	Repo string `json:"repo"`
	// CPU and memory the job used, once it completed (from the VM's 30-second samples).
	Resources *CiResources `json:"resources,omitempty"`
	RunID     int64        `json:"run_id"`
	// Seconds the job ran (its billed job time).
	RunSecs *int64 `json:"run_secs,omitempty"`
	// The sandbox that runs it (`ci-<repo>-<job id>`).
	Sandbox   *string `json:"sandbox,omitempty"`
	Size      Size    `json:"size"`
	StartedAt *int64  `json:"started_at,omitempty"`
	// queued (waiting for a sandbox) | starting | running | completed | refused | failed
	State string `json:"state"`
	// Test results from the JUnit XML files the job wrote, once it completed.
	Tests *CiTestSummary `json:"tests,omitempty"`
	// Seconds from queued to the sandbox running.
	WaitSecs *int64  `json:"wait_secs,omitempty"`
	Workflow *string `json:"workflow,omitempty"`
}

// `GET /v1/ci/jobs/{id}/metrics`: the job's CPU and memory over time, and its steps.
type CiJobMetrics struct {
	// Averaged into at most 120 buckets, oldest first.
	Points    []MetricsPoint `json:"points"`
	Resources *CiResources   `json:"resources,omitempty"`
	// The job's steps, from GitHub, with their times.
	Steps []CiStep `json:"steps"`
}

type CiJobStats struct {
	// A better size for it, when the runs say so.
	Advice *CiSizeAdvice `json:"advice,omitempty"`
	// With `/cache=`: the share of runs that started from the cache (0-1).
	CacheHitRate *float64 `json:"cache_hit_rate,omitempty"`
	Cancelled    uint32   `json:"cancelled"`
	CostUSD      float64  `json:"cost_usd"`
	// Average of the runs' CPU averages.
	CPUAvgPercent *float64 `json:"cpu_avg_percent,omitempty"`
	Failed        uint32   `json:"failed"`
	FailureRate   float64  `json:"failure_rate"`
	JobName       string   `json:"job_name"`
	// Highest memory any run used, as a share of its VM's.
	MemoryPeakPercent *float64 `json:"memory_peak_percent,omitempty"`
	Minutes           float64  `json:"minutes"`
	// Runs in which the kernel killed a process for want of memory.
	OomRuns uint32 `json:"oom_runs"`
	P50Secs *int64 `json:"p50_secs,omitempty"`
	P90Secs *int64 `json:"p90_secs,omitempty"`
	Repo    string `json:"repo"`
	Runs    uint32 `json:"runs"`
	// The size of its latest run.
	Size Size `json:"size"`
	// Its slowest steps (by p50 over the runs), at most 5.
	SlowSteps []CiStepStats `json:"slow_steps,omitempty"`
	Succeeded uint32        `json:"succeeded"`
	// Queued to running.
	WaitP50Secs *int64  `json:"wait_p50_secs,omitempty"`
	WaitP90Secs *int64  `json:"wait_p90_secs,omitempty"`
	Workflow    *string `json:"workflow,omitempty"`
}

// `GET /v1/ci/jobs/{id}/tests`.
type CiJobTests struct {
	// None: the job wrote no JUnit XML froog found.
	Summary *CiTestSummary `json:"summary,omitempty"`
	// Failed first, then skipped, then passed; at most 1,000.
	Tests []CiTestCase `json:"tests"`
}

type CiLogMatch struct {
	// The line's time, from GitHub's timestamp.
	At         *string `json:"at,omitempty"`
	Branch     *string `json:"branch,omitempty"`
	Conclusion *string `json:"conclusion,omitempty"`
	JobID      string  `json:"job_id"`
	JobName    string  `json:"job_name"`
	// 1-based line number in the job's log.
	Line     uint32 `json:"line"`
	QueuedAt int64  `json:"queued_at"`
	Repo     string `json:"repo"`
	// The step the line is in (from the log's `##[group]` markers).
	Step *string `json:"step,omitempty"`
	// The line without its timestamp (first 500 characters).
	Text     string  `json:"text"`
	Workflow *string `json:"workflow,omitempty"`
}

// `GET /v1/ci/logs/search?q=&days=&limit=&order=`: lines of the account's kept CI job logs.
type CiLogSearch struct {
	// Jobs whose logs were searched.
	JobsSearched uint32 `json:"jobs_searched"`
	// Newest job first (`order=oldest`: oldest first, for where a failure first appeared).
	Matches []CiLogMatch `json:"matches"`
	Query   string       `json:"query"`
	// More lines matched than `limit`.
	Truncated bool `json:"truncated"`
}

// A CI monitor (`GET/POST /v1/ci/monitors`): an alert when matching jobs fail or run long.
type CiMonitor struct {
	// Only runs for this branch.
	Branch *string `json:"branch,omitempty"`
	// failure: alert after this many failures in a row (a success starts over).
	Consecutive uint32 `json:"consecutive"`
	// No second alert from this monitor for this long.
	CooldownMins uint32 `json:"cooldown_mins"`
	CreatedAt    int64  `json:"created_at"`
	// duration: alert when a job has run this long.
	DurationSecs *uint32 `json:"duration_secs,omitempty"`
	// Email the account (webhooks listing `ci.monitor.triggered` get it either way).
	Email bool `json:"email"`
	// `cim_…`
	ID string `json:"id"`
	// Only jobs whose name contains this.
	Job *string `json:"job,omitempty"`
	// failure (a job failed or timed out) | duration (a job ran longer than `duration_secs`)
	Kind            string `json:"kind"`
	LastTriggeredAt *int64 `json:"last_triggered_at,omitempty"`
	// failure: only failures whose log contains this (any case), e.g. `OutOfMemory` or `SIGKILL`.
	LogPattern *string `json:"log_pattern,omitempty"`
	Name       string  `json:"name"`
	// Its last alerts, newest first (at most 10).
	Recent []CiMonitorEvent `json:"recent,omitempty"`
	// Only this repository (owner/name).
	Repo *string `json:"repo,omitempty"`
	// Failures in a row so far.
	Streak uint32 `json:"streak"`
	// Only workflows whose name contains this.
	Workflow *string `json:"workflow,omitempty"`
}

type CiMonitorEvent struct {
	At     int64  `json:"at"`
	JobID  string `json:"job_id"`
	Reason string `json:"reason"`
}

// `GET /v1/ci`.
type CiOverview struct {
	// The account's caches (`/cache=<name>`: saved snapshots named `cache-<name>`).
	Caches []CiCache `json:"caches,omitempty"`
	// This server takes CI jobs (a GitHub App with a webhook secret is set up).
	Configured bool `json:"configured"`
	// The account has a linked installation.
	Connected bool `json:"connected"`
	// Newest first.
	Jobs []CiJob `json:"jobs"`
	// `runs-on` label: `<label>-small`, `<label>`, `<label>-large`, `<label>-xlarge` (or `<label>-2vcpu` ... `-16vcpu`), plus `/from=<saved snapshot>`, `/cache=<name>` and `/debug`.
	Label string `json:"label"`
	// CI job time in the current billing period.
	Period *CiPeriod `json:"period,omitempty"`
	// The CI price: dollars per vCPU per minute of job time.
	PricePerVcpuMinute float64 `json:"price_per_vcpu_minute,omitempty"`
	// Jobs from public repositories run (off by default).
	PublicRepos bool `json:"public_repos"`
	// When public repositories are on: the GitHub users whose runs may use froog (empty = anyone).
	PublicUsers []string `json:"public_users"`
	// Last 7 days.
	Stats CiStats `json:"stats"`
}

type CiPeriod struct {
	CostUSD float64 `json:"cost_usd"`
	// Start of the billing period.
	From    int64   `json:"from"`
	Minutes float64 `json:"minutes"`
}

type CiRepoStats struct {
	CostUSD float64 `json:"cost_usd"`
	Failed  uint32  `json:"failed"`
	// failed / finished runs (0-1).
	FailureRate float64 `json:"failure_rate"`
	Minutes     float64 `json:"minutes"`
	Repo        string  `json:"repo"`
	Runs        uint32  `json:"runs"`
}

// What one job used of its VM.
type CiResources struct {
	// Average share of all vCPUs busy while the job ran.
	CPUAvgPercent *float64 `json:"cpu_avg_percent,omitempty"`
	// Busiest 30 seconds.
	CPUPeakPercent *float64 `json:"cpu_peak_percent,omitempty"`
	MemoryPeakMb   *uint64  `json:"memory_peak_mb,omitempty"`
	MemoryTotalMb  *uint64  `json:"memory_total_mb,omitempty"`
	// Processes the kernel killed for want of memory during the job.
	OomKills uint32 `json:"oom_kills,omitempty"`
}

// One GitHub Actions workflow run: the jobs of one run that ran on froog (`GET /v1/ci/runs`, `GET /v1/ci/runs/{run_id}`).
type CiRun struct {
	// The GitHub user who started it.
	Actor *string `json:"actor,omitempty"`
	// What its jobs' time costs.
	CostUSD float64 `json:"cost_usd,omitempty"`
	// First job started to last job finished (so far, while it runs).
	DurationSecs *int64 `json:"duration_secs,omitempty"`
	// What started it: push, pull_request, schedule, workflow_dispatch, ...
	Event      *string `json:"event,omitempty"`
	HeadBranch *string `json:"head_branch,omitempty"`
	HeadSha    *string `json:"head_sha,omitempty"`
	// The run on GitHub.
	HtmlURL *string `json:"html_url,omitempty"`
	// Its jobs on froog, oldest first.
	Jobs []CiJob `json:"jobs"`
	// The workflow file, e.g. `.github/workflows/ci.yml`.
	Path *string `json:"path,omitempty"`
	// When its first job was queued.
	QueuedAt int64 `json:"queued_at"`
	// owner/name
	Repo string `json:"repo"`
	// GitHub's run id.
	RunID int64 `json:"run_id"`
	// The run's number in its workflow (`#250`).
	RunNumber *int64 `json:"run_number,omitempty"`
	// GitHub's title for the run: the head commit's message, or the pull request's title.
	Title    *string `json:"title,omitempty"`
	Workflow *string `json:"workflow,omitempty"`
}

// `GET /v1/ci/runs`: one page of runs, newest first, and what the filters can pick from.
type CiRunList struct {
	Branches []string `json:"branches"`
	Repos    []string `json:"repos"`
	Runs     []CiRun  `json:"runs"`
	// Runs that match the filters, on every page.
	Total uint64 `json:"total"`
	// The account's workflows (every run, not only the filtered ones), with their run counts.
	Workflows []CiFacet `json:"workflows"`
}

// `PUT /v1/ci/settings`.
type CiSettings struct {
	PublicRepos bool `json:"public_repos"`
	// GitHub logins; empty = anyone (only with public_repos).
	PublicUsers []string `json:"public_users,omitempty"`
}

type CiSizeAdvice struct {
	// cost | speed | memory
	Kind string `json:"kind"`
	// The `runs-on` label for it.
	Label string `json:"label"`
	Size  Size   `json:"size"`
	Why   string `json:"why"`
}

type CiStats struct {
	Failed    uint32 `json:"failed"`
	Jobs      uint32 `json:"jobs"`
	Refused   uint32 `json:"refused"`
	RunSecs   int64  `json:"run_secs"`
	Succeeded uint32 `json:"succeeded"`
}

type CiStep struct {
	CompletedAt *int64  `json:"completed_at,omitempty"`
	Conclusion  *string `json:"conclusion,omitempty"`
	Name        string  `json:"name"`
	Number      int64   `json:"number"`
	Secs        *int64  `json:"secs,omitempty"`
	StartedAt   *int64  `json:"started_at,omitempty"`
}

type CiStepStats struct {
	Name    string `json:"name"`
	P50Secs int64  `json:"p50_secs"`
	P90Secs int64  `json:"p90_secs"`
	Runs    uint32 `json:"runs"`
}

type CiTestCase struct {
	// The failure's message (first 500 characters).
	Message *string  `json:"message,omitempty"`
	Name    string   `json:"name"`
	Secs    *float64 `json:"secs,omitempty"`
	// passed | failed | skipped
	Status string `json:"status"`
	// The JUnit `classname` (or the suite's name).
	Suite *string `json:"suite,omitempty"`
}

type CiTestSummary struct {
	Failed  uint32 `json:"failed"`
	Passed  uint32 `json:"passed"`
	Skipped uint32 `json:"skipped"`
	Total   uint32 `json:"total"`
}

// `GET /cli-login/{code}`: what's being approved.
type CliLoginInfo struct {
	Code      string `json:"code"`
	Device    string `json:"device"`
	ExpiresAt int64  `json:"expires_at"`
}

// `POST /cli-login/poll`
type CliLoginPoll struct {
	PollToken string `json:"poll_token"`
}

// `POST /cli-login` (no auth): start an `froog login` in the browser.
type CliLoginRequest struct {
	// Shown on the approval page, e.g. the computer's hostname.
	Device string `json:"device,omitempty"`
}

type CliLoginResult struct {
	Email *string `json:"email,omitempty"`
	Key   *string `json:"key,omitempty"`
	// "pending" or "approved"
	Status string `json:"status"`
}

type CliLoginStarted struct {
	// Show this; the dashboard asks the person to check it matches.
	Code      string `json:"code"`
	ExpiresIn int64  `json:"expires_in"`
	// Secret: poll with it. Never show it.
	PollToken string `json:"poll_token"`
	// Open this in the browser.
	URL string `json:"url"`
}

type CommandList struct {
	Commands []CommandRecord `json:"commands"`
	// Pass as `cursor` for the next (older) page; None at the end.
	NextCursor *string `json:"next_cursor,omitempty"`
}

// A chunk of a command's stored output from `offset`. Read again from `next_offset` until `done` (the command ended and everything was read).
type CommandLogs struct {
	// UTF-8 text, or base64 with `encoding=base64`.
	Data       string `json:"data"`
	Done       bool   `json:"done"`
	NextOffset uint64 `json:"next_offset"`
	Offset     uint64 `json:"offset"`
	// Bytes stored so far for this stream.
	Size   uint64 `json:"size"`
	Status string `json:"status"`
}

// One command in a sandbox's history (`GET /sandboxes/{name}/command-history`). Kept 90 days, readable while the sandbox is stopped. `mode`: sync | stream | detached. `status`: running | paused (frozen with a sandbox stopped with its memory kept; a start resumes it) | exited | timed_out | failed | killed | lost.
type CommandRecord struct {
	// The command text (first 64 KiB).
	Command  string `json:"command"`
	EndedAt  *int64 `json:"ended_at,omitempty"`
	ExitCode *int32 `json:"exit_code,omitempty"`
	ID       string `json:"id"`
	// The background job behind a stream/detached command (`froog logs`, `froog kill`).
	JobID *string `json:"job_id,omitempty"`
	Mode  string  `json:"mode"`
	// The kernel killed it for using too much memory (exit code 137).
	OomKilled bool `json:"oom_killed,omitempty"`
	// Output past the per-command or per-account cap was dropped.
	OutputTruncated bool   `json:"output_truncated"`
	StartedAt       int64  `json:"started_at"`
	Status          string `json:"status"`
	StderrSize      uint64 `json:"stderr_size"`
	StdoutSize      uint64 `json:"stdout_size"`
}

// One way to sign in to the account (`GET /v1/account/connections`).
type Connection struct {
	CreatedAt int64 `json:"created_at"`
	// The address this connection signs in with (GitHub: the account's verified primary address).
	Email *string `json:"email,omitempty"`
	// `con_…`
	ID string `json:"id"`
	// The GitHub login, for a GitHub connection.
	Login *string `json:"login,omitempty"`
	// Its address gets the account's emails (the account's email).
	Primary bool `json:"primary"`
	// google | github | email | agentid
	Provider string `json:"provider"`
}

// A conversation: a thread of prompts with the agent's memory (the harness's own session).
type Conversation struct {
	// The harness of its last prompt.
	Agent     string `json:"agent"`
	CreatedAt int64  `json:"created_at"`
	// The one a prompt without `conversation_id` or `new` continues (the most recent).
	Current bool   `json:"current"`
	ID      string `json:"id"`
	// The start of its last prompt.
	LastPrompt string  `json:"last_prompt"`
	Model      *string `json:"model,omitempty"`
	Prompts    uint32  `json:"prompts"`
	// Prompts waiting for their turn.
	Queued uint32 `json:"queued"`
	// A turn is running in it now.
	Running   bool   `json:"running"`
	Sandbox   string `json:"sandbox"`
	UpdatedAt int64  `json:"updated_at"`
}

// A conversation with its prompts, oldest first.
type ConversationDetail struct {
	// The harness of its last prompt.
	Agent     string `json:"agent"`
	CreatedAt int64  `json:"created_at"`
	// The one a prompt without `conversation_id` or `new` continues (the most recent).
	Current bool   `json:"current"`
	ID      string `json:"id"`
	// The start of its last prompt.
	LastPrompt string  `json:"last_prompt"`
	Model      *string `json:"model,omitempty"`
	Prompts    uint32  `json:"prompts"`
	// Prompts waiting for their turn.
	Queued uint32 `json:"queued"`
	// A turn is running in it now.
	Running   bool     `json:"running"`
	Sandbox   string   `json:"sandbox"`
	Turns     []Prompt `json:"turns"`
	UpdatedAt int64    `json:"updated_at"`
}

type ConversationList struct {
	Conversations []Conversation `json:"conversations"`
}

type CreateAiProfile struct {
	// Start with a copy of this profile's credentials.
	CopyFrom *string `json:"copy_from,omitempty"`
	// 1-40 of a-z, 0-9 and -.
	Name string `json:"name"`
}

// `POST /v1/ci/monitors`.
type CreateCiMonitor struct {
	Branch *string `json:"branch,omitempty"`
	// 1-20, default 1.
	Consecutive *uint32 `json:"consecutive,omitempty"`
	// 0-1440, default 60.
	CooldownMins *uint32 `json:"cooldown_mins,omitempty"`
	// Required for duration: 60 to 21600.
	DurationSecs *uint32 `json:"duration_secs,omitempty"`
	// Default true.
	Email *bool   `json:"email,omitempty"`
	Job   *string `json:"job,omitempty"`
	// failure (default) | duration
	Kind       *string `json:"kind,omitempty"`
	LogPattern *string `json:"log_pattern,omitempty"`
	Name       string  `json:"name"`
	Repo       *string `json:"repo,omitempty"`
	Workflow   *string `json:"workflow,omitempty"`
}

type CreateInbox struct {
	// Your label for it.
	Name *string `json:"name,omitempty"`
	// Attach it to this sandbox: it's `FROOG_EMAIL` there (from the sandbox's next start), and the key inside the sandbox can read it. The address starts with the sandbox's name unless `username` says otherwise.
	Sandbox *string `json:"sandbox,omitempty"`
	// The start of the address (letters, digits, dots, dashes; 1 to 32). froog adds a random suffix: `signup` becomes `signup-k3f9qz@froogmail.com`. Default: `agent`.
	Username *string `json:"username,omitempty"`
}

type CreateSandbox struct {
	// true: while stopped, a request to one of its public URLs, or an exec, file, job or wait call, starts it. Default false.
	AutoStart *bool `json:"auto_start,omitempty"`
	// Create `count` sandboxes named `name-1..name-N`.
	Count *uint32 `json:"count,omitempty"`
	// Delete it after it has stayed stopped this many seconds (0 or absent = never; at most 365 days). A start before then cancels the countdown.
	DeleteAfterSecs *uint64 `json:"delete_after_secs,omitempty"`
	// Its own environment variables (on top of the account's; they win, and apply even with `environment: false`). With `from`, the default is the saved snapshot's source's.
	Env map[string]string `json:"env,omitempty"`
	// false: start with none of the account's environment (no env vars, repos, files or setup script), e.g. for code or people you don't trust with your credentials. Default true, except from a share code (`from: "fsh_…"`) or the saved copy one gave: false there (another account prepared that disk); `true` opts in.
	Environment *bool `json:"environment,omitempty"`
	// The environment it uses (`GET /environments`), at its latest version. Default: the account's default environment. Kept with `environment: false` (it then gets none of it).
	EnvironmentName *string `json:"environment_name,omitempty"`
	// Delete it this many seconds after it's created, running or not (60 s to 365 days; absent = never). Shows as `expires_at`.
	ExpiresInSecs *uint64 `json:"expires_in_secs,omitempty"`
	// Files written into the home directory before the sandbox counts as running (first boot only; mode 600; at most 20, 1 MiB together).
	Files []NewFile `json:"files,omitempty"`
	// Start from this saved snapshot (its name): its disk, OS and, unless `size` is given, size. Or a share code (`fsh_…`) another account gave you: a copy of its saved snapshot becomes a saved snapshot of yours, and the owner's logins, keys and secret files are removed from the disk before the sandbox counts as running.
	From *string `json:"from,omitempty"`
	// Key-value tags (at most 32; keys 1-63 of `a-z A-Z 0-9 . _ - /`, values up to 255 characters).
	Labels map[string]string `json:"labels,omitempty"`
	// Stop it this many seconds after each start, busy or not, even while an agent prompt runs (0 or absent = no limit; at most 30 days). The auto-stop timer (`ttl_secs`) still applies.
	MaxLifetimeSecs *uint64 `json:"max_lifetime_secs,omitempty"`
	// false: keep no command history or process samples. Default: the account's setting.
	Monitoring *bool   `json:"monitoring,omitempty"`
	Name       *string `json:"name,omitempty"`
	// Operating system; default Ubuntu 24.04.
	OS *Os `json:"os,omitempty"`
	// A git repo cloned into this sandbox only, by the setup job at boot (also with `environment: false`).
	Repo *SandboxRepo `json:"repo,omitempty"`
	// A script run once, in the background, after the first boot (as job "setup").
	Setup *string `json:"setup,omitempty"`
	Size  *Size   `json:"size,omitempty"`
	// false: never snapshot it; a stop erases it. Default: the account's setting.
	Snapshots *bool `json:"snapshots,omitempty"`
	// Auto-stop this many seconds after each start, busy or not (at most 30 days; 0 = no auto-stop). Absent or null: 1 hour (2 hours during the free trial, which needs one).
	TTLSecs *uint64 `json:"ttl_secs,omitempty"`
	// In an organization: `shared` (every member can use it) or `private` (only you). Default: the organization's default visibility. A personal sandbox is always private.
	Visibility *string `json:"visibility,omitempty"`
}

type CreateSnapshot struct {
	Label *string `json:"label,omitempty"`
}

type CreatedApiKey struct {
	Actions      []string `json:"actions,omitempty"`
	Environments []string `json:"environments,omitempty"`
	ExpiresAt    *int64   `json:"expires_at,omitempty"`
	ID           string   `json:"id"`
	// Shown once; store it now.
	Key       string            `json:"key"`
	Label     string            `json:"label"`
	Labels    map[string]string `json:"labels,omitempty"`
	Sandboxes []string          `json:"sandboxes,omitempty"`
}

// `POST /saved-snapshots/{name}/shares`: the code, once.
type CreatedSnapshotShare struct {
	// `fsh_…`: another account runs `froog new --from fsh_…`. Shown once; froog keeps only a hash.
	Code      string `json:"code"`
	CreatedAt int64  `json:"created_at"`
	ID        string `json:"id"`
	Name      string `json:"name"`
}

// The account's plan, mirrored from its Stripe subscription.
type CurrentPlan struct {
	// Canceled: the plan ends with the period.
	CancelAtPeriodEnd bool   `json:"cancel_at_period_end"`
	MaxSandboxes      uint32 `json:"max_sandboxes"`
	PeriodEnd         int64  `json:"period_end"`
	// The current billing period (or the trial), unix seconds.
	PeriodStart    int64  `json:"period_start"`
	SavedSnapshots uint32 `json:"saved_snapshots"`
	// trialing | active | past_due | unpaid | canceled | incomplete
	Status      string `json:"status"`
	TrialEndsAt *int64 `json:"trial_ends_at,omitempty"`
	// Dollars a month.
	USD uint32 `json:"usd"`
}

// `POST /v1/dashboard-link`: open the dashboard signed in. The code in the URL works once and expires at `expires_at`; it can't call the API.
type DashboardLink struct {
	ExpiresAt int64  `json:"expires_at"`
	URL       string `json:"url"`
}

// `GET/PATCH /account/data-retention`. Changing it needs a dashboard session, not an API key.
type DataRetention struct {
	// Turning zero data retention on just now started erasing: this deletion operation (`GET /deletion-operations/{id}`) follows it. Only in the answer to that change.
	DeletionOperation *string `json:"deletion_operation,omitempty"`
	// New sandboxes keep no command history or process samples.
	MonitoringOff bool `json:"monitoring_off"`
	// New sandboxes are never snapshotted (a stop erases them).
	SnapshotsOff bool `json:"snapshots_off"`
	// Erase each sandbox's data (disk, snapshots, history) when it stops; no saved snapshots.
	ZeroDataRetention bool `json:"zero_data_retention"`
}

// `DELETE /sandboxes/{name}/snapshots`: the automatic snapshots that were deleted.
type DeletedSnapshots struct {
	// Their ids.
	Deleted []string `json:"deleted"`
	// The snapshot a stopped (or errored) sandbox will start from, which is kept.
	Kept *string `json:"kept,omitempty"`
}

// A deletion that runs in the background: closing an account (erased after the 30-day reopen window) or turning on zero data retention. `GET /v1/deletion-operations/{id}`.
type DeletionOperation struct {
	CreatedAt int64 `json:"created_at"`
	// Why the last attempt couldn't finish (status blocked).
	Error *string `json:"error,omitempty"`
	// When the erasure is due (an account close: the end of the reopen window).
	ExpectedBy *int64 `json:"expected_by,omitempty"`
	// `dop_…`
	ID string `json:"id"`
	// account_close | zero_data_retention
	Kind string `json:"kind"`
	// waiting_for_reopen_window | removing | retrying | completed | reopened
	Stage string `json:"stage"`
	// pending | processing | blocked | completed | canceled. `blocked` is not final: it is retried.
	Status    string `json:"status"`
	UpdatedAt int64  `json:"updated_at"`
}

// `POST /sandboxes/{name}/desktop`: a link to the VM's desktop in the browser.
type DesktopLink struct {
	ExpiresAt int64 `json:"expires_at"`
	// Secret: carries a token (in the fragment, after `#`). Open it within 10 minutes; an open session keeps working. Add `overlay=0` to the query (before the `#`) to hide the viewer's controls and stats, e.g. in an iframe.
	URL string `json:"url"`
}

type EmailAlert struct {
	Description string `json:"description"`
	Enabled     bool   `json:"enabled"`
	// disk | stop_refused | unresponsive | rebuilt | plan_nudge | billing
	Kind string `json:"kind"`
}

// `POST /v1/account/connections/email`: send a 6-digit code to an address to connect it.
type EmailCodeRequest struct {
	Email string `json:"email"`
}

// `POST /v1/account/connections/email/verify`: the code from the email.
type EmailCodeVerify struct {
	Code  string `json:"code"`
	Email string `json:"email"`
}

// A secret file written into every sandbox at each start (content never returned).
type EnvFile struct {
	// Relative to the home directory, e.g. `.npmrc` or `app/.env`.
	Path      string `json:"path"`
	Size      uint64 `json:"size"`
	UpdatedAt int64  `json:"updated_at"`
}

// An account environment variable as listed (the value is never returned, only a preview).
type EnvVar struct {
	Name string `json:"name"`
	// e.g. "sk-a…9xQ"; the whole value if it's short and not secret-looking.
	Preview   string `json:"preview"`
	UpdatedAt int64  `json:"updated_at"`
}

// The account's environment: what every new sandbox starts with, besides env vars.
type Environment struct {
	// New sandboxes use it when they name no environment.
	Default bool      `json:"default,omitempty"`
	Files   []EnvFile `json:"files"`
	// Its name (`default` for the account's first one).
	Name string `json:"name,omitempty"`
	// A setup script per repo (by its directory under ~), run in that directory after cloning, at every start, before the environment's own setup script.
	RepoSetup map[string]string `json:"repo_setup,omitempty"`
	// Git URLs cloned into `~/<repo>` when the directory doesn't exist yet (`https://github.com/org/repo`, optionally `#branch`).
	Repos []string `json:"repos"`
	// Shell script run in the background at every start, after cloning (as job "setup").
	Setup     *string `json:"setup,omitempty"`
	UpdatedAt int64   `json:"updated_at"`
	// Its latest version: every change makes a new one. Sandboxes stay on the version they started with until `POST /environments/{env}/upgrade`.
	Version uint32 `json:"version,omitempty"`
}

// One environment in `GET /environments`.
type EnvironmentSummary struct {
	CreatedAt int64 `json:"created_at"`
	// New sandboxes use it when they name none.
	Default bool   `json:"default"`
	Files   uint32 `json:"files"`
	Name    string `json:"name"`
	Repos   uint32 `json:"repos"`
	// Sandboxes (not deleted) per version they are on, newest version first.
	Sandboxes []EnvironmentVersionUse `json:"sandboxes"`
	// It has a setup script.
	Setup     bool  `json:"setup"`
	UpdatedAt int64 `json:"updated_at"`
	// Its latest version (every change makes a new one).
	Version uint32 `json:"version"`
}

// The answer to `POST /environments/{env}/upgrade`.
type EnvironmentUpgrade struct {
	// Running sandboxes that got the new version now (secret files written, files the new version dropped removed, the setup job started again).
	Applied []string `json:"applied"`
	// Sandboxes that get it at their next start (stopped, or not reachable right now).
	AtNextStart []string `json:"at_next_start"`
	Environment string   `json:"environment"`
	// The version they are on now.
	Version uint32 `json:"version"`
}

// One version of an environment (`GET /environments/{env}/versions`).
type EnvironmentVersion struct {
	// What the change was, e.g. "repos", "setup script", "file .npmrc set".
	Change    string `json:"change"`
	CreatedAt int64  `json:"created_at"`
	// Sandboxes (not deleted) on this version.
	Sandboxes uint32 `json:"sandboxes"`
	Version   uint32 `json:"version"`
}

type EnvironmentVersionUse struct {
	Sandboxes uint32 `json:"sandboxes"`
	Version   uint32 `json:"version"`
}

type ErrorBody struct {
	Error ErrorDetail `json:"error"`
}

type ErrorDetail struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	// This request's id (also in the `X-Request-Id` header); quote it when reporting a problem.
	RequestID *string `json:"request_id,omitempty"`
}

type EventList struct {
	Events []AgentEvent `json:"events"`
	// The id of the last event: pass it as `cursor` to get only newer ones.
	NextCursor int64 `json:"next_cursor"`
}

// One line of a streamed exec (`stream: true`): `started` first, then `stdout`/`stderr` chunks as they arrive, then exactly one `exit` or `error`.
type ExecEvent struct {
	CommandID *string `json:"command_id,omitempty"`
	Type      string  `json:"type"`
	Data      string  `json:"data,omitempty"`
	ExitCode  int32   `json:"exit_code,omitempty"`
	OomKilled bool    `json:"oom_killed,omitempty"`
	TimedOut  bool    `json:"timed_out,omitempty"`
	Code      string  `json:"code,omitempty"`
	Message   string  `json:"message,omitempty"`
}

type ExecRequest struct {
	Command string `json:"command"`
	// Run in this directory (relative to the home directory, or absolute).
	Cwd *string `json:"cwd,omitempty"`
	// Extra environment variables for this command only.
	Env map[string]string `json:"env,omitempty"`
	// Text fed to the command's standard input, which then ends (at most 1 MiB). Without it (and `stdin_b64`), stdin is empty.
	Stdin *string `json:"stdin,omitempty"`
	// Like `stdin`, base64-encoded (for binary input; at most 1 MiB decoded).
	StdinB64 *string `json:"stdin_b64,omitempty"`
	// true: answer with `application/x-ndjson`, one `ExecEvent` per line as output arrives (also chosen by `Accept: application/x-ndjson`).
	Stream      *bool   `json:"stream,omitempty"`
	TimeoutSecs *uint64 `json:"timeout_secs,omitempty"`
}

type ExecResult struct {
	// The command's record in the history (`GET /sandboxes/{name}/command-history/{id}`).
	CommandID *string `json:"command_id,omitempty"`
	// 124 when it timed out.
	ExitCode int32 `json:"exit_code"`
	// The kernel killed it for using too much memory (exit code 137 and an out-of-memory kill in the kernel log while it ran).
	OomKilled bool   `json:"oom_killed,omitempty"`
	Stderr    string `json:"stderr"`
	Stdout    string `json:"stdout"`
	// It ran past `timeout_secs` and was killed; stdout and stderr hold what it printed until then.
	TimedOut bool `json:"timed_out,omitempty"`
}

// Starts past the start limits (per minute, hour, day), paid from the wallet. Off by default.
type ExtraStarts struct {
	// They can run now: on, with a paid plan (not the trial or a plan set to end) and money in the wallet.
	Available bool `json:"available"`
	// The price of a start by how far past the included number it is. A start pays the highest price of the minute, hour and day windows.
	Brackets []StartBracket `json:"brackets"`
	// Turned on for this account (`PUT /billing/extra-starts`).
	Enabled bool `json:"enabled"`
	// Past this multiple of the per-minute limit, a start fails even when paid.
	MaxMinuteMultiple float64 `json:"max_minute_multiple"`
}

// `POST /feedback`: a message to the froog team (replies go to the account email).
type Feedback struct {
	Message string `json:"message"`
}

// One entry of a directory listing. `kind`: file | dir | link | other.
type FileEntry struct {
	Kind     string `json:"kind"`
	Modified int64  `json:"modified"`
	Name     string `json:"name"`
	Size     uint64 `json:"size"`
}

// One line of `GET /sandboxes/{name}/files/watch` (NDJSON). `type`: `ready` (watching; `method` inotify or poll), `change` (`event` create | modify | delete, `path`, `dir`), `error` (`message`), and last `end` (`reason` timeout | exited | error).
type FileEvent struct {
	Dir     *bool   `json:"dir,omitempty"`
	Event   *string `json:"event,omitempty"`
	Message *string `json:"message,omitempty"`
	Method  *string `json:"method,omitempty"`
	// As inotify or find print it: absolute if the watched path was, else relative to ~.
	Path   *string `json:"path,omitempty"`
	Reason *string `json:"reason,omitempty"`
	Type   string  `json:"type"`
}

// `GET /sandboxes/{name}/files/find?path=&pattern=&regex=`: lines that contain a text.
type FileFind struct {
	Errors []string `json:"errors,omitempty"`
	// At most 1,000.
	Matches   []FileMatch `json:"matches"`
	Truncated bool        `json:"truncated"`
}

// `GET /sandboxes/{name}/files?path=`: a file's details, or a directory and its entries.
type FileInfo struct {
	Entries  []FileEntry `json:"entries,omitempty"`
	Kind     string      `json:"kind"`
	Modified int64       `json:"modified"`
	Path     string      `json:"path"`
	Size     uint64      `json:"size"`
}

type FileMatch struct {
	// The line, cut at 500 characters.
	Content string `json:"content"`
	File    string `json:"file"`
	Line    uint64 `json:"line"`
}

// `POST /sandboxes/{name}/files/permissions`: chmod. The owner (the sandbox user) stays.
type FilePermissions struct {
	// Octal, e.g. 755 or 0600.
	Mode string `json:"mode"`
	Path string `json:"path"`
	// Also everything inside a directory.
	Recursive bool `json:"recursive,omitempty"`
}

// `POST /sandboxes/{name}/files/replace`: replace every occurrence of `pattern` (plain text, byte for byte) with `new_value` in each file (up to 100 files, 5 MiB each).
type FileReplace struct {
	NewValue string   `json:"new_value"`
	Paths    []string `json:"paths"`
	Pattern  string   `json:"pattern"`
}

type FileReplaced struct {
	Error        *string `json:"error,omitempty"`
	File         string  `json:"file"`
	Replacements *uint64 `json:"replacements,omitempty"`
	Success      bool    `json:"success"`
}

// `GET /sandboxes/{name}/files/search?path=&pattern=`: paths whose name matches a glob.
type FileSearch struct {
	// Parts of the tree that couldn't be searched (the results are then incomplete).
	Errors []string `json:"errors,omitempty"`
	// Absolute paths, at most 10,000.
	Files     []string `json:"files"`
	Truncated bool     `json:"truncated"`
}

// A URL that GETs (download) or PUTs (upload, raw bytes) one file with no Authorization header.
type FileUrl struct {
	ExpiresAt int64  `json:"expires_at"`
	Method    string `json:"method"`
	URL       string `json:"url"`
}

type ForkSandbox struct {
	Count *uint32 `json:"count,omitempty"`
	// Environment variables set on top of the source's own (these win).
	Env map[string]string `json:"env,omitempty"`
	// false: the copies run without the environment; when the source had it, the owner's logins, keys and secret files are removed from their disks before they count as running. Default: the source's.
	Environment *bool `json:"environment,omitempty"`
	// Move the copies to this environment's latest version. Default: exactly the source's environment and version.
	EnvironmentName *string `json:"environment_name,omitempty"`
	// Labels set on top of the source's own (these win).
	Labels map[string]string `json:"labels,omitempty"`
	// Override the source's monitoring setting.
	Monitoring *bool  `json:"monitoring,omitempty"`
	Name       string `json:"name"`
	// Size of the new sandboxes; defaults to the source's.
	Size *Size `json:"size,omitempty"`
	// Snapshot id or label. Defaults to a fresh snapshot (running) or the latest one (stopped).
	Snapshot *string `json:"snapshot,omitempty"`
	// false: never snapshot the copies; a stop erases them. Default: the account's setting.
	Snapshots *bool `json:"snapshots,omitempty"`
	// Auto-stop timer of the copies in seconds (0 = no auto-stop). Default: the source's.
	TTLSecs *uint64 `json:"ttl_secs,omitempty"`
}

// `POST /v1/github/connect`: where to install the app (or pick another account).
type GithubConnect struct {
	URL string `json:"url"`
}

// A GitHub App installation linked to the account.
type GithubInstallation struct {
	// The GitHub user or organization it is installed on.
	Account string `json:"account"`
	// User | Organization
	AccountType string `json:"account_type"`
	CreatedAt   int64  `json:"created_at"`
	// GitHub's installation id.
	ID int64 `json:"id"`
	// all | selected (the repositories GitHub lets the app see)
	RepositorySelection string `json:"repository_selection"`
}

// A repository the app can see (`GET /v1/github/repos`).
type GithubRepo struct {
	// https://github.com/owner/name.git
	CloneURL      string  `json:"clone_url"`
	DefaultBranch *string `json:"default_branch,omitempty"`
	// owner/name
	FullName       string `json:"full_name"`
	ID             int64  `json:"id"`
	InstallationID int64  `json:"installation_id"`
	Private        bool   `json:"private"`
}

// `GET /v1/github`.
type GithubStatus struct {
	// The `runs-on` label of CI jobs that run on froog.
	CiLabel string `json:"ci_label"`
	// This server has a GitHub App set up (otherwise every other GitHub call is 404 github_not_configured).
	Configured    bool                 `json:"configured"`
	Installations []GithubInstallation `json:"installations"`
	// `owner/name` of the repositories the token in each sandbox may use; empty = every repository the app can see.
	TokenRepos []string `json:"token_repos"`
}

// A short-lived GitHub installation token (one hour).
type GithubToken struct {
	ExpiresAt int64 `json:"expires_at"`
	// The GitHub account it works for.
	Owner string `json:"owner"`
	// The repositories it is limited to; empty = every repository of the installation.
	Repositories []string `json:"repositories"`
	Token        string   `json:"token"`
}

// `POST /v1/sandboxes/{name}/github-token`.
type GithubTokenRequest struct {
	// The GitHub user or organization (default: the first linked installation).
	Owner *string `json:"owner,omitempty"`
}

// An email address that receives mail for an agent (`/v1/inboxes`). Receive-only for now.
type Inbox struct {
	// The full address, e.g. `signup-k3f9qz@froogmail.com`.
	Address       string `json:"address"`
	CreatedAt     int64  `json:"created_at"`
	ID            string `json:"id"`
	LastMessageAt *int64 `json:"last_message_at,omitempty"`
	// Messages kept (older than the retention period are deleted).
	Messages uint64 `json:"messages,omitempty"`
	// Your label for it (not part of the address).
	Name string `json:"name,omitempty"`
	// The sandbox it belongs to, if any.
	Sandbox *string `json:"sandbox,omitempty"`
}

// What the account pays for inboxes (`GET /v1/inboxes/billing`).
type InboxBilling struct {
	// Whether inboxes can be made and receive mail now.
	Active            bool   `json:"active"`
	CancelAtPeriodEnd bool   `json:"cancel_at_period_end,omitempty"`
	PeriodEnd         *int64 `json:"period_end,omitempty"`
	// One inbox a month, in US cents.
	PriceCents uint32 `json:"price_cents"`
	// Whether this account pays for inboxes (false: billing-exempt, or a server without payments).
	Required bool `json:"required"`
	// Inboxes paid for this period.
	Seats uint32 `json:"seats"`
	// The Stripe subscription's status (active, past_due, ...); none without one.
	Status *string `json:"status,omitempty"`
	// Inboxes the account has.
	Used uint32 `json:"used"`
}

// `POST /v1/inboxes/billing/checkout`: how many inboxes to buy (default: yours plus one).
type InboxCheckout struct {
	Quantity *uint32 `json:"quantity,omitempty"`
}

// A received message. Lists leave out `text` and `html`; get one message for them.
type InboxMessage struct {
	// One-time codes found in the subject and body, most likely first.
	Codes []string `json:"codes,omitempty"`
	// The SMTP envelope sender (bounce address); may differ from `from`.
	EnvelopeFrom string `json:"envelope_from,omitempty"`
	// The From header's address.
	From     string  `json:"from"`
	FromName string  `json:"from_name,omitempty"`
	Html     *string `json:"html,omitempty"`
	ID       string  `json:"id"`
	InboxID  string  `json:"inbox_id"`
	// Verification, confirmation and sign-in links found in the body, most likely first.
	Links []string `json:"links,omitempty"`
	// The first 200 characters of the text.
	Preview    string  `json:"preview,omitempty"`
	ReceivedAt int64   `json:"received_at"`
	Size       uint64  `json:"size"`
	Subject    string  `json:"subject"`
	Text       *string `json:"text,omitempty"`
	To         string  `json:"to"`
}

type InboxMessageList struct {
	// Newest first.
	Messages []InboxMessage `json:"messages"`
}

// `GET /v1/inboxes/{inbox}/wait`: the first matching message, or none when the wait timed out.
type InboxWait struct {
	Message *InboxMessage `json:"message,omitempty"`
}

type InterruptRequest struct {
	// Only this conversation (default: every conversation on the sandbox).
	ConversationID *string `json:"conversation_id,omitempty"`
}

type InterruptResult struct {
	// The prompts stopped (running) or cancelled (queued).
	Interrupted []string `json:"interrupted"`
}

// An opened port's public IPv4 address: the host's address and a port of its own, forwarded to the sandbox's port.
type Ipv4Port struct {
	// Connect here over IPv4: "203.0.113.7:40001".
	Address string `json:"address"`
	// The sandbox's port, "8080/tcp".
	Port string `json:"port"`
	// The port on the host's IPv4 address that forwards to it.
	PublicPort uint16 `json:"public_port"`
}

// A background command. `state`: running | paused (frozen with a sandbox stopped with its memory kept; a start resumes it) | exited | killed | lost (the sandbox stopped under it).
type Job struct {
	// First 200 characters.
	Command string `json:"command"`
	// The job's record in the command history (None for jobs froog started itself, like setup).
	CommandID *string `json:"command_id,omitempty"`
	ExitCode  *int32  `json:"exit_code,omitempty"`
	ID        string  `json:"id"`
	StartedAt int64   `json:"started_at"`
	State     string  `json:"state"`
}

// A job's state and a chunk of its output (stdout and stderr together), read from `offset`. Pass `next_offset` as the next `offset` to follow it.
type JobOutput struct {
	ExitCode   *int32 `json:"exit_code,omitempty"`
	ID         string `json:"id"`
	NextOffset uint64 `json:"next_offset"`
	Output     string `json:"output"`
	OutputSize uint64 `json:"output_size"`
	StartedAt  int64  `json:"started_at"`
	State      string `json:"state"`
}

// Write to a running job's standard input (one started with `stdin_open`), and/or close it.
type JobStdin struct {
	// Close the input after writing: the command reads end of file.
	Close *bool `json:"close,omitempty"`
	// Text to write (at most 1 MiB).
	Data *string `json:"data,omitempty"`
	// Like `data`, base64-encoded (at most 1 MiB decoded).
	DataB64 *string `json:"data_b64,omitempty"`
}

type JsonRpcError struct {
	// -32700 parse error, -32600 invalid request, -32601 unknown method, -32602 bad params.
	Code    int64  `json:"code"`
	Message string `json:"message"`
}

// A JSON-RPC 2.0 message to `POST /mcp` (a batch is an array of them). Without `id` it's a notification, answered with 202 and no body.
type JsonRpcRequest struct {
	ID any `json:"id,omitempty"`
	// Always "2.0".
	JSONRPC string `json:"jsonrpc"`
	// initialize, ping, tools/list, tools/call, notifications/initialized, …
	Method string `json:"method"`
	Params any    `json:"params,omitempty"`
}

// The answer to a JSON-RPC request: `result` or `error`. A failed tool call is a `result` with `isError: true` and the API error (`code`, `message`, `request_id`) as its text.
type JsonRpcResponse struct {
	Error   *JsonRpcError `json:"error,omitempty"`
	ID      any           `json:"id"`
	JSONRPC string        `json:"jsonrpc"`
	Result  any           `json:"result,omitempty"`
}

// What an account may do now: `GET /v1/limits`.
type Limits struct {
	// Sandboxes at once (lower during the free trial).
	MaxSandboxes uint32 `json:"max_sandboxes"`
	// Only when another plan fits the last 30 days' usage better.
	PlanNudge *PlanNudge `json:"plan_nudge,omitempty"`
	// Running or starting now.
	Running uint32 `json:"running"`
	// Saved snapshots included in the plan vs kept now.
	SavedSnapshotAllowance *SavedSnapshotAllowance `json:"saved_snapshot_allowance,omitempty"`
	// Sizes the account can start.
	Sizes  []Size      `json:"sizes"`
	Starts StartLimits `json:"starts"`
	// The free trial; None when this account has none (billing off, or exempt).
	Trial *Trial `json:"trial,omitempty"`
}

type MakeDir struct {
	// Octal permissions of the new directory, e.g. 700 (default: the umask's, usually 755).
	Mode *string `json:"mode,omitempty"`
	Path string  `json:"path"`
}

// `GET /sandboxes/{name}/metrics/history`: CPU, memory and disk sampled every 30 seconds while the sandbox runs (monitoring on), kept 7 days, averaged per `step_secs`, oldest first. Times when it was stopped have no points.
type MetricsHistory struct {
	Points []MetricsPoint `json:"points"`
	// The window, unix seconds.
	Since int64 `json:"since"`
	// Each point averages this many seconds (a multiple of 30; at most 1000 points).
	StepSecs uint32 `json:"step_secs"`
	Until    int64  `json:"until"`
}

type MetricsPoint struct {
	// The bucket's start, unix seconds.
	At            int64    `json:"at"`
	CPUMaxPercent *float64 `json:"cpu_max_percent,omitempty"`
	// Share of all CPUs busy (0-100), the average and the highest sample in the bucket.
	CPUPercent *float64 `json:"cpu_percent,omitempty"`
	// Of the root filesystem.
	DiskUsedPercent *float64 `json:"disk_used_percent,omitempty"`
	MemoryTotalMb   *uint64  `json:"memory_total_mb,omitempty"`
	MemoryUsedMb    *uint64  `json:"memory_used_mb,omitempty"`
	// Samples in the bucket.
	Samples uint32 `json:"samples"`
}

type MoveFile struct {
	From string `json:"from"`
	To   string `json:"to"`
}

type NewApiKey struct {
	Actions []string `json:"actions,omitempty"`
	// Limit the key to sandboxes of these environments (names). Sandboxes it creates use the first one unless they name another of them.
	Environments []string `json:"environments,omitempty"`
	Label        *string  `json:"label,omitempty"`
	// Limit the key to sandboxes that have all of these labels. Sandboxes it creates or forks get them, and it can't change or remove them.
	Labels map[string]string `json:"labels,omitempty"`
	// A key that works until revoked.
	NeverExpires bool `json:"never_expires,omitempty"`
	// read-only | full-sandbox | ci | admin (`scopes::PRESETS`). Use this or `actions`.
	Preset *string `json:"preset,omitempty"`
	// Limit the key to these sandboxes (names), plus any it creates.
	Sandboxes []string `json:"sandboxes,omitempty"`
	// Days until it expires (1-365). Default 90.
	TTLDays *uint32 `json:"ttl_days,omitempty"`
}

// `POST /environments`
type NewEnvironment struct {
	// Start with a copy of this environment's latest version (repos, scripts, secret files). Default: empty.
	CopyFrom *string `json:"copy_from,omitempty"`
	// Make it the default right away.
	Default *bool  `json:"default,omitempty"`
	Name    string `json:"name"`
}

// A file to write into a new sandbox (`CreateSandbox.files`).
type NewFile struct {
	Content string `json:"content"`
	// utf8 (default) or base64 (for binary files).
	Encoding *string `json:"encoding,omitempty"`
	// Relative to the home directory, e.g. `app/.env`.
	Path string `json:"path"`
}

// `POST /sandboxes/{name}/files/download-url` or `/upload-url`.
type NewFileUrl struct {
	// Seconds the URL works (60-86400, default 900).
	ExpiresIn *uint64 `json:"expires_in,omitempty"`
	Path      string  `json:"path"`
}

// Start a background command: `POST /sandboxes/{name}/jobs`.
type NewJob struct {
	Command string `json:"command"`
	// Run in this directory (relative to the home directory, or absolute).
	Cwd *string `json:"cwd,omitempty"`
	// Extra environment variables for this command only.
	Env map[string]string `json:"env,omitempty"`
	// "detached" (default) or "stream" (`froog exec --timeout 0` follows it); shown in the history.
	Mode *string `json:"mode,omitempty"`
	// Text for the job's standard input, given at start (at most 1 MiB). Kept with the job on the sandbox's disk (`~/.froog/jobs/<id>/stdin`).
	Stdin *string `json:"stdin,omitempty"`
	// Like `stdin`, base64-encoded (at most 1 MiB decoded).
	StdinB64 *string `json:"stdin_b64,omitempty"`
	// Keep standard input open: write to it later with `POST /jobs/{id}/stdin` (after `stdin`, if given), and close it there for end of file.
	StdinOpen *bool `json:"stdin_open,omitempty"`
}

// `POST /sandboxes/{name}/urls`: host a port, or change how an already hosted port is reached.
type NewPublicUrl struct {
	Port uint16 `json:"port"`
	// true: no token. Default false (private), like boat. Hosting a public port again without it gives it a new token; with it, removes the token.
	Public bool `json:"public,omitempty"`
	// Rename the URL to `https://<subdomain>.<domain>` (3-40 lowercase letters, digits, hyphens).
	Subdomain *string `json:"subdomain,omitempty"`
	// A label for people (at most 100 characters); "" removes it. Hosting again without it keeps the old one.
	Title *string `json:"title,omitempty"`
}

// `POST /sandboxes/{name}/urls/{port}/tokens`: another token for a private URL, e.g. one per viewer, that expires and can be revoked on its own.
type NewUrlToken struct {
	// Who or what it's for (at most 100 characters).
	Label *string `json:"label,omitempty"`
	// Seconds until it stops working: 60 to 30 days, default 1 day.
	TTLSecs *uint64 `json:"ttl_secs,omitempty"`
}

type NewWebhook struct {
	Events []string `json:"events,omitempty"`
	Name   *string  `json:"name,omitempty"`
	URL    string   `json:"url"`
}

// `POST /sandboxes/{name}/ports`: open one inbound port (on the sandbox's IPv6 address, and on a public port of its host's IPv4 address).
type OpenPort struct {
	Port uint16 `json:"port"`
	// tcp (default) or udp.
	Protocol string `json:"protocol,omitempty"`
}

// One workspace in `GET /orgs`: your personal account first, then each organization.
type OrgSummary struct {
	// The active workspace: requests that name none use it (CLI, account API keys).
	Active bool `json:"active"`
	// `personal`, or the organization id (`org_…`).
	ID string `json:"id"`
	// personal | org
	Kind string `json:"kind"`
	// Members (1 for personal).
	Members uint32 `json:"members,omitempty"`
	Name    string `json:"name"`
	// Your role: owner | admin | member (`owner` for personal).
	Role string `json:"role"`
}

// The operating system a sandbox runs. All come with the same tools. `omarchy` is Arch Linux with Omarchy's Hyprland desktop (a sandbox with a screen). Accepts the id or the distro's name ("debian").
type Os string

const (
	OsUbuntu2404 Os = "ubuntu-24.04"
	OsUbuntu2604 Os = "ubuntu-26.04"
	OsDebian13   Os = "debian-13"
	OsOmarchy    Os = "omarchy"
)

// An OS sandboxes can start with: `GET /v1/images`.
type OsImage struct {
	Default bool   `json:"default"`
	ID      Os     `json:"id"`
	Name    string `json:"name"`
}

// What choosing a plan did: a Checkout page to open (first plan), or the plan changed.
type PlanChange struct {
	Billing Billing `json:"billing"`
	// Open this Stripe page to subscribe (and save a card). None = changed already.
	URL *string `json:"url,omitempty"`
}

// A plan on offer.
type PlanInfo struct {
	// Sandboxes at once.
	MaxSandboxes uint32 `json:"max_sandboxes"`
	// Saved snapshots included.
	SavedSnapshots uint32 `json:"saved_snapshots"`
	// Dollars a month, all of it usable as running time and storage.
	USD uint32 `json:"usd"`
	// The plan can start `xlarge` sandboxes.
	Xlarge bool `json:"xlarge,omitempty"`
}

// In `GET /limits`, only when another plan fits the last 30 days' usage better.
type PlanNudge struct {
	// The suggestion, with the sum.
	Text string `json:"text"`
	// The plan that fits, dollars a month.
	USD uint32 `json:"usd"`
	// Running time used in the last 30 days, at list prices.
	UsedUSD float64 `json:"used_usd"`
}

// One line of `GET /sandboxes/{name}/ports/watch` (NDJSON). `type`: `ready`, then `open` / `close` (`port`, `protocol` tcp) as TCP ports start or stop listening (the ones already listening come first as `open`), and last `end` (`reason`, `message`).
type PortEvent struct {
	Message  *string `json:"message,omitempty"`
	Port     *uint16 `json:"port,omitempty"`
	Protocol *string `json:"protocol,omitempty"`
	Reason   *string `json:"reason,omitempty"`
	Type     string  `json:"type"`
}

// The answer to `POST /sandboxes/{name}/wait`.
type PortWait struct {
	Port uint16 `json:"port"`
	// true: something listens on the port (on any address); false: the timeout passed first.
	Ready bool `json:"ready"`
	// How long it waited, in milliseconds.
	WaitedMs uint64 `json:"waited_ms"`
}

// Inbound ports open on a sandbox (`GET /sandboxes/{name}/ports`): on its public IPv6 address as they are, and the explicitly opened ones also on a public port of its host's IPv4 address (`ipv4`). They stay open across stops and starts.
type Ports struct {
	// The ones open because a public URL serves that TCP port: they close with the URL (or when it's made private) unless they were also opened on their own.
	FromURLs []string `json:"from_urls,omitempty"`
	// Where the explicitly opened ports are reached over IPv4: sandboxes share their host's IPv4 address, so each gets a public port of its own there. Empty while stopped (a start gives them again, the same ones when it lands on the same host) and for public URLs' ports.
	IPv4 []Ipv4Port `json:"ipv4,omitempty"`
	// Why some opened ports have no IPv4 address (stopped, a limit reached, the host's range full).
	IPv4Note *string `json:"ipv4_note,omitempty"`
	// Everything open besides SSH, as "8080/tcp" or "27015/udp", sorted.
	Ports []string `json:"ports"`
}

// A process in a sandbox (`GET /sandboxes/{name}/processes`), busiest first.
type ProcessInfo struct {
	Command string `json:"command"`
	// The history record of the command this process belongs to, if any.
	CommandID  *string `json:"command_id,omitempty"`
	CPUPercent float64 `json:"cpu_percent"`
	MemoryMb   float64 `json:"memory_mb"`
	Pid        uint32  `json:"pid"`
	User       string  `json:"user"`
}

// `live`: false when the sandbox isn't running and this is the last sample kept (`sampled_at`).
type ProcessList struct {
	Live      bool          `json:"live"`
	Processes []ProcessInfo `json:"processes"`
	SampledAt *int64        `json:"sampled_at,omitempty"`
	Total     uint32        `json:"total"`
}

type ProcessRecord struct {
	// Its command line (up to 1024 characters).
	Command string `json:"command"`
	// The first sample it was missing from: it ended between `last_seen_at` and this. None while it runs.
	EndedAt *int64 `json:"ended_at,omitempty"`
	// The last sample it was in.
	LastSeenAt int64  `json:"last_seen_at"`
	Pid        uint32 `json:"pid"`
	Ppid       uint32 `json:"ppid"`
	// Unix seconds (from its elapsed time when first seen).
	StartedAt int64  `json:"started_at"`
	User      string `json:"user"`
}

// `GET /sandboxes/{name}/processes/records`: processes seen in the 30-second samples (monitoring on), kept 72 hours after they end, newest start first. A process that starts and ends between two samples isn't recorded.
type ProcessRecords struct {
	Records []ProcessRecord `json:"records"`
}

// `GET /sandboxes/{name}/processes/samples`: the 12 busiest processes, sampled every 30 seconds while the sandbox runs and kept 7 days, newest first.
type ProcessSamples struct {
	Samples []ProcessList `json:"samples"`
}

// One prompt (a turn of a conversation).
type Prompt struct {
	Agent string `json:"agent"`
	// Paths of its attachments in the sandbox.
	Attachments    []string `json:"attachments,omitempty"`
	ConversationID string   `json:"conversation_id"`
	CreatedAt      int64    `json:"created_at"`
	// done, failed or interrupted.
	Done   bool    `json:"done"`
	Effort *string `json:"effort,omitempty"`
	// Why it failed (failed prompts).
	Error *string `json:"error,omitempty"`
	// e.g. provider_not_configured, timed_out, sandbox_stopped.
	ErrorCode  *string `json:"error_code,omitempty"`
	ExitCode   *int32  `json:"exit_code,omitempty"`
	Fast       *bool   `json:"fast,omitempty"`
	FinishedAt *int64  `json:"finished_at,omitempty"`
	ID         string  `json:"id"`
	Model      *string `json:"model,omitempty"`
	// The prompt text (empty for a sandbox with monitoring off, once it started).
	Prompt string `json:"prompt"`
	// The agent's final answer (done prompts).
	Result    *string `json:"result,omitempty"`
	Sandbox   string  `json:"sandbox"`
	StartedAt *int64  `json:"started_at,omitempty"`
	// queued | running | done | failed | interrupted
	Status string `json:"status"`
}

// A file sent with a prompt (`attachments`): saved in the sandbox under `~/attachments/<prompt id>/<name>`, and its path given to the agent.
type PromptAttachment struct {
	// The file's bytes, base64.
	ContentB64 string `json:"content_b64"`
	// File name (no directories), e.g. "error.png".
	Name string `json:"name"`
}

type PromptList struct {
	Prompts []Prompt `json:"prompts"`
}

type PromptRequest struct {
	// The harness: claude (default), codex, gemini, opencode, pi, amp, cursor, prime, kimi or mistral.
	Agent *string `json:"agent,omitempty"`
	// Images, PDFs or other files for the agent: up to 12, 25 MB each, 64 MB in total.
	Attachments []PromptAttachment `json:"attachments,omitempty"`
	// Continue this conversation (default: the sandbox's most recent one).
	ConversationID *string `json:"conversation_id,omitempty"`
	// Reasoning effort / thinking level, for harnesses that take one (claude, codex, pi, prime, kimi).
	Effort *string `json:"effort,omitempty"`
	// Fast mode (same model, faster output at the provider's fast rate): claude (Opus) and codex. false forces standard speed.
	Fast *bool `json:"fast,omitempty"`
	// Passed to the harness's model flag, e.g. "opus", "gpt-6.1-sol", "anthropic/claude-sonnet-4-5" (`GET /provider-models`).
	Model *string `json:"model,omitempty"`
	// Start a new conversation.
	New         *bool   `json:"new,omitempty"`
	Prompt      string  `json:"prompt"`
	TimeoutSecs *uint64 `json:"timeout_secs,omitempty"`
}

// What `POST /sandboxes/{name}/prompt` (wait for the answer) returns.
type PromptResult struct {
	ConversationID string `json:"conversation_id"`
	// 0 when the agent finished; 124 when it timed out.
	ExitCode int32  `json:"exit_code"`
	PromptID string `json:"prompt_id"`
	// done | failed | interrupted | running (still running when the wait timed out)
	Status string `json:"status"`
	// Why it failed, if it did.
	Stderr string `json:"stderr"`
	// The agent's final answer.
	Stdout   string `json:"stdout"`
	TimedOut bool   `json:"timed_out"`
}

// One model of a harness in the catalog.
type ProviderModel struct {
	// Accepted `effort` levels (empty: none).
	Efforts []string `json:"efforts"`
	// Takes `fast: true`.
	FastMode bool   `json:"fast_mode"`
	ID       string `json:"id"`
	Title    string `json:"title"`
}

// A harness and the models it accepts.
type ProviderModels struct {
	// The `agent` value.
	Agent   string   `json:"agent"`
	Aliases []string `json:"aliases"`
	// Takes model ids beyond `models` (e.g. `openrouter/…`, full model ids).
	AnyModel bool `json:"any_model"`
	// Effort levels of the harness.
	Efforts []string `json:"efforts"`
	// Has a fast mode.
	FastMode bool `json:"fast_mode"`
	// Env vars it runs on (any one).
	Keys []string `json:"keys"`
	// Model ids the harness listed in the sandbox (`?sandbox=`; harnesses that can list them).
	LiveModels []string        `json:"live_models,omitempty"`
	Models     []ProviderModel `json:"models"`
	// Takes a message in the middle of a running turn (`steer`).
	Steer bool `json:"steer"`
	// Takes `model` at all.
	TakesModel bool   `json:"takes_model"`
	Title      string `json:"title"`
}

// A public HTTPS URL for a port inside a sandbox.
type PublicUrl struct {
	CreatedAt int64  `json:"created_at"`
	Port      uint16 `json:"port"`
	// false: only people with the token (in the URL, or the `__Host-froog_url` cookie) get through.
	Public bool `json:"public,omitempty"`
	// A label for people, e.g. "API docs".
	Title *string `json:"title,omitempty"`
	// For a private URL, includes `?_token=…`: treat it like a password.
	URL string `json:"url"`
}

// What a size costs to run, billed per second.
type Rate struct {
	PerHourUSD float64 `json:"per_hour_usd"`
	// A full month (730 hours).
	PerMonthUSD float64 `json:"per_month_usd"`
	Size        Size    `json:"size"`
}

type Restore struct {
	Snapshot string `json:"snapshot"`
}

type Sandbox struct {
	// In an organization: what you may do with it. `owner` (you created it), `use` (a shared sandbox) or `view` (a teammate's private sandbox: see it and its usage).
	Access *string `json:"access,omitempty"`
	// While stopped, a request to one of its public URLs, or an exec, file, job or wait call, starts it (billed and limited like any start).
	AutoStart bool `json:"auto_start,omitempty"`
	// While running with an auto-stop timer: when it stops (unix seconds). A running agent prompt moves it one `ttl_secs` later; `PATCH ttl_secs` sets it to now + ttl_secs.
	AutoStopAt *int64 `json:"auto_stop_at,omitempty"`
	Backend    string `json:"backend"`
	// Unix seconds.
	CreatedAt int64 `json:"created_at"`
	// In an organization: the email of the member who created it (absent for personal sandboxes and for those the organization made itself, such as CI jobs).
	CreatedBy *string `json:"created_by,omitempty"`
	// Deleted after staying stopped this many seconds. 0 = never.
	DeleteAfterSecs uint64 `json:"delete_after_secs,omitempty"`
	// While stopped with `delete_after_secs`: when it will be deleted (unix seconds).
	DeleteAt *int64 `json:"delete_at,omitempty"`
	// Set once the disk of a stopped sandbox was deleted; start and fork then fail.
	DiskExpired bool `json:"disk_expired,omitempty"`
	// For a stopped sandbox: when its disk will be deleted (save a snapshot to keep it longer).
	DiskExpiresAt *int64 `json:"disk_expires_at,omitempty"`
	// Disk use of a running sandbox in percent (checked every 30 seconds).
	DiskUsedPercent *uint8 `json:"disk_used_percent,omitempty"`
	// Names of its own environment variables (values are never returned).
	Env []string `json:"env,omitempty"`
	// false: it runs without its environment (`--no-environment`).
	Environment bool `json:"environment,omitempty"`
	// The environment it belongs to (`default` unless it was created with another), also when it runs without it (`environment: false`).
	EnvironmentName *string `json:"environment_name,omitempty"`
	// The version of that environment it is on: the latest when it was created; it moves only with `POST /environments/{env}/upgrade` (or a start with `environment_name`).
	EnvironmentVersion *uint32 `json:"environment_version,omitempty"`
	Error              *string `json:"error,omitempty"`
	// A deadline: deleted at this time (unix seconds) whatever its state, like a TTL from creation (`expires_in_secs`). Saved snapshots stay.
	ExpiresAt *int64 `json:"expires_at,omitempty"`
	// Where its disk came from, when it was forked or started from a saved snapshot (absent for new sandboxes, ones from a share code, and those made before this was recorded).
	ForkedFrom *SandboxOrigin `json:"forked_from,omitempty"`
	IPv4       *string        `json:"ipv4,omitempty"`
	// Its own public IPv6 address (inbound: SSH and its open ports, `/sandboxes/{name}/ports`). None while stopped; it can change at a start (not at a warm resume).
	IPv6 *string `json:"ipv6,omitempty"`
	// Your own key-value tags (`label=key=value` filters lists).
	Labels       map[string]string `json:"labels,omitempty"`
	LastActiveAt int64             `json:"last_active_at"`
	// When its operating system last booted: a start from its disk, a restart after a crash or a host restart, or a reboot inside it (a warm resume isn't a reboot). Unix seconds.
	LastReboot *int64 `json:"last_reboot,omitempty"`
	// Stopped this many seconds after each start, busy or not. 0 = no limit.
	MaxLifetimeSecs uint64 `json:"max_lifetime_secs,omitempty"`
	// false: no command history or process samples.
	Monitoring bool   `json:"monitoring,omitempty"`
	Name       string `json:"name"`
	OS         Os     `json:"os,omitempty"`
	// What its running time cost in the current billing period, at list prices (in lists and info; saved snapshots are billed per account, not here).
	PeriodCostUSD *float64 `json:"period_cost_usd,omitempty"`
	// While creating or starting: what takes the time, e.g. copying its disk from another host.
	Progress *string `json:"progress,omitempty"`
	// The repo cloned into this sandbox at create (`CreateSandbox.repo`).
	Repo *SandboxRepo `json:"repo,omitempty"`
	// Why setup failed: its error lines (a failed clone, a script's exit code).
	SetupError *string `json:"setup_error,omitempty"`
	// The setup job (repos, the environment's setup script, `new --setup`) of the last boot: none | running | done | failed.
	SetupStatus string `json:"setup_status,omitempty"`
	Size        Size   `json:"size"`
	// false: never snapshotted; a stop erases it (no start, fork or restore).
	Snapshots bool `json:"snapshots,omitempty"`
	// SSH port at `ipv4` (VMs on a shared host each have their own).
	SSHPort   uint16 `json:"ssh_port,omitempty"`
	StartedAt *int64 `json:"started_at,omitempty"`
	State     State  `json:"state"`
	// While running with `max_lifetime_secs`: when it will be stopped (unix seconds).
	StopAt *int64 `json:"stop_at,omitempty"`
	// Why the stop was refused: the last snapshot error.
	StopError *string `json:"stop_error,omitempty"`
	// Only in the answer to `POST /sandboxes/{name}/stop`: the stop's id, for `GET /stops/{id}`.
	StopID *string `json:"stop_id,omitempty"`
	// Set while a stop is refused because its snapshot failed: since when. The sandbox keeps running, unbilled, and froog retries the stop until a snapshot works (a forced stop after 3 days); a start cancels the stop.
	StopRefusedAt *int64 `json:"stop_refused_at,omitempty"`
	// The auto-stop timer: it stops this many seconds after each start (or resume), busy or not, unless an agent prompt is running. 0 = no auto-stop.
	TTLSecs uint64 `json:"ttl_secs"`
	// Set while a running sandbox doesn't answer (commands and SSH hang): since when. Billing pauses meanwhile; after 3 days it's stopped onto its last snapshot.
	UnresponsiveSince *int64 `json:"unresponsive_since,omitempty"`
	// In an organization: `shared` (every member can use it) or `private` (only its creator).
	Visibility *string `json:"visibility,omitempty"`
	// Stopped, with its memory kept until then: a start before it resumes every running program in about a second (after it, the start boots from the disk snapshot).
	WarmUntil *int64 `json:"warm_until,omitempty"`
}

// One sandbox's AI settings and usage (`GET /v1/sandboxes/{name}/ai`).
type SandboxAi struct {
	// Its AI requests are refused.
	Blocked bool   `json:"blocked"`
	Days    uint32 `json:"days"`
	// The profile it uses (its own, its environment's, or the default).
	Profile *string `json:"profile,omitempty"`
	// Set on the sandbox itself (else it follows its environment).
	ProfileSet bool `json:"profile_set"`
	// It holds placeholders now (the proxy was on at its last start).
	Proxied bool         `json:"proxied"`
	Sandbox string       `json:"sandbox"`
	Totals  AiTotals     `json:"totals"`
	Usage   []AiUsageRow `json:"usage"`
}

// `GET /sandboxes/{name}/metrics`: one sample read from the running sandbox now.
type SandboxMetrics struct {
	CPUCount uint32 `json:"cpu_count"`
	// Share of all CPUs busy over half a second (0-100).
	CPUPercent     float64 `json:"cpu_percent"`
	DiskTotalBytes uint64  `json:"disk_total_bytes"`
	// Of the root filesystem.
	DiskUsedBytes uint64 `json:"disk_used_bytes"`
	// Share of the last minute some task waited for disk I/O (0-1).
	IoPressure       float64 `json:"io_pressure"`
	MemoryTotalBytes uint64  `json:"memory_total_bytes"`
	MemoryUsedBytes  uint64  `json:"memory_used_bytes"`
	// Received and sent since boot, all interfaces but loopback.
	NetworkRxBytes uint64 `json:"network_rx_bytes"`
	NetworkTxBytes uint64 `json:"network_tx_bytes"`
	// Unix seconds.
	SampledAt int64 `json:"sampled_at"`
}

// `Sandbox.forked_from`: the sandbox and snapshot a fork (or a start from a saved snapshot) copied its disk from.
type SandboxOrigin struct {
	// Unix seconds: when the copy was made.
	At int64 `json:"at"`
	// The snapshot's name, when it has one.
	Label *string `json:"label,omitempty"`
	// The source sandbox's name (it may have been deleted or renamed since).
	Sandbox string `json:"sandbox"`
	// The snapshot's id (`snap_…`); absent for a fork of the source's state at that moment.
	Snapshot *string `json:"snapshot,omitempty"`
}

// A repo cloned into one sandbox (`CreateSandbox.repo`), like the account's repos: at boot, when the directory has no checkout yet. Credentials come from the sandbox's environment (`GH_TOKEN`, `~/.git-credentials`), never the URL.
type SandboxRepo struct {
	// Branch or tag to check out (default: the repo's default branch).
	Branch *string `json:"branch,omitempty"`
	// Directory relative to the home directory (default: the repo's name).
	Dir *string `json:"dir,omitempty"`
	// `https://`, `ssh://` or `git@` URL.
	URL string `json:"url"`
}

// `GET /sandboxes/{name}/usage`: one sandbox's running time and cost in a window.
type SandboxUsage struct {
	// At list prices (saved snapshots are billed per account, not here).
	CostUSD float64 `json:"cost_usd"`
	// The window, unix seconds (`to` is at most now).
	From int64 `json:"from"`
	// One line per size it ran at.
	Lines []UsageLine `json:"lines"`
	Name  string      `json:"name"`
	// Running time in the window, billed per second.
	Seconds uint64 `json:"seconds"`
	To      int64  `json:"to"`
}

// A saved snapshot of the account (`GET /saved-snapshots`): a template for `new --from NAME`. It outlives its source sandbox.
type SavedSnapshot struct {
	CreatedAt int64  `json:"created_at"`
	ID        string `json:"id"`
	Name      string `json:"name"`
	// A copy is kept off the host, on separate storage (encrypted).
	Offsite bool `json:"offsite,omitempty"`
	OS      Os   `json:"os"`
	// It came from another account's share code (`new --from fsh_…`); `source` is the code's id.
	Shared bool     `json:"shared,omitempty"`
	Size   Size     `json:"size"`
	SizeGB *float64 `json:"size_gb,omitempty"`
	// The sandbox it was saved from.
	Source string `json:"source"`
	// The source sandbox was deleted (the saved snapshot stays).
	SourceDeleted bool `json:"source_deleted"`
	// creating | available
	Status string `json:"status"`
}

// Saved snapshots the plan includes and how many the account keeps (`GET /limits`).
type SavedSnapshotAllowance struct {
	// Above the included ones, billed per day.
	Extra               uint64  `json:"extra"`
	Included            uint64  `json:"included"`
	PricePerExtraDayUSD float64 `json:"price_per_extra_day_usd"`
	// Saved snapshots (names) the account keeps now.
	Used uint64 `json:"used"`
}

// Which environment's sandboxes get which profile (`PUT /v1/ai/environments/{env}`).
type SetAiEnvironmentProfile struct {
	// A profile name or id; null: the account's default profile.
	Profile *string `json:"profile,omitempty"`
}

// `PUT /environment/files`
type SetEnvFile struct {
	Content string `json:"content"`
	Path    string `json:"path"`
}

type SetEnvVar struct {
	Value string `json:"value"`
}

// `PUT /v1/billing/extra-starts`.
type SetExtraStarts struct {
	Enabled bool `json:"enabled"`
}

// `PUT /sandboxes/{name}/ports`: the ports to open, replacing the ones opened before ("8080/tcp", "27015/udp"; a bare number is TCP). Public URLs' ports stay open regardless.
type SetPorts struct {
	Ports []string `json:"ports"`
}

// `PUT /v1/account/primary`: the connection whose address gets the account's emails.
type SetPrimary struct {
	// A connection id (`con_…`).
	Connection string `json:"connection"`
}

// `PUT /telemetry`: the collector's base URL (froog adds /v1/metrics and /v1/logs) and the headers your vendor needs, e.g. `{"dd-api-key": "…"}`.
type SetTelemetry struct {
	Endpoint string            `json:"endpoint"`
	Headers  map[string]string `json:"headers,omitempty"`
}

// `PUT /v1/github/token-repos`: limit the token in each sandbox to these repositories.
type SetTokenRepos struct {
	// `owner/name`; empty = every repository the app can see.
	Repos []string `json:"repos"`
}

// Machine sizes. Mapped to provider server types by the server's config.
type Size string

const (
	SizeSmall    Size = "small"
	SizeDefault  Size = "default"
	SizeLarge    Size = "large"
	SizeXlarge   Size = "xlarge"
	SizeStandard Size = "standard"
)

type Snapshot struct {
	CreatedAt int64   `json:"created_at"`
	ID        string  `json:"id"`
	Label     *string `json:"label,omitempty"`
	// A copy is kept off the host, on separate storage (encrypted), so the snapshot survives the loss of the machine. Usually true within minutes of the snapshot being taken.
	Offsite bool `json:"offsite,omitempty"`
	// The sandbox it belongs to (in account-wide lists and single lookups).
	Sandbox *string `json:"sandbox,omitempty"`
	// Saved snapshots are kept until deleted and billed per day; automatic ones are free.
	Saved  bool     `json:"saved"`
	SizeGB *float64 `json:"size_gb,omitempty"`
	Status string   `json:"status"`
}

// A share code of a saved snapshot, as listed (`GET /saved-snapshots/{name}/shares`). The code itself is shown only once, when it's made.
type SnapshotShare struct {
	CreatedAt int64 `json:"created_at"`
	// `shr_…`
	ID         string `json:"id"`
	LastUsedAt *int64 `json:"last_used_at,omitempty"`
	// The saved snapshot's name.
	Name string `json:"name"`
	// Set once revoked: the code creates nothing any more.
	RevokedAt *int64 `json:"revoked_at,omitempty"`
	// Sandboxes created from it so far.
	Uses uint32 `json:"uses"`
}

type SshInfo struct {
	Host string `json:"host"`
	// OpenSSH public host key line, to pin in known_hosts.
	HostKey string `json:"host_key"`
	Port    uint16 `json:"port"`
	User    string `json:"user"`
}

type SshKey struct {
	CreatedAt int64  `json:"created_at"`
	ID        string `json:"id"`
	Name      string `json:"name"`
	PublicKey string `json:"public_key"`
}

// Starts up to `up_to` times the included number cost `price_usd` each.
type StartBracket struct {
	PriceUSD float64 `json:"price_usd"`
	UpTo     float64 `json:"up_to"`
}

// Sandbox starts (create, start, fork, restore) allowed per window; 0 = no limit.
type StartLimits struct {
	PerDay     uint32 `json:"per_day"`
	PerHour    uint32 `json:"per_hour"`
	PerMinute  uint32 `json:"per_minute"`
	UsedDay    uint32 `json:"used_day"`
	UsedHour   uint32 `json:"used_hour"`
	UsedMinute uint32 `json:"used_minute"`
}

// Optional body of `POST /sandboxes/{name}/start`.
type StartSandbox struct {
	// Environment variables set on top of the sandbox's own (these win; kept).
	Env map[string]string `json:"env,omitempty"`
	// false: boot without the account's environment (env vars, repos, secret files, setup script, in-sandbox CLI key); true: with it. Default: as before. Kept for later starts.
	Environment *bool `json:"environment,omitempty"`
	// Move it to this environment's latest version (it also gets the environment again).
	EnvironmentName *string `json:"environment_name,omitempty"`
	// Start at a different size.
	Size *Size `json:"size,omitempty"`
	// A new auto-stop timer in seconds (0 = no auto-stop). Default: keep it.
	TTLSecs *uint64 `json:"ttl_secs,omitempty"`
}

type StartSubscriptionLogin struct {
	// A running sandbox to run the sign-in in (it runs `claude setup-token` or `codex login --device-auth` there; nothing is left behind).
	Sandbox string `json:"sandbox"`
}

type State string

const (
	StateCreating  State = "creating"
	StateRunning   State = "running"
	StateStopping  State = "stopping"
	StateStopped   State = "stopped"
	StateStarting  State = "starting"
	StateRestoring State = "restoring"
	StateDeleting  State = "deleting"
	StateDeleted   State = "deleted"
	StateError     State = "error"
)

type SteerRequest struct {
	// The conversation to steer (default: the sandbox's current one).
	ConversationID *string `json:"conversation_id,omitempty"`
	Message        string  `json:"message"`
}

type SteerResult struct {
	ConversationID string `json:"conversation_id"`
	// `native`: added to the running turn. `queued`: the harness can't take a message mid-turn, so it runs as the next turn of the same conversation (`queued_prompt_id`).
	Mode string `json:"mode"`
	// The harness took the message into the running turn.
	Native bool `json:"native"`
	// The running prompt the message went to.
	PromptID       string  `json:"prompt_id"`
	QueuedPromptID *string `json:"queued_prompt_id,omitempty"`
}

// A stop (`GET /stops/{id}`): `POST /sandboxes/{name}/stop` returns its id, and an idle or billing stop gets one too. Stopping again while one is pending returns the same id.
type Stop struct {
	EndedAt *int64  `json:"ended_at,omitempty"`
	Error   *string `json:"error,omitempty"`
	// `stop_…`
	ID string `json:"id"`
	// Unix seconds.
	RequestedAt int64 `json:"requested_at"`
	// The sandbox's name.
	Sandbox string `json:"sandbox"`
	// pending (snapshotting and shutting down) | failing (its snapshot failed, see `error`: the sandbox keeps running, unbilled, and froog retries this stop) | completed (stopped, or erased when it had snapshots off) | superseded (a start cancelled a failing stop) | failed (see `error`; the sandbox is in the error state, a start retries from its last snapshot).
	Status string `json:"status"`
}

// Optional body of `POST /sandboxes/{name}/stop`.
type StopSandbox struct {
	// Stop even when the stop snapshot fails: the sandbox then keeps its last good snapshot and loses what was written after it. Skips `~/.froog/pre-stop` and everything that needs the sandbox to answer, so it also stops one that's hung.
	Force bool `json:"force,omitempty"`
}

// A subscription sign-in (Claude Pro/Max for Claude Code, ChatGPT for Codex).
type Subscription struct {
	// The harness it signs in.
	Agent       string `json:"agent"`
	Connected   bool   `json:"connected"`
	ConnectedAt *int64 `json:"connected_at,omitempty"`
	// A sign-in in progress.
	Login *SubscriptionLogin `json:"login,omitempty"`
	// claude | chatgpt
	Provider string `json:"provider"`
	Title    string `json:"title"`
}

type SubscriptionCode struct {
	Code string `json:"code"`
}

// A sign-in in progress: open `url`, then (Claude) paste the code shown back with `POST …/login/code`, or (ChatGPT) enter `user_code` on that page.
type SubscriptionLogin struct {
	Error *string `json:"error,omitempty"`
	// Paste the code the vendor's page shows into `POST …/login/code`.
	NeedsCode bool   `json:"needs_code"`
	Provider  string `json:"provider"`
	// The sandbox the vendor's own CLI runs the sign-in in.
	Sandbox   string `json:"sandbox"`
	StartedAt int64  `json:"started_at"`
	// starting | waiting_for_user | connected | failed
	Status   string  `json:"status"`
	URL      *string `json:"url,omitempty"`
	UserCode *string `json:"user_code,omitempty"`
}

// `GET /telemetry`: the account's OpenTelemetry endpoint (header values are never returned).
type Telemetry struct {
	Endpoint string `json:"endpoint"`
	// Header names only.
	Headers      []string `json:"headers"`
	LastError    *string  `json:"last_error,omitempty"`
	LastExportAt *int64   `json:"last_export_at,omitempty"`
}

type TelemetrySaved struct {
	Telemetry Telemetry     `json:"telemetry"`
	Test      TelemetryTest `json:"test"`
}

// The collector's answer to the test export sent on save.
type TelemetryTest struct {
	Message *string `json:"message,omitempty"`
	Ok      bool    `json:"ok"`
	Status  *uint16 `json:"status,omitempty"`
}

type Trial struct {
	// In the trial now. Before choosing a plan this is false and `ends_at` None: the trial starts with the first plan.
	Active bool `json:"active"`
	// The card already had a free trial on another account (`trial_blocked`): the plan is paid from the start.
	Blocked bool   `json:"blocked,omitempty"`
	Days    uint64 `json:"days"`
	// Unix seconds; None until the trial starts.
	EndsAt *int64 `json:"ends_at,omitempty"`
	// Hours of running time the trial includes (all sandboxes together).
	Hours uint64 `json:"hours,omitempty"`
	// Hours used so far.
	HoursUsed float64 `json:"hours_used,omitempty"`
	// The longest auto-stop timer during the trial, in seconds (also its default): the trial needs one (`trial_auto_stop_required`). It can't save new saved snapshots either.
	MaxAutoStopSecs uint64 `json:"max_auto_stop_secs,omitempty"`
	// Sandboxes at once during the trial.
	MaxSandboxes uint32 `json:"max_sandboxes"`
}

type UpdateAiProfile struct {
	// true: make it the account's default profile.
	Default *bool `json:"default,omitempty"`
	// Rename.
	Name *string `json:"name,omitempty"`
}

// Change the proxy settings (`PUT /v1/ai/proxy`); fields left out stay.
type UpdateAiProxy struct {
	Blocked               *bool `json:"blocked,omitempty"`
	Enabled               *bool `json:"enabled,omitempty"`
	WithholdSubscriptions *bool `json:"withhold_subscriptions,omitempty"`
}

// `PATCH /v1/account/alerts`: `{"emails": {"plan_nudge": false}, "auto_rebuild": true}`; kinds left out stay as they are.
type UpdateAlertSettings struct {
	AutoRebuild *bool           `json:"auto_rebuild,omitempty"`
	Emails      map[string]bool `json:"emails,omitempty"`
}

type UpdateDataRetention struct {
	// Turning zero data retention on erases the data of every stopped sandbox: send "delete stopped sandbox data" to confirm.
	Confirmation      *string `json:"confirmation,omitempty"`
	MonitoringOff     *bool   `json:"monitoring_off,omitempty"`
	SnapshotsOff      *bool   `json:"snapshots_off,omitempty"`
	ZeroDataRetention *bool   `json:"zero_data_retention,omitempty"`
}

// `PATCH /environment`: fields left out stay as they are; `setup: ""` removes the script.
type UpdateEnvironment struct {
	// true: make it the default (the one new sandboxes use when they name none).
	Default *bool `json:"default,omitempty"`
	// Rename it (sandboxes stay on their versions).
	Name *string `json:"name,omitempty"`
	// Setup scripts per repo directory; `""` removes one. Scripts not named stay.
	RepoSetup map[string]string `json:"repo_setup,omitempty"`
	Repos     []string          `json:"repos,omitempty"`
	Setup     *string           `json:"setup,omitempty"`
}

// `PATCH /v1/inboxes/{inbox}`: rename it, or attach it to a sandbox (`""` detaches it).
type UpdateInbox struct {
	Name    *string `json:"name,omitempty"`
	Sandbox *string `json:"sandbox,omitempty"`
}

// `PATCH /sandboxes/{name}`: rename, change the auto-stop timer, or change labels.
type UpdateSandbox struct {
	// true: while stopped, a request to one of its public URLs, or an exec, file, job or wait call, starts it; false: off.
	AutoStart *bool `json:"auto_start,omitempty"`
	// Delete it after staying stopped this many seconds (0 = never).
	DeleteAfterSecs *uint64 `json:"delete_after_secs,omitempty"`
	// Delete it this many seconds from now, running or not (60 s to 365 days); 0 removes the deadline.
	ExpiresInSecs *uint64 `json:"expires_in_secs,omitempty"`
	// Labels to set; a null value removes that label. Labels not named stay.
	Labels map[string]string `json:"labels,omitempty"`
	// Stop it this many seconds after each start (0 = no limit). Set while it runs, it counts from that start.
	MaxLifetimeSecs *uint64 `json:"max_lifetime_secs,omitempty"`
	Name            *string `json:"name,omitempty"`
	// A new auto-stop timer in seconds (0 = no auto-stop). While running, the sandbox then stops this long from now (`froog extend --hours`); later starts use it too.
	TTLSecs *uint64 `json:"ttl_secs,omitempty"`
}

// Change a sandbox's AI settings (`PUT /v1/sandboxes/{name}/ai`).
type UpdateSandboxAi struct {
	// Refuse (true) or accept again (false) its AI requests, at once. Blocking needs `sandbox.update`; unblocking needs `environment.write` or `account.admin` on a key not limited to some sandboxes (never the in-sandbox key), and an owner or admin in an organization.
	Blocked *bool `json:"blocked,omitempty"`
	// A profile name or id; "" (empty) goes back to its environment's. Proxied requests use it at once; the variables the sandbox gets change at its next start. Changing it needs what unblocking needs.
	Profile *string `json:"profile,omitempty"`
}

// `PATCH /webhooks/{id}`: change what's given. The signing secret stays (rotate changes it).
type UpdateWebhook struct {
	Enabled *bool `json:"enabled,omitempty"`
	// Empty = every event.
	Events []string `json:"events,omitempty"`
	// An empty name removes it.
	Name *string `json:"name,omitempty"`
	URL  *string `json:"url,omitempty"`
}

// Optional body of `POST /environments/{env}/upgrade`.
type UpgradeEnvironment struct {
	// Only these sandboxes (names). Default: every sandbox of the environment on an older version.
	Sandboxes []string `json:"sandboxes,omitempty"`
}

// A viewer token of a private URL (`GET /sandboxes/{name}/urls/{port}/tokens`).
type UrlToken struct {
	CreatedAt int64 `json:"created_at"`
	ExpiresAt int64 `json:"expires_at"`
	// `utk_…`, for revoking it.
	ID    string  `json:"id"`
	Label *string `json:"label,omitempty"`
	// Only when created: the secret (`pvt_…`). Send it as `?_token=` (the proxy turns it into a cookie) or in the `X-Froog-Url-Token` header.
	Token *string `json:"token,omitempty"`
	// Only when created: the URL with `?_token=` for this token.
	URL *string `json:"url,omitempty"`
}

type Usage struct {
	// Usage past the plan, taken from the wallet.
	BillableUSD float64 `json:"billable_usd"`
	// The period is the free trial: nothing is charged before this time (unix seconds).
	FreeTrialUntil *int64 `json:"free_trial_until,omitempty"`
	// The period, unix seconds: the current billing period (or the trial), or the calendar month asked for. `to` is now for a period still running.
	From int64 `json:"from,omitempty"`
	// What the plan paid for this period covers.
	IncludedUSD float64     `json:"included_usd,omitempty"`
	Lines       []UsageLine `json:"lines"`
	// "YYYY-MM" of the period's start.
	Month             string  `json:"month"`
	SavedSnapshotDays float64 `json:"saved_snapshot_days"`
	SnapshotCostUSD   float64 `json:"snapshot_cost_usd"`
	To                int64   `json:"to,omitempty"`
	// Everything used in the period, at list prices.
	TotalUSD float64 `json:"total_usd"`
}

type UsageLine struct {
	// `froog`: a sandbox's running time. `ci`: CI job time (`runs-on: froog`), priced per vCPU minute.
	Backend string  `json:"backend"`
	CostUSD float64 `json:"cost_usd"`
	// Running time (or CI job time) in the period, billed per second.
	Seconds uint64 `json:"seconds"`
	Size    Size   `json:"size"`
}

// `POST /sandboxes/{name}/wait`: wait until something listens on a TCP port inside the sandbox.
type WaitForPort struct {
	Port uint16 `json:"port"`
	// Give up after this many seconds (1-600, default 60). Time spent starting counts.
	TimeoutSecs *uint64 `json:"timeout_secs,omitempty"`
}

type Waitlist struct {
	Email string  `json:"email"`
	Note  *string `json:"note,omitempty"`
}

// The wallet bonus: none below `from_usd`; from `from_percent` there, growing evenly on a log scale to `to_percent` at `to_usd`, and `to_percent` above that.
type WalletBonus struct {
	// What some amounts get.
	Examples    []WalletQuote `json:"examples"`
	FromPercent float64       `json:"from_percent"`
	FromUSD     uint32        `json:"from_usd"`
	ToPercent   float64       `json:"to_percent"`
	ToUSD       uint32        `json:"to_usd"`
}

// `POST /v1/billing/wallet/pay`: charge the saved card and add the money to the wallet.
type WalletPay struct {
	// Whole dollars, from `Billing.min_topup_usd` to `Billing.max_wallet_per_day_usd` (also the most charged in any 24 hours).
	AmountUSD uint32 `json:"amount_usd"`
}

// `GET /v1/billing/wallet/quote?amount_usd=N`: what a payment of N dollars puts in the wallet.
type WalletQuote struct {
	AmountUSD    uint32  `json:"amount_usd"`
	BonusPercent float64 `json:"bonus_percent"`
	BonusUSD     float64 `json:"bonus_usd"`
	CreditedUSD  float64 `json:"credited_usd"`
}

// A webhook endpoint (the signing secret is shown only when created or rotated).
type Webhook struct {
	CreatedAt int64 `json:"created_at"`
	// false: paused, nothing is sent (test pings still are).
	Enabled bool `json:"enabled,omitempty"`
	// Empty = every event.
	Events []string `json:"events"`
	ID     string   `json:"id"`
	// Your own name for it (up to 100 characters).
	Name      *string `json:"name,omitempty"`
	UpdatedAt *int64  `json:"updated_at,omitempty"`
	URL       string  `json:"url"`
}

// One delivery attempt of a webhook (`GET /webhooks/{id}/deliveries`, kept 30 days).
type WebhookDelivery struct {
	// 1-8 (`froog-attempt`).
	Attempt uint32 `json:"attempt"`
	// Unix seconds.
	CreatedAt  int64  `json:"created_at"`
	DurationMs uint64 `json:"duration_ms"`
	// What went wrong (a connection error, a non-2xx answer).
	Error *string `json:"error,omitempty"`
	// sandbox.running, …, or ping.
	Event string `json:"event"`
	// The event id (`evt_…`, also the `froog-delivery` header): the same on every attempt.
	EventID string `json:"event_id"`
	// When the next attempt is due, if one is.
	NextAttemptAt *int64 `json:"next_attempt_at,omitempty"`
	// The endpoint's HTTP status; none when it couldn't be reached.
	ResponseCode *uint16 `json:"response_code,omitempty"`
	// The sandbox it was about (none for a ping).
	Sandbox *string `json:"sandbox,omitempty"`
	// delivered | failed (another attempt follows unless this was the last).
	Status string `json:"status"`
}

type WebhookSecret struct {
	Events []string `json:"events"`
	ID     string   `json:"id"`
	Name   *string  `json:"name,omitempty"`
	// Verify `froog-signature` with this; shown once.
	Secret string `json:"secret"`
	URL    string `json:"url"`
}

// Error is what every failed call returns: the HTTP status, the API's error code and message, and the request id
// (quote it when reporting a problem). Use errors.As(err, &e) with e *froog.Error.
type Error struct {
	Status    int
	Code      string
	Message   string
	RequestID string
}

func (e *Error) Error() string { return e.Code + ": " + e.Message }

// IsNotFound reports whether err is a 404 from the API.
func IsNotFound(err error) bool {
	var e *Error
	return errors.As(err, &e) && e.Status == http.StatusNotFound
}

// Client calls the froog API. Its zero value isn't usable; make one with New.
type Client struct {
	// An API key (froog_…).
	APIKey string
	// Default https://api.froogcorp.com, or FROOG_API_URL.
	BaseURL string
	// The organization requests act in (id or name, or "personal"), sent as X-Froog-Org.
	// Default FROOG_ORG; empty: the active one of the key's account.
	Org string
	// Default: a client without a timeout (use the context).
	HTTPClient *http.Client
	// Sandboxes as objects: c.Sandboxes.Create(ctx, ...), c.Sandboxes.Get(ctx, name).
	Sandboxes SandboxService
}

// New makes a client. An empty apiKey means FROOG_API_KEY from the environment.
func New(apiKey string) *Client {
	if apiKey == "" {
		apiKey = os.Getenv("FROOG_API_KEY")
	}
	base := os.Getenv("FROOG_API_URL")
	if base == "" {
		base = "https://api.froogcorp.com"
	}
	c := &Client{APIKey: apiKey, BaseURL: strings.TrimRight(base, "/"), Org: os.Getenv("FROOG_ORG"), HTTPClient: &http.Client{}}
	c.Sandboxes = SandboxService{c: c}
	return c
}

type request struct {
	query   url.Values
	json    any
	raw     io.Reader
	noAuth  bool
	idemKey string
	accept  string
}

func (c *Client) send(ctx context.Context, method, path string, r request) (*http.Response, error) {
	u := strings.TrimRight(c.BaseURL, "/") + path
	if len(r.query) > 0 {
		u += "?" + r.query.Encode()
	}
	var body io.Reader
	contentType := ""
	if r.json != nil {
		b, err := json.Marshal(r.json)
		if err != nil {
			return nil, err
		}
		body, contentType = bytes.NewReader(b), "application/json"
	} else if r.raw != nil {
		body, contentType = r.raw, "application/octet-stream"
	}
	req, err := http.NewRequestWithContext(ctx, method, u, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "froog-go")
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}
	if r.accept != "" {
		req.Header.Set("Accept", r.accept)
	}
	if !r.noAuth && c.APIKey != "" {
		req.Header.Set("Authorization", "Bearer "+c.APIKey)
	}
	if r.idemKey != "" {
		req.Header.Set("Idempotency-Key", r.idemKey)
	}
	if c.Org != "" {
		req.Header.Set("X-Froog-Org", c.Org)
	}
	hc := c.HTTPClient
	if hc == nil {
		hc = http.DefaultClient
	}
	res, err := hc.Do(req)
	if err != nil {
		return nil, err
	}
	if res.StatusCode >= 300 {
		defer res.Body.Close()
		e := &Error{Status: res.StatusCode, Code: fmt.Sprintf("http_%d", res.StatusCode), Message: http.StatusText(res.StatusCode), RequestID: res.Header.Get("X-Request-Id")}
		var eb ErrorBody
		if b, err := io.ReadAll(res.Body); err == nil && json.Unmarshal(b, &eb) == nil && eb.Error.Code != "" {
			e.Code, e.Message = eb.Error.Code, eb.Error.Message
			if eb.Error.RequestID != nil && *eb.Error.RequestID != "" {
				e.RequestID = *eb.Error.RequestID
			}
		}
		return nil, e
	}
	return res, nil
}

// do sends a request and decodes a JSON answer into out (when out isn't nil), or returns the raw body.
func (c *Client) do(ctx context.Context, method, path string, r request, out any) ([]byte, error) {
	res, err := c.send(ctx, method, path, r)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	b, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	if out != nil && len(bytes.TrimSpace(b)) > 0 {
		if err := json.Unmarshal(b, out); err != nil {
			return nil, err
		}
	}
	return b, nil
}

func setQuery(q url.Values, k, v string) {
	if v != "" {
		q.Set(k, v)
	}
}

// ExecStream runs a command and calls fn with each event as output arrives: "started", then "stdout"/"stderr" chunks
// (Data), then "exit" (ExitCode, TimedOut) or "error". Returning an error from fn stops reading (and the command).
func (c *Client) ExecStream(ctx context.Context, name string, body ExecRequest, fn func(ExecEvent) error) error {
	t := true
	body.Stream = &t
	res, err := c.send(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/exec", request{json: body, accept: "application/x-ndjson"})
	if err != nil {
		return err
	}
	defer res.Body.Close()
	rd := bufio.NewReader(res.Body)
	for {
		line, err := rd.ReadBytes('\n')
		if len(bytes.TrimSpace(line)) > 0 {
			var ev ExecEvent
			if jerr := json.Unmarshal(line, &ev); jerr != nil {
				return jerr
			}
			if ferr := fn(ev); ferr != nil {
				return ferr
			}
		}
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
	}
}

// streamEach sends a request answered with NDJSON and calls fn with each line, decoded, until the stream ends or fn
// returns an error.
func streamEach[T any](ctx context.Context, c *Client, method, path string, r request, fn func(T) error) error {
	res, err := c.send(ctx, method, path, r)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	rd := bufio.NewReader(res.Body)
	for {
		line, err := rd.ReadBytes('\n')
		if len(bytes.TrimSpace(line)) > 0 {
			var ev T
			if jerr := json.Unmarshal(line, &ev); jerr != nil {
				return jerr
			}
			if ferr := fn(ev); ferr != nil {
				return ferr
			}
		}
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
	}
}

// WaitUntil polls until the sandbox reaches state (e.g. StateRunning after create or start). timeout 0 means 10 minutes.
func (c *Client) WaitUntil(ctx context.Context, name string, state State, timeout time.Duration) (*Sandbox, error) {
	if timeout == 0 {
		timeout = 10 * time.Minute
	}
	until := time.Now().Add(timeout)
	for {
		sb, err := c.GetSandbox(ctx, name)
		if err != nil {
			return nil, err
		}
		if sb.State == state {
			return sb, nil
		}
		if sb.State == StateError {
			msg := "error"
			if sb.Error != nil {
				msg = *sb.Error
			}
			return sb, &Error{Status: 409, Code: "sandbox_error", Message: name + ": " + msg}
		}
		if time.Now().After(until) {
			return sb, &Error{Status: 408, Code: "timeout", Message: fmt.Sprintf("%s didn't become %s", name, state)}
		}
		if err := sleepCtx(ctx, pollInterval); err != nil {
			return nil, err
		}
	}
}

var pollInterval = 2 * time.Second

func sleepCtx(ctx context.Context, d time.Duration) error {
	t := time.NewTimer(d)
	defer t.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-t.C:
		return nil
	}
}

// Mcp: MCP server (Streamable HTTP, stateless): JSON-RPC 2.0 initialize, tools/list and tools/call, with tools to list, create, run commands in, fork, snapshot, stop and delete sandboxes, run background commands and read their output, read and write files and host ports. A scoped key may call only the tools its actions allow. An exec call with _meta.progressToken from a client that accepts text/event-stream is answered as server-sent events (output as notifications/progress, then the result). With FROOG_SERVER_MCP_OAUTH on, a 401 points at OAuth metadata (MCP authorization) and the access tokens are scoped API keys. `POST /mcp`
func (c *Client) Mcp(ctx context.Context, body JsonRpcRequest) (*JsonRpcResponse, error) {
	out := new(JsonRpcResponse)
	if _, err := c.do(ctx, "POST", "/mcp", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetAccount: The signed-in account. `GET /v1/account`
func (c *Client) GetAccount(ctx context.Context) (*Account, error) {
	out := new(Account)
	if _, err := c.do(ctx, "GET", "/v1/account", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetAlertSettings: Alerts settings: which emails the account gets, and automatic rebuilds. `GET /v1/account/alerts`
func (c *Client) GetAlertSettings(ctx context.Context) (*AlertSettings, error) {
	out := new(AlertSettings)
	if _, err := c.do(ctx, "GET", "/v1/account/alerts", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateAlertSettings: Turn kinds of email, or automatic rebuilds, on or off. `PATCH /v1/account/alerts`
func (c *Client) UpdateAlertSettings(ctx context.Context, body UpdateAlertSettings) (*AlertSettings, error) {
	out := new(AlertSettings)
	if _, err := c.do(ctx, "PATCH", "/v1/account/alerts", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// CloseAccount: Close the account: cancel the plan now, stop every sandbox, revoke every key; reopen within 30 days. Needs the header X-Froog-Confirm-Close set to the account's email, and a full-access key or a dashboard session. `POST /v1/account/close`
func (c *Client) CloseAccount(ctx context.Context) (*AccountClosing, error) {
	out := new(AccountClosing)
	if _, err := c.do(ctx, "POST", "/v1/account/close", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListConnections: Ways to sign in to the account (Google, GitHub, email, AgentID); the primary one's address gets the account's emails. `GET /v1/account/connections`
func (c *Client) ListConnections(ctx context.Context) ([]Connection, error) {
	var out []Connection
	if _, err := c.do(ctx, "GET", "/v1/account/connections", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// SendConnectionEmailCode: Send a 6-digit code to an address to add it as a sign-in connection. `POST /v1/account/connections/email`
func (c *Client) SendConnectionEmailCode(ctx context.Context, body EmailCodeRequest) error {
	_, err := c.do(ctx, "POST", "/v1/account/connections/email", request{json: body}, nil)
	return err
}

// VerifyConnectionEmailCode: Add the address with the code from the email. `POST /v1/account/connections/email/verify`
func (c *Client) VerifyConnectionEmailCode(ctx context.Context, body EmailCodeVerify) (*Connection, error) {
	out := new(Connection)
	if _, err := c.do(ctx, "POST", "/v1/account/connections/email/verify", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveConnection: Remove a sign-in connection (not the last one: 409 last_connection). `DELETE /v1/account/connections/{id}`
func (c *Client) RemoveConnection(ctx context.Context, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/account/connections/"+url.PathEscape(id), request{}, nil)
	return err
}

// GetDataRetention: Data controls: zero data retention, snapshots off, monitoring off. `GET /v1/account/data-retention`
func (c *Client) GetDataRetention(ctx context.Context) (*DataRetention, error) {
	out := new(DataRetention)
	if _, err := c.do(ctx, "GET", "/v1/account/data-retention", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateDataRetention: Change data controls (dashboard session only). `PATCH /v1/account/data-retention`
func (c *Client) UpdateDataRetention(ctx context.Context, body UpdateDataRetention) (*DataRetention, error) {
	out := new(DataRetention)
	if _, err := c.do(ctx, "PATCH", "/v1/account/data-retention", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SetPrimaryConnection: Make a connection primary: its address gets the account's emails. `PUT /v1/account/primary`
func (c *Client) SetPrimaryConnection(ctx context.Context, body SetPrimary) (*Connection, error) {
	out := new(Connection)
	if _, err := c.do(ctx, "PUT", "/v1/account/primary", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ReopenAccount: Reopen a closed account within its 30-day window (the plan isn't restored). `POST /v1/account/reopen`
func (c *Client) ReopenAccount(ctx context.Context) (*AccountClosing, error) {
	out := new(AccountClosing)
	if _, err := c.do(ctx, "POST", "/v1/account/reopen", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SetAiEnvironmentProfile: The credential profile an environment's sandboxes get. `PUT /v1/ai/environments/{env}`
func (c *Client) SetAiEnvironmentProfile(ctx context.Context, env string, body SetAiEnvironmentProfile) (*AiProfile, error) {
	out := new(AiProfile)
	if _, err := c.do(ctx, "PUT", "/v1/ai/environments/"+url.PathEscape(env), request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListAiProfiles: Credential profiles and their accounts (never the keys). `GET /v1/ai/profiles`
func (c *Client) ListAiProfiles(ctx context.Context) ([]AiProfile, error) {
	var out []AiProfile
	if _, err := c.do(ctx, "GET", "/v1/ai/profiles", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// CreateAiProfile: Create a credential profile, empty or as a copy. `POST /v1/ai/profiles`
func (c *Client) CreateAiProfile(ctx context.Context, body CreateAiProfile) (*AiProfile, error) {
	out := new(AiProfile)
	if _, err := c.do(ctx, "POST", "/v1/ai/profiles", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteAiProfile: Delete a profile (not the default, not one an environment uses). `DELETE /v1/ai/profiles/{profile}`
func (c *Client) DeleteAiProfile(ctx context.Context, profile string) error {
	_, err := c.do(ctx, "DELETE", "/v1/ai/profiles/"+url.PathEscape(profile), request{}, nil)
	return err
}

// GetAiProfile: One credential profile (name or id). `GET /v1/ai/profiles/{profile}`
func (c *Client) GetAiProfile(ctx context.Context, profile string) (*AiProfile, error) {
	out := new(AiProfile)
	if _, err := c.do(ctx, "GET", "/v1/ai/profiles/"+url.PathEscape(profile), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateAiProfile: Rename a profile, or make it the default. `PATCH /v1/ai/profiles/{profile}`
func (c *Client) UpdateAiProfile(ctx context.Context, profile string, body UpdateAiProfile) (*AiProfile, error) {
	out := new(AiProfile)
	if _, err := c.do(ctx, "PATCH", "/v1/ai/profiles/"+url.PathEscape(profile), request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// AddAiCredential: Add a provider key or custom API to a profile (another account when the provider has one). `POST /v1/ai/profiles/{profile}/credentials`
func (c *Client) AddAiCredential(ctx context.Context, profile string, body AddAiCredential) (*AiCredential, error) {
	out := new(AiCredential)
	if _, err := c.do(ctx, "POST", "/v1/ai/profiles/"+url.PathEscape(profile)+"/credentials", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveAiCredential: Remove a key from a profile. `DELETE /v1/ai/profiles/{profile}/credentials/{id}`
func (c *Client) RemoveAiCredential(ctx context.Context, profile string, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/ai/profiles/"+url.PathEscape(profile)+"/credentials/"+url.PathEscape(id), request{}, nil)
	return err
}

// SwitchAiCredential: Use this account first for its provider. `PUT /v1/ai/profiles/{profile}/credentials/{id}/active`
func (c *Client) SwitchAiCredential(ctx context.Context, profile string, id string) (*AiProfile, error) {
	out := new(AiProfile)
	if _, err := c.do(ctx, "PUT", "/v1/ai/profiles/"+url.PathEscape(profile)+"/credentials/"+url.PathEscape(id)+"/active", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetAiProxy: The AI proxy: on or off, blocked, subscriptions withheld, its URL. `GET /v1/ai/proxy`
func (c *Client) GetAiProxy(ctx context.Context) (*AiProxy, error) {
	out := new(AiProxy)
	if _, err := c.do(ctx, "GET", "/v1/ai/proxy", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateAiProxy: Turn the AI proxy on or off (sandboxes switch at their next start), block every proxied sandbox. `PUT /v1/ai/proxy`
func (c *Client) UpdateAiProxy(ctx context.Context, body UpdateAiProxy) (*AiProxy, error) {
	out := new(AiProxy)
	if _, err := c.do(ctx, "PUT", "/v1/ai/proxy", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetAiUsageParams holds the optional parameters of GetAiUsage.
type GetAiUsageParams struct {
	// 1-366 UTC days, default 30
	Days string
}

// GetAiUsage: Proxied AI usage per sandbox: requests, tokens, estimated cost. `GET /v1/ai/usage`
func (c *Client) GetAiUsage(ctx context.Context, params *GetAiUsageParams) (*AiUsage, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "days", params.Days)
	}
	out := new(AiUsage)
	if _, err := c.do(ctx, "GET", "/v1/ai/usage", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListApiKeys: API keys (secrets are never returned). `GET /v1/api-keys`
func (c *Client) ListApiKeys(ctx context.Context) ([]ApiKeyInfo, error) {
	var out []ApiKeyInfo
	if _, err := c.do(ctx, "GET", "/v1/api-keys", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// CreateApiKey: Create an API key: a preset or actions, sandboxes or labels, expiry. `POST /v1/api-keys`
func (c *Client) CreateApiKey(ctx context.Context, body NewApiKey) (*CreatedApiKey, error) {
	out := new(CreatedApiKey)
	if _, err := c.do(ctx, "POST", "/v1/api-keys", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RevokeApiKey: Revoke an API key. `DELETE /v1/api-keys/{id}`
func (c *Client) RevokeApiKey(ctx context.Context, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/api-keys/"+url.PathEscape(id), request{}, nil)
	return err
}

// RotateApiKey: A new secret for a key (same id, scope and expiry). `POST /v1/api-keys/{id}/rotate`
func (c *Client) RotateApiKey(ctx context.Context, id string) (*CreatedApiKey, error) {
	out := new(CreatedApiKey)
	if _, err := c.do(ctx, "POST", "/v1/api-keys/"+url.PathEscape(id)+"/rotate", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListAuditEventsParams holds the optional parameters of ListAuditEvents.
type ListAuditEventsParams struct {
	// exact actions, comma-separated (e.g. sandbox.delete,sandbox.auto_stop)
	Action string
	// entries about this sandbox
	Sandbox string
	// unix seconds
	Since string
	// unix seconds
	Until string
	// desc (newest first, the default) or asc
	Sort string
	// 1-200, default 50
	Limit string
	// next_cursor of the previous page
	Cursor string
}

// ListAuditEvents: The account's audit log, newest first (90 days): who created, deleted or changed what, from which IP. `GET /v1/audit`
func (c *Client) ListAuditEvents(ctx context.Context, params *ListAuditEventsParams) (*AuditList, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "action", params.Action)
		setQuery(q, "sandbox", params.Sandbox)
		setQuery(q, "since", params.Since)
		setQuery(q, "until", params.Until)
		setQuery(q, "sort", params.Sort)
		setQuery(q, "limit", params.Limit)
		setQuery(q, "cursor", params.Cursor)
	}
	out := new(AuditList)
	if _, err := c.do(ctx, "GET", "/v1/audit", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ExportAuditParams holds the optional parameters of ExportAudit.
type ExportAuditParams struct {
	// csv (default) or ndjson
	Format string
	// exact actions, comma-separated
	Action string
	// entries about this sandbox
	Sandbox string
	// unix seconds
	Since string
	// unix seconds
	Until string
	// asc (oldest first, the default) or desc
	Sort string
}

// ExportAudit: Every matching audit entry of the last 90 days as one CSV or NDJSON file. `GET /v1/audit/export`
func (c *Client) ExportAudit(ctx context.Context, params *ExportAuditParams) ([]byte, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "format", params.Format)
		setQuery(q, "action", params.Action)
		setQuery(q, "sandbox", params.Sandbox)
		setQuery(q, "since", params.Since)
		setQuery(q, "until", params.Until)
		setQuery(q, "sort", params.Sort)
	}
	return c.do(ctx, "GET", "/v1/audit/export", request{query: q}, nil)
}

// GetBilling: Plan, rates, this period's usage, wallet and auto top-up. `GET /v1/billing`
func (c *Client) GetBilling(ctx context.Context) (*Billing, error) {
	out := new(Billing)
	if _, err := c.do(ctx, "GET", "/v1/billing", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SetExtraStarts: Turn extra starts on or off: starts past the start limits paid from the wallet. `PUT /v1/billing/extra-starts`
func (c *Client) SetExtraStarts(ctx context.Context, body SetExtraStarts) (*Billing, error) {
	out := new(Billing)
	if _, err := c.do(ctx, "PUT", "/v1/billing/extra-starts", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// CancelPlanParams holds the optional parameters of CancelPlan.
type CancelPlanParams struct {
	// true: pause instead. When the plan ends every sandbox stops, and stopped disks and saved snapshots are kept free for 90 days
	KeepDisks string
}

// CancelPlan: Cancel the plan at the end of the period. `DELETE /v1/billing/plan`
func (c *Client) CancelPlan(ctx context.Context, params *CancelPlanParams) (*Billing, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "keep_disks", params.KeepDisks)
	}
	out := new(Billing)
	if _, err := c.do(ctx, "DELETE", "/v1/billing/plan", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ChoosePlan: Choose or change the plan (the first one returns a Stripe Checkout page). `POST /v1/billing/plan`
func (c *Client) ChoosePlan(ctx context.Context, body ChoosePlan) (*PlanChange, error) {
	out := new(PlanChange)
	if _, err := c.do(ctx, "POST", "/v1/billing/plan", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// BillingPortal: A Stripe page to change the card and download invoices. `POST /v1/billing/portal`
func (c *Client) BillingPortal(ctx context.Context) (*BillingUrl, error) {
	out := new(BillingUrl)
	if _, err := c.do(ctx, "POST", "/v1/billing/portal", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// EndTrial: End the free trial now and start the plan. `POST /v1/billing/trial/end`
func (c *Client) EndTrial(ctx context.Context) (*Billing, error) {
	out := new(Billing)
	if _, err := c.do(ctx, "POST", "/v1/billing/trial/end", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SetAutoTopup: Set auto top-up (0 = off). `PUT /v1/billing/wallet/auto-topup`
func (c *Client) SetAutoTopup(ctx context.Context, body AutoTopup) (*Billing, error) {
	out := new(Billing)
	if _, err := c.do(ctx, "PUT", "/v1/billing/wallet/auto-topup", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// WalletPay: Charge the saved card and add the money to the wallet. `POST /v1/billing/wallet/pay`
func (c *Client) WalletPay(ctx context.Context, body WalletPay) (*Billing, error) {
	out := new(Billing)
	if _, err := c.do(ctx, "POST", "/v1/billing/wallet/pay", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// WalletQuote: What a wallet payment adds, with its bonus. `GET /v1/billing/wallet/quote`
func (c *Client) WalletQuote(ctx context.Context, amountUsd string) (*WalletQuote, error) {
	q := url.Values{}
	q.Set("amount_usd", amountUsd)
	out := new(WalletQuote)
	if _, err := c.do(ctx, "GET", "/v1/billing/wallet/quote", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetCiOverviewParams holds the optional parameters of GetCiOverview.
type GetCiOverviewParams struct {
	// owner/name
	Repo string
	// all (default), active or failed
	State string
	// 1-200, default 50
	Limit string
}

// GetCiOverview: CI on froog: setup, 7-day stats and the last GitHub Actions jobs (runs-on: froog). `GET /v1/ci`
func (c *Client) GetCiOverview(ctx context.Context, params *GetCiOverviewParams) (*CiOverview, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "repo", params.Repo)
		setQuery(q, "state", params.State)
		setQuery(q, "limit", params.Limit)
	}
	out := new(CiOverview)
	if _, err := c.do(ctx, "GET", "/v1/ci", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetCiAnalyticsParams holds the optional parameters of GetCiAnalytics.
type GetCiAnalyticsParams struct {
	// 1-30, default 7
	Days string
	// owner/name
	Repo string
}

// GetCiAnalytics: CI analytics: per repository and job, runs, failure rate, p50/p90 duration and wait, cost, CPU and memory, a size advice, and flaky tests. `GET /v1/ci/analytics`
func (c *Client) GetCiAnalytics(ctx context.Context, params *GetCiAnalyticsParams) (*CiAnalytics, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "days", params.Days)
		setQuery(q, "repo", params.Repo)
	}
	out := new(CiAnalytics)
	if _, err := c.do(ctx, "GET", "/v1/ci/analytics", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetCiJob: One CI job: state, reason, wait, run time and sandbox. `GET /v1/ci/jobs/{id}`
func (c *Client) GetCiJob(ctx context.Context, id string) (*CiJob, error) {
	out := new(CiJob)
	if _, err := c.do(ctx, "GET", "/v1/ci/jobs/"+url.PathEscape(id), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetCiJobLog: One CI job's log from GitHub (text, the last 1 MiB). `GET /v1/ci/jobs/{id}/logs`
func (c *Client) GetCiJobLog(ctx context.Context, id string) ([]byte, error) {
	return c.do(ctx, "GET", "/v1/ci/jobs/"+url.PathEscape(id)+"/logs", request{}, nil)
}

// GetCiJobMetrics: One CI job's CPU and memory over time, and its steps with their times. `GET /v1/ci/jobs/{id}/metrics`
func (c *Client) GetCiJobMetrics(ctx context.Context, id string) (*CiJobMetrics, error) {
	out := new(CiJobMetrics)
	if _, err := c.do(ctx, "GET", "/v1/ci/jobs/"+url.PathEscape(id)+"/metrics", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetCiJobTests: One CI job's test results, from the JUnit XML files it wrote (failed first). `GET /v1/ci/jobs/{id}/tests`
func (c *Client) GetCiJobTests(ctx context.Context, id string) (*CiJobTests, error) {
	out := new(CiJobTests)
	if _, err := c.do(ctx, "GET", "/v1/ci/jobs/"+url.PathEscape(id)+"/tests", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SearchCiLogsParams holds the optional parameters of SearchCiLogs.
type SearchCiLogsParams struct {
	// words, "phrases", -excluded, repo: branch: workflow: job: conclusion: step: level:error|warning
	Q string
	// 1-30, default 7
	Days string
	// 1-500, default 100
	Limit string
	// newest (default) or oldest
	Order string
}

// SearchCiLogs: Search the kept logs of finished CI jobs, line by line. `GET /v1/ci/logs/search`
func (c *Client) SearchCiLogs(ctx context.Context, params *SearchCiLogsParams) (*CiLogSearch, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "q", params.Q)
		setQuery(q, "days", params.Days)
		setQuery(q, "limit", params.Limit)
		setQuery(q, "order", params.Order)
	}
	out := new(CiLogSearch)
	if _, err := c.do(ctx, "GET", "/v1/ci/logs/search", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListCiMonitors: CI monitors: alerts when matching jobs fail or run long, with their last alerts. `GET /v1/ci/monitors`
func (c *Client) ListCiMonitors(ctx context.Context) ([]CiMonitor, error) {
	var out []CiMonitor
	if _, err := c.do(ctx, "GET", "/v1/ci/monitors", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// CreateCiMonitor: Add a CI monitor (email and the ci.monitor.triggered webhook event). `POST /v1/ci/monitors`
func (c *Client) CreateCiMonitor(ctx context.Context, body CreateCiMonitor) (*CiMonitor, error) {
	out := new(CiMonitor)
	if _, err := c.do(ctx, "POST", "/v1/ci/monitors", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteCiMonitor: Remove a CI monitor. `DELETE /v1/ci/monitors/{id}`
func (c *Client) DeleteCiMonitor(ctx context.Context, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/ci/monitors/"+url.PathEscape(id), request{}, nil)
	return err
}

// ListCiRunsParams holds the optional parameters of ListCiRuns.
type ListCiRunsParams struct {
	// owner/name
	Repo string
	// all (default), active or failed
	State string
	// workflow name
	Workflow string
	// branch
	Branch string
	// words in the title, job names, workflow, repository, branch, commit or actor
	Q string
	// 1-100, default 25
	Limit string
	// runs to skip
	Offset string
}

// ListCiRuns: CI workflow runs, newest first, a page at a time: each run's GitHub title, event, number, actor and its jobs on froog, plus the total and the workflows, repositories and branches to filter by. `GET /v1/ci/runs`
func (c *Client) ListCiRuns(ctx context.Context, params *ListCiRunsParams) (*CiRunList, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "repo", params.Repo)
		setQuery(q, "state", params.State)
		setQuery(q, "workflow", params.Workflow)
		setQuery(q, "branch", params.Branch)
		setQuery(q, "q", params.Q)
		setQuery(q, "limit", params.Limit)
		setQuery(q, "offset", params.Offset)
	}
	out := new(CiRunList)
	if _, err := c.do(ctx, "GET", "/v1/ci/runs", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetCiRun: One CI workflow run (GitHub's run id) and its jobs on froog. `GET /v1/ci/runs/{run_id}`
func (c *Client) GetCiRun(ctx context.Context, runId string) (*CiRun, error) {
	out := new(CiRun)
	if _, err := c.do(ctx, "GET", "/v1/ci/runs/"+url.PathEscape(runId), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SetCiSettings: Let jobs from public repositories run, optionally only for listed GitHub users. `PUT /v1/ci/settings`
func (c *Client) SetCiSettings(ctx context.Context, body CiSettings) (*CiSettings, error) {
	out := new(CiSettings)
	if _, err := c.do(ctx, "PUT", "/v1/ci/settings", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// StartCliLogin: Start a browser login for the CLI. `POST /v1/cli-login`
func (c *Client) StartCliLogin(ctx context.Context, body CliLoginRequest) (*CliLoginStarted, error) {
	out := new(CliLoginStarted)
	if _, err := c.do(ctx, "POST", "/v1/cli-login", request{json: body, noAuth: true}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// PollCliLogin: Poll a CLI login (202 until approved). `POST /v1/cli-login/poll`
func (c *Client) PollCliLogin(ctx context.Context, body CliLoginPoll) (*CliLoginResult, error) {
	out := new(CliLoginResult)
	if _, err := c.do(ctx, "POST", "/v1/cli-login/poll", request{json: body, noAuth: true}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DenyCliLogin: Deny a CLI login. `DELETE /v1/cli-login/{code}`
func (c *Client) DenyCliLogin(ctx context.Context, code string) error {
	_, err := c.do(ctx, "DELETE", "/v1/cli-login/"+url.PathEscape(code), request{}, nil)
	return err
}

// GetCliLogin: Describe a pending CLI login. `GET /v1/cli-login/{code}`
func (c *Client) GetCliLogin(ctx context.Context, code string) (*CliLoginInfo, error) {
	out := new(CliLoginInfo)
	if _, err := c.do(ctx, "GET", "/v1/cli-login/"+url.PathEscape(code), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ApproveCliLogin: Approve a CLI login (creates a key). `POST /v1/cli-login/{code}/approve`
func (c *Client) ApproveCliLogin(ctx context.Context, code string) error {
	_, err := c.do(ctx, "POST", "/v1/cli-login/"+url.PathEscape(code)+"/approve", request{}, nil)
	return err
}

// CreateDashboardLink: A link that opens the dashboard signed in: single use, 15 minutes (full-access keys and sessions only). `POST /v1/dashboard-link`
func (c *Client) CreateDashboardLink(ctx context.Context) (*DashboardLink, error) {
	out := new(DashboardLink)
	if _, err := c.do(ctx, "POST", "/v1/dashboard-link", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetDeletionOperation: A deletion operation (dop_…): an account close or a zero data retention erasure. `GET /v1/deletion-operations/{id}`
func (c *Client) GetDeletionOperation(ctx context.Context, id string) (*DeletionOperation, error) {
	out := new(DeletionOperation)
	if _, err := c.do(ctx, "GET", "/v1/deletion-operations/"+url.PathEscape(id), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListEnv: Account environment variables (names and previews). `GET /v1/env`
func (c *Client) ListEnv(ctx context.Context) ([]EnvVar, error) {
	var out []EnvVar
	if _, err := c.do(ctx, "GET", "/v1/env", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveEnv: Remove an account environment variable. `DELETE /v1/env/{name}`
func (c *Client) RemoveEnv(ctx context.Context, name string) error {
	_, err := c.do(ctx, "DELETE", "/v1/env/"+url.PathEscape(name), request{}, nil)
	return err
}

// SetEnv: Set an account environment variable. `PUT /v1/env/{name}`
func (c *Client) SetEnv(ctx context.Context, name string, body SetEnvVar) error {
	_, err := c.do(ctx, "PUT", "/v1/env/"+url.PathEscape(name), request{json: body}, nil)
	return err
}

// GetEnvironment: Repos, secret files and setup script every new sandbox gets. `GET /v1/environment`
func (c *Client) GetEnvironment(ctx context.Context) (*Environment, error) {
	out := new(Environment)
	if _, err := c.do(ctx, "GET", "/v1/environment", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateEnvironment: Change repos or the setup script. `PATCH /v1/environment`
func (c *Client) UpdateEnvironment(ctx context.Context, body UpdateEnvironment) (*Environment, error) {
	out := new(Environment)
	if _, err := c.do(ctx, "PATCH", "/v1/environment", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveEnvironmentFile: Remove a secret file. `DELETE /v1/environment/files`
func (c *Client) RemoveEnvironmentFile(ctx context.Context, path string) error {
	q := url.Values{}
	q.Set("path", path)
	_, err := c.do(ctx, "DELETE", "/v1/environment/files", request{query: q}, nil)
	return err
}

// SetEnvironmentFile: Write a secret file. `PUT /v1/environment/files`
func (c *Client) SetEnvironmentFile(ctx context.Context, body SetEnvFile) error {
	_, err := c.do(ctx, "PUT", "/v1/environment/files", request{json: body}, nil)
	return err
}

// ListEnvironments: Named environments: default marker, latest version, sandboxes per version. `GET /v1/environments`
func (c *Client) ListEnvironments(ctx context.Context) ([]EnvironmentSummary, error) {
	var out []EnvironmentSummary
	if _, err := c.do(ctx, "GET", "/v1/environments", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// CreateEnvironment: Create an environment (empty, or a copy of another's latest version). `POST /v1/environments`
func (c *Client) CreateEnvironment(ctx context.Context, body NewEnvironment) (*Environment, error) {
	out := new(Environment)
	if _, err := c.do(ctx, "POST", "/v1/environments", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteEnvironment: Delete an environment (its sandboxes keep their version; not the default). `DELETE /v1/environments/{env}`
func (c *Client) DeleteEnvironment(ctx context.Context, env string) error {
	_, err := c.do(ctx, "DELETE", "/v1/environments/"+url.PathEscape(env), request{}, nil)
	return err
}

// GetNamedEnvironment: One environment: repos, setup scripts, secret files (names and sizes), latest version. `GET /v1/environments/{env}`
func (c *Client) GetNamedEnvironment(ctx context.Context, env string) (*Environment, error) {
	out := new(Environment)
	if _, err := c.do(ctx, "GET", "/v1/environments/"+url.PathEscape(env), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateNamedEnvironment: Change repos or setup scripts (a new version), rename, or make it the default. `PATCH /v1/environments/{env}`
func (c *Client) UpdateNamedEnvironment(ctx context.Context, env string, body UpdateEnvironment) (*Environment, error) {
	out := new(Environment)
	if _, err := c.do(ctx, "PATCH", "/v1/environments/"+url.PathEscape(env), request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveNamedEnvironmentFile: Remove a secret file (a new version). `DELETE /v1/environments/{env}/files`
func (c *Client) RemoveNamedEnvironmentFile(ctx context.Context, env string, path string) error {
	q := url.Values{}
	q.Set("path", path)
	_, err := c.do(ctx, "DELETE", "/v1/environments/"+url.PathEscape(env)+"/files", request{query: q}, nil)
	return err
}

// SetNamedEnvironmentFile: Write a secret file (a new version). `PUT /v1/environments/{env}/files`
func (c *Client) SetNamedEnvironmentFile(ctx context.Context, env string, body SetEnvFile) error {
	_, err := c.do(ctx, "PUT", "/v1/environments/"+url.PathEscape(env)+"/files", request{json: body}, nil)
	return err
}

// UpgradeEnvironment: Move its sandboxes to the latest version (running ones now, stopped ones at their next start). `POST /v1/environments/{env}/upgrade`
func (c *Client) UpgradeEnvironment(ctx context.Context, env string, body UpgradeEnvironment) (*EnvironmentUpgrade, error) {
	out := new(EnvironmentUpgrade)
	if _, err := c.do(ctx, "POST", "/v1/environments/"+url.PathEscape(env)+"/upgrade", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListEnvironmentVersions: An environment's versions, newest first, with sandboxes on each. `GET /v1/environments/{env}/versions`
func (c *Client) ListEnvironmentVersions(ctx context.Context, env string) ([]EnvironmentVersion, error) {
	var out []EnvironmentVersion
	if _, err := c.do(ctx, "GET", "/v1/environments/"+url.PathEscape(env)+"/versions", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// SendFeedback: Send feedback to the froog team. `POST /v1/feedback`
func (c *Client) SendFeedback(ctx context.Context, body Feedback) error {
	_, err := c.do(ctx, "POST", "/v1/feedback", request{json: body}, nil)
	return err
}

// DownloadSigned: Download through a signed link. `GET /v1/file-urls/{token}`
func (c *Client) DownloadSigned(ctx context.Context, token string) ([]byte, error) {
	return c.do(ctx, "GET", "/v1/file-urls/"+url.PathEscape(token), request{noAuth: true}, nil)
}

// UploadSigned: Upload through a signed link (raw bytes). `PUT /v1/file-urls/{token}`
func (c *Client) UploadSigned(ctx context.Context, token string, data io.Reader) error {
	_, err := c.do(ctx, "PUT", "/v1/file-urls/"+url.PathEscape(token), request{raw: data, noAuth: true}, nil)
	return err
}

// GetGithub: The GitHub connection: linked installations and the repositories the sandbox token is limited to. `GET /v1/github`
func (c *Client) GetGithub(ctx context.Context) (*GithubStatus, error) {
	out := new(GithubStatus)
	if _, err := c.do(ctx, "GET", "/v1/github", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ConnectGithub: A link that installs the froog GitHub App on a GitHub account or organization. `POST /v1/github/connect`
func (c *Client) ConnectGithub(ctx context.Context) (*GithubConnect, error) {
	out := new(GithubConnect)
	if _, err := c.do(ctx, "POST", "/v1/github/connect", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DisconnectGithubInstallation: Unlink a GitHub installation from the account. `DELETE /v1/github/installations/{id}`
func (c *Client) DisconnectGithubInstallation(ctx context.Context, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/github/installations/"+url.PathEscape(id), request{}, nil)
	return err
}

// ListGithubRepos: Repositories the linked installations can see. `GET /v1/github/repos`
func (c *Client) ListGithubRepos(ctx context.Context) ([]GithubRepo, error) {
	var out []GithubRepo
	if _, err := c.do(ctx, "GET", "/v1/github/repos", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// SetGithubTokenRepos: Limit the GitHub token in each sandbox to these repositories (empty: all the app can see). `PUT /v1/github/token-repos`
func (c *Client) SetGithubTokenRepos(ctx context.Context, body SetTokenRepos) (*GithubStatus, error) {
	out := new(GithubStatus)
	if _, err := c.do(ctx, "PUT", "/v1/github/token-repos", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListImages: Operating systems a sandbox can run. `GET /v1/images`
func (c *Client) ListImages(ctx context.Context) ([]OsImage, error) {
	var out []OsImage
	if _, err := c.do(ctx, "GET", "/v1/images", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListInboxes: Your agent inboxes. `GET /v1/inboxes`
func (c *Client) ListInboxes(ctx context.Context) ([]Inbox, error) {
	var out []Inbox
	if _, err := c.do(ctx, "GET", "/v1/inboxes", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// CreateInbox: Make an inbox ($2 a month): an email address that receives mail for an agent. Uses a paid seat, or adds one; 402 inbox_payment_required without an inbox subscription. `POST /v1/inboxes`
func (c *Client) CreateInbox(ctx context.Context, body CreateInbox) (*Inbox, error) {
	out := new(Inbox)
	if _, err := c.do(ctx, "POST", "/v1/inboxes", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetInboxBilling: What you pay for inboxes: seats paid for, inboxes used, the subscription's status. `GET /v1/inboxes/billing`
func (c *Client) GetInboxBilling(ctx context.Context) (*InboxBilling, error) {
	out := new(InboxBilling)
	if _, err := c.do(ctx, "GET", "/v1/inboxes/billing", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// BuyInboxes: A Stripe Checkout page that buys inboxes (default: yours plus one). `POST /v1/inboxes/billing/checkout`
func (c *Client) BuyInboxes(ctx context.Context, body InboxCheckout) (*BillingUrl, error) {
	out := new(BillingUrl)
	if _, err := c.do(ctx, "POST", "/v1/inboxes/billing/checkout", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteInbox: Delete an inbox and its messages (the address is never given out again; its seat stays paid until the period ends). `DELETE /v1/inboxes/{inbox}`
func (c *Client) DeleteInbox(ctx context.Context, inbox string) error {
	_, err := c.do(ctx, "DELETE", "/v1/inboxes/"+url.PathEscape(inbox), request{}, nil)
	return err
}

// GetInbox: One inbox (by id, address, or the part before @). `GET /v1/inboxes/{inbox}`
func (c *Client) GetInbox(ctx context.Context, inbox string) (*Inbox, error) {
	out := new(Inbox)
	if _, err := c.do(ctx, "GET", "/v1/inboxes/"+url.PathEscape(inbox), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateInbox: Rename an inbox, or attach it to a sandbox ("" detaches it). `PATCH /v1/inboxes/{inbox}`
func (c *Client) UpdateInbox(ctx context.Context, inbox string, body UpdateInbox) (*Inbox, error) {
	out := new(Inbox)
	if _, err := c.do(ctx, "PATCH", "/v1/inboxes/"+url.PathEscape(inbox), request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListInboxMessagesParams holds the optional parameters of ListInboxMessages.
type ListInboxMessagesParams struct {
	// unix seconds: only messages received at or after
	Since string
	// default 50, at most 100
	Limit string
}

// ListInboxMessages: Received messages, newest first, without their bodies. `GET /v1/inboxes/{inbox}/messages`
func (c *Client) ListInboxMessages(ctx context.Context, inbox string, params *ListInboxMessagesParams) (*InboxMessageList, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "since", params.Since)
		setQuery(q, "limit", params.Limit)
	}
	out := new(InboxMessageList)
	if _, err := c.do(ctx, "GET", "/v1/inboxes/"+url.PathEscape(inbox)+"/messages", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteInboxMessage: Delete a message. `DELETE /v1/inboxes/{inbox}/messages/{id}`
func (c *Client) DeleteInboxMessage(ctx context.Context, inbox string, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/inboxes/"+url.PathEscape(inbox)+"/messages/"+url.PathEscape(id), request{}, nil)
	return err
}

// GetInboxMessage: One message with its text, HTML, codes and links. `GET /v1/inboxes/{inbox}/messages/{id}`
func (c *Client) GetInboxMessage(ctx context.Context, inbox string, id string) (*InboxMessage, error) {
	out := new(InboxMessage)
	if _, err := c.do(ctx, "GET", "/v1/inboxes/"+url.PathEscape(inbox)+"/messages/"+url.PathEscape(id), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetInboxMessageRaw: The message as received (message/rfc822). `GET /v1/inboxes/{inbox}/messages/{id}/raw`
func (c *Client) GetInboxMessageRaw(ctx context.Context, inbox string, id string) ([]byte, error) {
	return c.do(ctx, "GET", "/v1/inboxes/"+url.PathEscape(inbox)+"/messages/"+url.PathEscape(id)+"/raw", request{}, nil)
}

// WaitForInboxMessageParams holds the optional parameters of WaitForInboxMessage.
type WaitForInboxMessageParams struct {
	// unix seconds: only messages received at or after (default: 60 seconds ago)
	Since string
	// a message id: only messages that arrived after it
	After string
	// part of the From address or name
	Sender string
	// part of the subject
	Subject string
	// seconds to wait, default 60, at most 120
	Timeout string
}

// WaitForInboxMessage: Wait for the next message (its one-time codes and verification links already pulled out); message is null on timeout. `GET /v1/inboxes/{inbox}/wait`
func (c *Client) WaitForInboxMessage(ctx context.Context, inbox string, params *WaitForInboxMessageParams) (*InboxWait, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "since", params.Since)
		setQuery(q, "after", params.After)
		setQuery(q, "sender", params.Sender)
		setQuery(q, "subject", params.Subject)
		setQuery(q, "timeout", params.Timeout)
	}
	out := new(InboxWait)
	if _, err := c.do(ctx, "GET", "/v1/inboxes/"+url.PathEscape(inbox)+"/wait", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetLimits: What the account can run now: sandboxes at once, sizes, trial, start limits. `GET /v1/limits`
func (c *Client) GetLimits(ctx context.Context) (*Limits, error) {
	out := new(Limits)
	if _, err := c.do(ctx, "GET", "/v1/limits", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ProviderModels: The harnesses, their models, effort levels and fast mode. `GET /v1/provider-models`
func (c *Client) ProviderModels(ctx context.Context) ([]ProviderModels, error) {
	var out []ProviderModels
	if _, err := c.do(ctx, "GET", "/v1/provider-models", request{noAuth: true}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListSandboxesParams holds the optional parameters of ListSandboxes.
type ListSandboxesParams struct {
	// key=value, or key for any value; repeat to require several
	Label []string
	// exact name
	Name string
	// names that start with this
	NamePrefix string
	// creating, starting, running, stopping, stopped, restoring, deleting or error; comma-separated for several
	State string
	// asc (oldest first, the default) or desc (newest first)
	Sort string
	// 1-200; without it, every match. With it, X-Next-Cursor names the next page
	Limit string
	// X-Next-Cursor of the previous page (it is `<created_at>_<id>` of that page's last sandbox)
	Cursor string
}

// ListSandboxes: Sandboxes, oldest first, filtered and paged. `GET /v1/sandboxes`
func (c *Client) ListSandboxes(ctx context.Context, params *ListSandboxesParams) ([]Sandbox, error) {
	q := url.Values{}
	if params != nil {
		for _, v := range params.Label {
			q.Add("label", v)
		}
		setQuery(q, "name", params.Name)
		setQuery(q, "name_prefix", params.NamePrefix)
		setQuery(q, "state", params.State)
		setQuery(q, "sort", params.Sort)
		setQuery(q, "limit", params.Limit)
		setQuery(q, "cursor", params.Cursor)
	}
	var out []Sandbox
	if _, err := c.do(ctx, "GET", "/v1/sandboxes", request{query: q}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// CreateSandboxesParams holds the optional parameters of CreateSandboxes.
type CreateSandboxesParams struct {
	// Any unique string: a retry with the same key within 24 hours returns the first answer.
	IdempotencyKey string
}

// CreateSandboxes: Create sandboxes (202; poll until running). `POST /v1/sandboxes`
func (c *Client) CreateSandboxes(ctx context.Context, body CreateSandbox, params *CreateSandboxesParams) ([]Sandbox, error) {
	idem := ""
	if params != nil {
		idem = params.IdempotencyKey
	}
	var out []Sandbox
	if _, err := c.do(ctx, "POST", "/v1/sandboxes", request{json: body, idemKey: idem}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteSandbox: Delete a sandbox (saved snapshots stay). `DELETE /v1/sandboxes/{name}`
func (c *Client) DeleteSandbox(ctx context.Context, name string) error {
	_, err := c.do(ctx, "DELETE", "/v1/sandboxes/"+url.PathEscape(name), request{}, nil)
	return err
}

// GetSandbox: One sandbox. `GET /v1/sandboxes/{name}`
func (c *Client) GetSandbox(ctx context.Context, name string) (*Sandbox, error) {
	out := new(Sandbox)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateSandbox: Rename; change the auto-stop timer (counting from now while running), lifetime, auto-delete, auto-start or labels. `PATCH /v1/sandboxes/{name}`
func (c *Client) UpdateSandbox(ctx context.Context, name string, body UpdateSandbox) (*Sandbox, error) {
	out := new(Sandbox)
	if _, err := c.do(ctx, "PATCH", "/v1/sandboxes/"+url.PathEscape(name), request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetSandboxAiParams holds the optional parameters of GetSandboxAi.
type GetSandboxAiParams struct {
	// 1-366 UTC days, default 30
	Days string
}

// GetSandboxAi: A sandbox's AI profile, proxy state, block, and usage per day and model. `GET /v1/sandboxes/{name}/ai`
func (c *Client) GetSandboxAi(ctx context.Context, name string, params *GetSandboxAiParams) (*SandboxAi, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "days", params.Days)
	}
	out := new(SandboxAi)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/ai", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateSandboxAi: Block or unblock a sandbox's AI requests, or give it another profile. `PUT /v1/sandboxes/{name}/ai`
func (c *Client) UpdateSandboxAi(ctx context.Context, name string, body UpdateSandboxAi) (*SandboxAi, error) {
	out := new(SandboxAi)
	if _, err := c.do(ctx, "PUT", "/v1/sandboxes/"+url.PathEscape(name)+"/ai", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetBrowserLinkParams holds the optional parameters of GetBrowserLink.
type GetBrowserLinkParams struct {
	// dark (default) or light: the viewer's look
	Theme string
}

// GetBrowserLink: A link to the browser view: only Chrome, 1920x1080. `POST /v1/sandboxes/{name}/browser`
func (c *Client) GetBrowserLink(ctx context.Context, name string, body BrowserRequest, params *GetBrowserLinkParams) (*DesktopLink, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "theme", params.Theme)
	}
	out := new(DesktopLink)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/browser", request{json: body, query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListCommandsParams holds the optional parameters of ListCommands.
type ListCommandsParams struct {
	// commands (default) | process | sync | stream | detached
	Mode string
	// desc (newest first, the default) or asc
	Sort string
	// 1-200, default 50
	Limit string
	// next_cursor of the previous page
	Cursor string
}

// ListCommands: Commands run in the sandbox, newest first (90 days, also while stopped). `GET /v1/sandboxes/{name}/command-history`
func (c *Client) ListCommands(ctx context.Context, name string, params *ListCommandsParams) (*CommandList, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "mode", params.Mode)
		setQuery(q, "sort", params.Sort)
		setQuery(q, "limit", params.Limit)
		setQuery(q, "cursor", params.Cursor)
	}
	out := new(CommandList)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/command-history", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetCommand: One command's record. `GET /v1/sandboxes/{name}/command-history/{id}`
func (c *Client) GetCommand(ctx context.Context, name string, id string) (*CommandRecord, error) {
	out := new(CommandRecord)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/command-history/"+url.PathEscape(id), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ReadCommandLogsParams holds the optional parameters of ReadCommandLogs.
type ReadCommandLogsParams struct {
	// stdout (default) | stderr
	Stream string
	// byte offset
	Offset string
	// utf8 (default) | base64
	Encoding string
}

// ReadCommandLogs: A command's stored output from an offset (at most 1 MiB). `GET /v1/sandboxes/{name}/command-history/{id}/logs`
func (c *Client) ReadCommandLogs(ctx context.Context, name string, id string, params *ReadCommandLogsParams) (*CommandLogs, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "stream", params.Stream)
		setQuery(q, "offset", params.Offset)
		setQuery(q, "encoding", params.Encoding)
	}
	out := new(CommandLogs)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/command-history/"+url.PathEscape(id)+"/logs", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListConversations: Conversations, most recent first. `GET /v1/sandboxes/{name}/conversations`
func (c *Client) ListConversations(ctx context.Context, name string) (*ConversationList, error) {
	out := new(ConversationList)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/conversations", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteConversation: Delete a conversation and its history. `DELETE /v1/sandboxes/{name}/conversations/{id}`
func (c *Client) DeleteConversation(ctx context.Context, name string, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/sandboxes/"+url.PathEscape(name)+"/conversations/"+url.PathEscape(id), request{}, nil)
	return err
}

// GetConversation: A conversation and its turns (id or current). `GET /v1/sandboxes/{name}/conversations/{id}`
func (c *Client) GetConversation(ctx context.Context, name string, id string) (*ConversationDetail, error) {
	out := new(ConversationDetail)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/conversations/"+url.PathEscape(id), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetDesktopLinkParams holds the optional parameters of GetDesktopLink.
type GetDesktopLinkParams struct {
	// dark (default) or light: the desktop's and the viewer's look
	Theme string
}

// GetDesktopLink: A link to the desktop (1920x1080) in froog's viewer. `POST /v1/sandboxes/{name}/desktop`
func (c *Client) GetDesktopLink(ctx context.Context, name string, params *GetDesktopLinkParams) (*DesktopLink, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "theme", params.Theme)
	}
	out := new(DesktopLink)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/desktop", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListEventsParams holds the optional parameters of ListEvents.
type ListEventsParams struct {
	// only this conversation (or current)
	Conversation string
	// id of the last event seen
	Cursor string
	// 1-1000, default 100 (JSON pages)
	Limit string
}

// ListEvents: Every conversation's events: a JSON page, or a stream (Accept: text/event-stream or application/x-ndjson). `GET /v1/sandboxes/{name}/events`
func (c *Client) ListEvents(ctx context.Context, name string, params *ListEventsParams) (*EventList, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "conversation", params.Conversation)
		setQuery(q, "cursor", params.Cursor)
		setQuery(q, "limit", params.Limit)
	}
	out := new(EventList)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/events", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// Exec: Run a command and wait for it; with stream: true, NDJSON events as output arrives. `POST /v1/sandboxes/{name}/exec`
func (c *Client) Exec(ctx context.Context, name string, body ExecRequest) (*ExecResult, error) {
	out := new(ExecResult)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/exec", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteFileParams holds the optional parameters of DeleteFile.
type DeleteFileParams struct {
	// true to delete a directory
	Recursive string
}

// DeleteFile: Delete a file or directory. `DELETE /v1/sandboxes/{name}/files`
func (c *Client) DeleteFile(ctx context.Context, name string, path string, params *DeleteFileParams) error {
	q := url.Values{}
	q.Set("path", path)
	if params != nil {
		setQuery(q, "recursive", params.Recursive)
	}
	_, err := c.do(ctx, "DELETE", "/v1/sandboxes/"+url.PathEscape(name)+"/files", request{query: q}, nil)
	return err
}

// FileInfo: A file's details or a directory listing. `GET /v1/sandboxes/{name}/files`
func (c *Client) FileInfo(ctx context.Context, name string, path string) (*FileInfo, error) {
	q := url.Values{}
	q.Set("path", path)
	out := new(FileInfo)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/files", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UploadArchive: Unpack a .tar.gz into a directory (raw bytes, up to 100 MiB). `PUT /v1/sandboxes/{name}/files/archive`
func (c *Client) UploadArchive(ctx context.Context, name string, path string, data io.Reader) error {
	q := url.Values{}
	q.Set("path", path)
	_, err := c.do(ctx, "PUT", "/v1/sandboxes/"+url.PathEscape(name)+"/files/archive", request{raw: data, query: q}, nil)
	return err
}

// ReadFile: Download a file (raw bytes, up to 100 MiB). `GET /v1/sandboxes/{name}/files/content`
func (c *Client) ReadFile(ctx context.Context, name string, path string) ([]byte, error) {
	q := url.Values{}
	q.Set("path", path)
	return c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/files/content", request{query: q}, nil)
}

// WriteFileParams holds the optional parameters of WriteFile.
type WriteFileParams struct {
	// e.g. 755
	Mode string
}

// WriteFile: Upload a file (raw bytes, up to 100 MiB, atomic). `PUT /v1/sandboxes/{name}/files/content`
func (c *Client) WriteFile(ctx context.Context, name string, path string, data io.Reader, params *WriteFileParams) error {
	q := url.Values{}
	q.Set("path", path)
	if params != nil {
		setQuery(q, "mode", params.Mode)
	}
	_, err := c.do(ctx, "PUT", "/v1/sandboxes/"+url.PathEscape(name)+"/files/content", request{raw: data, query: q}, nil)
	return err
}

// CreateDownloadUrl: A link that downloads a file without an API key. `POST /v1/sandboxes/{name}/files/download-url`
func (c *Client) CreateDownloadUrl(ctx context.Context, name string, body NewFileUrl) (*FileUrl, error) {
	out := new(FileUrl)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/files/download-url", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// FindInFilesParams holds the optional parameters of FindInFiles.
type FindInFilesParams struct {
	// true for a regular expression
	Regex string
}

// FindInFiles: Lines that contain a text. `GET /v1/sandboxes/{name}/files/find`
func (c *Client) FindInFiles(ctx context.Context, name string, pattern string, path string, params *FindInFilesParams) (*FileFind, error) {
	q := url.Values{}
	q.Set("pattern", pattern)
	q.Set("path", path)
	if params != nil {
		setQuery(q, "regex", params.Regex)
	}
	out := new(FileFind)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/files/find", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// MakeDirectory: Create a directory (and parents). `POST /v1/sandboxes/{name}/files/mkdir`
func (c *Client) MakeDirectory(ctx context.Context, name string, body MakeDir) error {
	_, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/files/mkdir", request{json: body}, nil)
	return err
}

// MoveFile: Move or rename. `POST /v1/sandboxes/{name}/files/move`
func (c *Client) MoveFile(ctx context.Context, name string, body MoveFile) error {
	_, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/files/move", request{json: body}, nil)
	return err
}

// SetFilePermissions: Change a file's or directory's permissions (chmod). `POST /v1/sandboxes/{name}/files/permissions`
func (c *Client) SetFilePermissions(ctx context.Context, name string, body FilePermissions) error {
	_, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/files/permissions", request{json: body}, nil)
	return err
}

// ReplaceInFiles: Replace a text in files, byte for byte. `POST /v1/sandboxes/{name}/files/replace`
func (c *Client) ReplaceInFiles(ctx context.Context, name string, body FileReplace) ([]FileReplaced, error) {
	var out []FileReplaced
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/files/replace", request{json: body}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// SearchFiles: Files whose name matches a glob. `GET /v1/sandboxes/{name}/files/search`
func (c *Client) SearchFiles(ctx context.Context, name string, pattern string, path string) (*FileSearch, error) {
	q := url.Values{}
	q.Set("pattern", pattern)
	q.Set("path", path)
	out := new(FileSearch)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/files/search", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// CreateUploadUrl: A link that takes a PUT of a file without an API key. `POST /v1/sandboxes/{name}/files/upload-url`
func (c *Client) CreateUploadUrl(ctx context.Context, name string, body NewFileUrl) (*FileUrl, error) {
	out := new(FileUrl)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/files/upload-url", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// WatchFilesParams holds the optional parameters of WatchFiles.
type WatchFilesParams struct {
	// false: only the directory itself (default true)
	Recursive string
	// 1-3600, default 3600
	TimeoutSecs string
}

// WatchFiles: Stream file changes under a path (NDJSON: ready, then create / modify / delete, then end). `GET /v1/sandboxes/{name}/files/watch`
// fn gets each event as it arrives; returning an error stops the stream.
func (c *Client) WatchFiles(ctx context.Context, name string, path string, params *WatchFilesParams, fn func(FileEvent) error) error {
	q := url.Values{}
	q.Set("path", path)
	if params != nil {
		setQuery(q, "recursive", params.Recursive)
		setQuery(q, "timeout_secs", params.TimeoutSecs)
	}
	return streamEach(ctx, c, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/files/watch", request{query: q, accept: "application/x-ndjson"}, fn)
}

// ForkSandboxParams holds the optional parameters of ForkSandbox.
type ForkSandboxParams struct {
	// Any unique string: a retry with the same key within 24 hours returns the first answer.
	IdempotencyKey string
}

// ForkSandbox: New sandboxes from a snapshot of this one. `POST /v1/sandboxes/{name}/fork`
func (c *Client) ForkSandbox(ctx context.Context, name string, body ForkSandbox, params *ForkSandboxParams) ([]Sandbox, error) {
	idem := ""
	if params != nil {
		idem = params.IdempotencyKey
	}
	var out []Sandbox
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/fork", request{json: body, idemKey: idem}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetSandboxGithubToken: A one-hour GitHub token for the sandbox (what its git credential helper uses; needs the environment). `POST /v1/sandboxes/{name}/github-token`
func (c *Client) GetSandboxGithubToken(ctx context.Context, name string, body GithubTokenRequest) (*GithubToken, error) {
	out := new(GithubToken)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/github-token", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// Interrupt: Stop running turns and cancel queued prompts (one conversation, or all). `POST /v1/sandboxes/{name}/interrupt`
func (c *Client) Interrupt(ctx context.Context, name string, body InterruptRequest) (*InterruptResult, error) {
	out := new(InterruptResult)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/interrupt", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListJobs: Background commands. `GET /v1/sandboxes/{name}/jobs`
func (c *Client) ListJobs(ctx context.Context, name string) ([]Job, error) {
	var out []Job
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/jobs", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// StartJob: Start a background command. `POST /v1/sandboxes/{name}/jobs`
func (c *Client) StartJob(ctx context.Context, name string, body NewJob) (*Job, error) {
	out := new(Job)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/jobs", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ReadJobParams holds the optional parameters of ReadJob.
type ReadJobParams struct {
	// byte offset to read from
	Offset string
	// bytes to read
	Limit string
}

// ReadJob: A background command's state and output from an offset. `GET /v1/sandboxes/{name}/jobs/{id}`
func (c *Client) ReadJob(ctx context.Context, name string, id string, params *ReadJobParams) (*JobOutput, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "offset", params.Offset)
		setQuery(q, "limit", params.Limit)
	}
	out := new(JobOutput)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/jobs/"+url.PathEscape(id), request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// WriteJobStdin: Write to a background command's open standard input (started with stdin_open), or close it. `POST /v1/sandboxes/{name}/jobs/{id}/stdin`
func (c *Client) WriteJobStdin(ctx context.Context, name string, id string, body JobStdin) error {
	_, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/jobs/"+url.PathEscape(id)+"/stdin", request{json: body}, nil)
	return err
}

// StopJob: Stop a background command. `POST /v1/sandboxes/{name}/jobs/{id}/stop`
func (c *Client) StopJob(ctx context.Context, name string, id string) error {
	_, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/jobs/"+url.PathEscape(id)+"/stop", request{}, nil)
	return err
}

// GetSandboxMetrics: CPU, memory, disk and network of a running sandbox, read now. `GET /v1/sandboxes/{name}/metrics`
func (c *Client) GetSandboxMetrics(ctx context.Context, name string) (*SandboxMetrics, error) {
	out := new(SandboxMetrics)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/metrics", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetSandboxMetricsHistoryParams holds the optional parameters of GetSandboxMetricsHistory.
type GetSandboxMetricsHistoryParams struct {
	// unix seconds, default an hour ago
	Since string
	// unix seconds, default now
	Until string
	// seconds per point, default the window / 360 (multiple of 30, at most 1000 points)
	Step string
}

// GetSandboxMetricsHistory: CPU, memory and disk every 30 seconds, kept 7 days, averaged per step. `GET /v1/sandboxes/{name}/metrics/history`
func (c *Client) GetSandboxMetricsHistory(ctx context.Context, name string, params *GetSandboxMetricsHistoryParams) (*MetricsHistory, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "since", params.Since)
		setQuery(q, "until", params.Until)
		setQuery(q, "step", params.Step)
	}
	out := new(MetricsHistory)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/metrics/history", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListPorts: Inbound ports open on the sandbox (besides SSH), including its public URLs' ports, and the host IPv4 address and port each opened one is reached at. `GET /v1/sandboxes/{name}/ports`
func (c *Client) ListPorts(ctx context.Context, name string) (*Ports, error) {
	out := new(Ports)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/ports", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// OpenPort: Open an inbound TCP or UDP port: on the sandbox's IPv6 address, and on a public port of its host's IPv4 address. `POST /v1/sandboxes/{name}/ports`
func (c *Client) OpenPort(ctx context.Context, name string, body OpenPort) (*Ports, error) {
	out := new(Ports)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/ports", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SetPorts: Set the opened ports, replacing the ones opened before (kept across stops and starts). `PUT /v1/sandboxes/{name}/ports`
func (c *Client) SetPorts(ctx context.Context, name string, body SetPorts) (*Ports, error) {
	out := new(Ports)
	if _, err := c.do(ctx, "PUT", "/v1/sandboxes/"+url.PathEscape(name)+"/ports", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// WatchPortsParams holds the optional parameters of WatchPorts.
type WatchPortsParams struct {
	// 1-3600, default 3600
	TimeoutSecs string
}

// WatchPorts: Stream TCP ports starting or stopping to listen (NDJSON: ready, open / close, end). `GET /v1/sandboxes/{name}/ports/watch`
// fn gets each event as it arrives; returning an error stops the stream.
func (c *Client) WatchPorts(ctx context.Context, name string, params *WatchPortsParams, fn func(PortEvent) error) error {
	q := url.Values{}
	if params != nil {
		setQuery(q, "timeout_secs", params.TimeoutSecs)
	}
	return streamEach(ctx, c, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/ports/watch", request{query: q, accept: "application/x-ndjson"}, fn)
}

// ClosePort: Close an opened port (IPv6 and IPv4). `DELETE /v1/sandboxes/{name}/ports/{port}/{protocol}`
func (c *Client) ClosePort(ctx context.Context, name string, port string, protocol string) (*Ports, error) {
	out := new(Ports)
	if _, err := c.do(ctx, "DELETE", "/v1/sandboxes/"+url.PathEscape(name)+"/ports/"+url.PathEscape(port)+"/"+url.PathEscape(protocol), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListProcesses: Processes, busiest first (a stopped sandbox: the sample taken at stop). `GET /v1/sandboxes/{name}/processes`
func (c *Client) ListProcesses(ctx context.Context, name string) (*ProcessList, error) {
	out := new(ProcessList)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/processes", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListProcessRecordsParams holds the optional parameters of ListProcessRecords.
type ListProcessRecordsParams struct {
	// unix seconds
	Since string
	// unix seconds
	Until string
	// true: only processes still running
	Running string
	// 1-1000, default 200
	Limit string
}

// ListProcessRecords: Processes seen in the 30-second samples, with start and end times, kept 72 hours. `GET /v1/sandboxes/{name}/processes/records`
func (c *Client) ListProcessRecords(ctx context.Context, name string, params *ListProcessRecordsParams) (*ProcessRecords, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "since", params.Since)
		setQuery(q, "until", params.Until)
		setQuery(q, "running", params.Running)
		setQuery(q, "limit", params.Limit)
	}
	out := new(ProcessRecords)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/processes/records", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListProcessSamplesParams holds the optional parameters of ListProcessSamples.
type ListProcessSamplesParams struct {
	// unix seconds
	Since string
	// unix seconds
	Until string
	// 1-1000, default 120
	Limit string
}

// ListProcessSamples: The 12 busiest processes every 30 seconds, kept 7 days, newest first. `GET /v1/sandboxes/{name}/processes/samples`
func (c *Client) ListProcessSamples(ctx context.Context, name string, params *ListProcessSamplesParams) (*ProcessSamples, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "since", params.Since)
		setQuery(q, "until", params.Until)
		setQuery(q, "limit", params.Limit)
	}
	out := new(ProcessSamples)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/processes/samples", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// Prompt: Prompt a coding agent and wait for its answer (continues the current conversation). `POST /v1/sandboxes/{name}/prompt`
func (c *Client) Prompt(ctx context.Context, name string, body PromptRequest) (*PromptResult, error) {
	out := new(PromptResult)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/prompt", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListPromptsParams holds the optional parameters of ListPrompts.
type ListPromptsParams struct {
	// only this conversation
	Conversation string
	// 1-200, default 50
	Limit string
}

// ListPrompts: Prompts, newest first. `GET /v1/sandboxes/{name}/prompts`
func (c *Client) ListPrompts(ctx context.Context, name string, params *ListPromptsParams) (*PromptList, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "conversation", params.Conversation)
		setQuery(q, "limit", params.Limit)
	}
	out := new(PromptList)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/prompts", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// QueuePrompt: Queue a prompt for a coding agent (returns at once; follow its events). `POST /v1/sandboxes/{name}/prompts`
func (c *Client) QueuePrompt(ctx context.Context, name string, body PromptRequest) (*Prompt, error) {
	out := new(Prompt)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/prompts", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetPrompt: One prompt: status (queued, running, done, failed, interrupted), result, error. `GET /v1/sandboxes/{name}/prompts/{id}`
func (c *Client) GetPrompt(ctx context.Context, name string, id string) (*Prompt, error) {
	out := new(Prompt)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/prompts/"+url.PathEscape(id), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// PromptEventsParams holds the optional parameters of PromptEvents.
type PromptEventsParams struct {
	// id of the last event seen
	Cursor string
	// 1-1000, default 100 (JSON pages)
	Limit string
}

// PromptEvents: A prompt's events: a JSON page, or a stream until it ends (Accept: text/event-stream or application/x-ndjson). `GET /v1/sandboxes/{name}/prompts/{id}/events`
func (c *Client) PromptEvents(ctx context.Context, name string, id string, params *PromptEventsParams) (*EventList, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "cursor", params.Cursor)
		setQuery(q, "limit", params.Limit)
	}
	out := new(EventList)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/prompts/"+url.PathEscape(id)+"/events", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SandboxProviderModels: The model catalog plus the models the harnesses list in this sandbox. `GET /v1/sandboxes/{name}/provider-models`
func (c *Client) SandboxProviderModels(ctx context.Context, name string) ([]ProviderModels, error) {
	var out []ProviderModels
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/provider-models", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// RestoreSandbox: Roll the sandbox back to a snapshot. `POST /v1/sandboxes/{name}/restore`
func (c *Client) RestoreSandbox(ctx context.Context, name string, body Restore) (*Sandbox, error) {
	out := new(Sandbox)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/restore", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteAutoSnapshots: Delete every automatic snapshot (saved ones and the one a stopped sandbox starts from stay). `DELETE /v1/sandboxes/{name}/snapshots`
func (c *Client) DeleteAutoSnapshots(ctx context.Context, name string) (*DeletedSnapshots, error) {
	out := new(DeletedSnapshots)
	if _, err := c.do(ctx, "DELETE", "/v1/sandboxes/"+url.PathEscape(name)+"/snapshots", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListSnapshotsParams holds the optional parameters of ListSnapshots.
type ListSnapshotsParams struct {
	// asc (oldest first, the default) or desc (newest first)
	Sort string
	// 1-200; without it, every one. With it, X-Next-Cursor names the next page
	Limit string
	// X-Next-Cursor of the previous page
	Cursor string
}

// ListSnapshots: A sandbox's snapshots, oldest first, paged. `GET /v1/sandboxes/{name}/snapshots`
func (c *Client) ListSnapshots(ctx context.Context, name string, params *ListSnapshotsParams) ([]Snapshot, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "sort", params.Sort)
		setQuery(q, "limit", params.Limit)
		setQuery(q, "cursor", params.Cursor)
	}
	var out []Snapshot
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/snapshots", request{query: q}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// SaveSnapshot: Save a snapshot (with a label: an account template). `POST /v1/sandboxes/{name}/snapshots`
func (c *Client) SaveSnapshot(ctx context.Context, name string, body CreateSnapshot) (*Snapshot, error) {
	out := new(Snapshot)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/snapshots", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteSnapshot: Delete a snapshot. `DELETE /v1/sandboxes/{name}/snapshots/{snap}`
func (c *Client) DeleteSnapshot(ctx context.Context, name string, snap string) error {
	_, err := c.do(ctx, "DELETE", "/v1/sandboxes/"+url.PathEscape(name)+"/snapshots/"+url.PathEscape(snap), request{}, nil)
	return err
}

// GetSnapshot: One snapshot by id or label; latest is the newest available one. `GET /v1/sandboxes/{name}/snapshots/{snap}`
func (c *Client) GetSnapshot(ctx context.Context, name string, snap string) (*Snapshot, error) {
	out := new(Snapshot)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/snapshots/"+url.PathEscape(snap), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SnapshotFileInfo: A file's details, or a folder's entries, inside a snapshot (the sandbox may be stopped). `GET /v1/sandboxes/{name}/snapshots/{snap}/files`
func (c *Client) SnapshotFileInfo(ctx context.Context, name string, snap string, path string) (*FileInfo, error) {
	q := url.Values{}
	q.Set("path", path)
	out := new(FileInfo)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/snapshots/"+url.PathEscape(snap)+"/files", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DownloadFromSnapshot: Download a file (raw bytes) or a folder (tar) out of a snapshot. `GET /v1/sandboxes/{name}/snapshots/{snap}/files/download`
func (c *Client) DownloadFromSnapshot(ctx context.Context, name string, snap string, path string) ([]byte, error) {
	q := url.Values{}
	q.Set("path", path)
	return c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/snapshots/"+url.PathEscape(snap)+"/files/download", request{query: q}, nil)
}

// GetSshInfo: Where and how to SSH in (host, port, pinned host key). `GET /v1/sandboxes/{name}/ssh`
func (c *Client) GetSshInfo(ctx context.Context, name string) (*SshInfo, error) {
	out := new(SshInfo)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/ssh", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// StartSandbox: Start a stopped sandbox (its auto-stop timer starts again), optionally with a new size, auto-stop timer, environment or env vars. `POST /v1/sandboxes/{name}/start`
func (c *Client) StartSandbox(ctx context.Context, name string, body StartSandbox) (*Sandbox, error) {
	out := new(Sandbox)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/start", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// Steer: Send a message to a running turn (Claude Code takes it mid-turn; others get it as the next turn). `POST /v1/sandboxes/{name}/steer`
func (c *Client) Steer(ctx context.Context, name string, body SteerRequest) (*SteerResult, error) {
	out := new(SteerResult)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/steer", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// StopSandbox: Stop: run ~/.froog/pre-stop, snapshot the disk, stop billing (refused and retried if the snapshot fails; force keeps the last good snapshot; the answer's stop_id tracks it). `POST /v1/sandboxes/{name}/stop`
func (c *Client) StopSandbox(ctx context.Context, name string, body StopSandbox) (*Sandbox, error) {
	out := new(Sandbox)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/stop", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// TouchSandbox: Record activity (last_active_at); the auto-stop deadline doesn't move (PATCH ttl_secs does). `POST /v1/sandboxes/{name}/touch`
func (c *Client) TouchSandbox(ctx context.Context, name string) error {
	_, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/touch", request{}, nil)
	return err
}

// ListUrls: Public URLs of the sandbox's ports. `GET /v1/sandboxes/{name}/urls`
func (c *Client) ListUrls(ctx context.Context, name string) ([]PublicUrl, error) {
	var out []PublicUrl
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/urls", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// HostPort: Host a port (private with a token unless public), or rename or title it. `POST /v1/sandboxes/{name}/urls`
func (c *Client) HostPort(ctx context.Context, name string, body NewPublicUrl) (*PublicUrl, error) {
	out := new(PublicUrl)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/urls", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveUrl: Remove a public URL. `DELETE /v1/sandboxes/{name}/urls/{port}`
func (c *Client) RemoveUrl(ctx context.Context, name string, port string) error {
	_, err := c.do(ctx, "DELETE", "/v1/sandboxes/"+url.PathEscape(name)+"/urls/"+url.PathEscape(port), request{}, nil)
	return err
}

// ListUrlTokens: Live viewer tokens of a private URL (secrets are never returned). `GET /v1/sandboxes/{name}/urls/{port}/tokens`
func (c *Client) ListUrlTokens(ctx context.Context, name string, port string) ([]UrlToken, error) {
	var out []UrlToken
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/urls/"+url.PathEscape(port)+"/tokens", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// CreateUrlToken: Another token for a private URL that expires and can be revoked on its own (the secret is returned once). `POST /v1/sandboxes/{name}/urls/{port}/tokens`
func (c *Client) CreateUrlToken(ctx context.Context, name string, port string, body NewUrlToken) (*UrlToken, error) {
	out := new(UrlToken)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/urls/"+url.PathEscape(port)+"/tokens", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RevokeUrlToken: Revoke a viewer token. `DELETE /v1/sandboxes/{name}/urls/{port}/tokens/{id}`
func (c *Client) RevokeUrlToken(ctx context.Context, name string, port string, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/sandboxes/"+url.PathEscape(name)+"/urls/"+url.PathEscape(port)+"/tokens/"+url.PathEscape(id), request{}, nil)
	return err
}

// GetSandboxUsageParams holds the optional parameters of GetSandboxUsage.
type GetSandboxUsageParams struct {
	// unix seconds; default the start of the current billing period
	Since string
	// unix seconds; default now
	Until string
}

// GetSandboxUsage: One sandbox's running time and cost in a window, at list prices. `GET /v1/sandboxes/{name}/usage`
func (c *Client) GetSandboxUsage(ctx context.Context, name string, params *GetSandboxUsageParams) (*SandboxUsage, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "since", params.Since)
		setQuery(q, "until", params.Until)
	}
	out := new(SandboxUsage)
	if _, err := c.do(ctx, "GET", "/v1/sandboxes/"+url.PathEscape(name)+"/usage", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// WaitForPort: Wait until something listens on a TCP port inside the sandbox (ready: false after the timeout). `POST /v1/sandboxes/{name}/wait`
func (c *Client) WaitForPort(ctx context.Context, name string, body WaitForPort) (*PortWait, error) {
	out := new(PortWait)
	if _, err := c.do(ctx, "POST", "/v1/sandboxes/"+url.PathEscape(name)+"/wait", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListSavedSnapshots: Saved snapshots of the account (templates for from). `GET /v1/saved-snapshots`
func (c *Client) ListSavedSnapshots(ctx context.Context) ([]SavedSnapshot, error) {
	var out []SavedSnapshot
	if _, err := c.do(ctx, "GET", "/v1/saved-snapshots", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// DeleteSavedSnapshot: Remove a saved snapshot. `DELETE /v1/saved-snapshots/{name}`
func (c *Client) DeleteSavedSnapshot(ctx context.Context, name string) error {
	_, err := c.do(ctx, "DELETE", "/v1/saved-snapshots/"+url.PathEscape(name), request{}, nil)
	return err
}

// GetSavedSnapshot: One saved snapshot and its status. `GET /v1/saved-snapshots/{name}`
func (c *Client) GetSavedSnapshot(ctx context.Context, name string) (*SavedSnapshot, error) {
	out := new(SavedSnapshot)
	if _, err := c.do(ctx, "GET", "/v1/saved-snapshots/"+url.PathEscape(name), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SavedSnapshotFileInfo: A file's details, or a folder's entries, inside a saved snapshot. `GET /v1/saved-snapshots/{name}/files`
func (c *Client) SavedSnapshotFileInfo(ctx context.Context, name string, path string) (*FileInfo, error) {
	q := url.Values{}
	q.Set("path", path)
	out := new(FileInfo)
	if _, err := c.do(ctx, "GET", "/v1/saved-snapshots/"+url.PathEscape(name)+"/files", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// DownloadFromSavedSnapshot: Download a file (raw bytes) or a folder (tar) out of a saved snapshot. `GET /v1/saved-snapshots/{name}/files/download`
func (c *Client) DownloadFromSavedSnapshot(ctx context.Context, name string, path string) ([]byte, error) {
	q := url.Values{}
	q.Set("path", path)
	return c.do(ctx, "GET", "/v1/saved-snapshots/"+url.PathEscape(name)+"/files/download", request{query: q}, nil)
}

// ListSnapshotShares: A saved snapshot's share codes (not the codes themselves). `GET /v1/saved-snapshots/{name}/shares`
func (c *Client) ListSnapshotShares(ctx context.Context, name string) ([]SnapshotShare, error) {
	var out []SnapshotShare
	if _, err := c.do(ctx, "GET", "/v1/saved-snapshots/"+url.PathEscape(name)+"/shares", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// ShareSavedSnapshot: Make a share code another account can create sandboxes from (shown once). `POST /v1/saved-snapshots/{name}/shares`
func (c *Client) ShareSavedSnapshot(ctx context.Context, name string) (*CreatedSnapshotShare, error) {
	out := new(CreatedSnapshotShare)
	if _, err := c.do(ctx, "POST", "/v1/saved-snapshots/"+url.PathEscape(name)+"/shares", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RevokeSnapshotShare: Revoke a share code. `DELETE /v1/saved-snapshots/{name}/shares/{id}`
func (c *Client) RevokeSnapshotShare(ctx context.Context, name string, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/saved-snapshots/"+url.PathEscape(name)+"/shares/"+url.PathEscape(id), request{}, nil)
	return err
}

// ListAccountSnapshotsParams holds the optional parameters of ListAccountSnapshots.
type ListAccountSnapshotsParams struct {
	// desc (newest first, the default) or asc (oldest first)
	Sort string
	// 1-200; without it, every one. With it, X-Next-Cursor names the next page
	Limit string
	// X-Next-Cursor of the previous page
	Cursor string
}

// ListAccountSnapshots: Every snapshot of the account, newest first, with its sandbox, paged. `GET /v1/snapshots`
func (c *Client) ListAccountSnapshots(ctx context.Context, params *ListAccountSnapshotsParams) ([]Snapshot, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "sort", params.Sort)
		setQuery(q, "limit", params.Limit)
		setQuery(q, "cursor", params.Cursor)
	}
	var out []Snapshot
	if _, err := c.do(ctx, "GET", "/v1/snapshots", request{query: q}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListSshKeys: SSH public keys that can log in to your sandboxes. `GET /v1/ssh-keys`
func (c *Client) ListSshKeys(ctx context.Context) ([]SshKey, error) {
	var out []SshKey
	if _, err := c.do(ctx, "GET", "/v1/ssh-keys", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// AddSshKey: Add an SSH public key. `POST /v1/ssh-keys`
func (c *Client) AddSshKey(ctx context.Context, body AddSshKey) (*SshKey, error) {
	out := new(SshKey)
	if _, err := c.do(ctx, "POST", "/v1/ssh-keys", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveSshKey: Remove an SSH key. `DELETE /v1/ssh-keys/{id}`
func (c *Client) RemoveSshKey(ctx context.Context, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/ssh-keys/"+url.PathEscape(id), request{}, nil)
	return err
}

// GetStatus: Platform status and 90-day uptime. `GET /v1/status`
func (c *Client) GetStatus(ctx context.Context) error {
	_, err := c.do(ctx, "GET", "/v1/status", request{noAuth: true}, nil)
	return err
}

// GetStop: A stop's progress: pending, failing (retried), completed, superseded or failed. `GET /v1/stops/{id}`
func (c *Client) GetStop(ctx context.Context, id string) (*Stop, error) {
	out := new(Stop)
	if _, err := c.do(ctx, "GET", "/v1/stops/"+url.PathEscape(id), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListSubscriptions: Subscription sign-ins (Claude, ChatGPT): connected or not. `GET /v1/subscriptions`
func (c *Client) ListSubscriptions(ctx context.Context) ([]Subscription, error) {
	var out []Subscription
	if _, err := c.do(ctx, "GET", "/v1/subscriptions", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// SignOutSubscription: Sign out of a subscription (every sandbox loses it at its next start). `DELETE /v1/subscriptions/{provider}`
func (c *Client) SignOutSubscription(ctx context.Context, provider string) error {
	_, err := c.do(ctx, "DELETE", "/v1/subscriptions/"+url.PathEscape(provider), request{}, nil)
	return err
}

// CancelSubscriptionLogin: Cancel the sign-in in progress. `DELETE /v1/subscriptions/{provider}/login`
func (c *Client) CancelSubscriptionLogin(ctx context.Context, provider string) error {
	_, err := c.do(ctx, "DELETE", "/v1/subscriptions/"+url.PathEscape(provider)+"/login", request{}, nil)
	return err
}

// GetSubscriptionLogin: The sign-in in progress (stores the credential once it's done). `GET /v1/subscriptions/{provider}/login`
func (c *Client) GetSubscriptionLogin(ctx context.Context, provider string) (*SubscriptionLogin, error) {
	out := new(SubscriptionLogin)
	if _, err := c.do(ctx, "GET", "/v1/subscriptions/"+url.PathEscape(provider)+"/login", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// StartSubscriptionLogin: Start a subscription sign-in in one of your running sandboxes. `POST /v1/subscriptions/{provider}/login`
func (c *Client) StartSubscriptionLogin(ctx context.Context, provider string, body StartSubscriptionLogin) (*SubscriptionLogin, error) {
	out := new(SubscriptionLogin)
	if _, err := c.do(ctx, "POST", "/v1/subscriptions/"+url.PathEscape(provider)+"/login", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// SendSubscriptionCode: Claude: paste the code the sign-in page showed. `POST /v1/subscriptions/{provider}/login/code`
func (c *Client) SendSubscriptionCode(ctx context.Context, provider string, body SubscriptionCode) (*SubscriptionLogin, error) {
	out := new(SubscriptionLogin)
	if _, err := c.do(ctx, "POST", "/v1/subscriptions/"+url.PathEscape(provider)+"/login/code", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveTelemetry: Stop the OpenTelemetry export. `DELETE /v1/telemetry`
func (c *Client) RemoveTelemetry(ctx context.Context) error {
	_, err := c.do(ctx, "DELETE", "/v1/telemetry", request{}, nil)
	return err
}

// GetTelemetry: The OpenTelemetry endpoint (null when none). `GET /v1/telemetry`
func (c *Client) GetTelemetry(ctx context.Context) (*Telemetry, error) {
	var out *Telemetry
	if _, err := c.do(ctx, "GET", "/v1/telemetry", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// SetTelemetry: Set the OpenTelemetry endpoint; sends a test export. `PUT /v1/telemetry`
func (c *Client) SetTelemetry(ctx context.Context, body SetTelemetry) (*TelemetrySaved, error) {
	out := new(TelemetrySaved)
	if _, err := c.do(ctx, "PUT", "/v1/telemetry", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// GetUsageParams holds the optional parameters of GetUsage.
type GetUsageParams struct {
	// YYYY-MM: that calendar month at list prices; default the current billing period
	Month string
}

// GetUsage: Usage this billing period: per-second running time, snapshots, what the plan covers. `GET /v1/usage`
func (c *Client) GetUsage(ctx context.Context, params *GetUsageParams) (*Usage, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "month", params.Month)
	}
	out := new(Usage)
	if _, err := c.do(ctx, "GET", "/v1/usage", request{query: q}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// JoinWaitlist: Join the waitlist. `POST /v1/waitlist`
func (c *Client) JoinWaitlist(ctx context.Context, body Waitlist) error {
	_, err := c.do(ctx, "POST", "/v1/waitlist", request{json: body, noAuth: true}, nil)
	return err
}

// ListWebhooks: Webhooks. `GET /v1/webhooks`
func (c *Client) ListWebhooks(ctx context.Context) ([]Webhook, error) {
	var out []Webhook
	if _, err := c.do(ctx, "GET", "/v1/webhooks", request{}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// AddWebhook: Add a webhook (the signing secret is returned once). `POST /v1/webhooks`
func (c *Client) AddWebhook(ctx context.Context, body NewWebhook) (*WebhookSecret, error) {
	out := new(WebhookSecret)
	if _, err := c.do(ctx, "POST", "/v1/webhooks", request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// RemoveWebhook: Remove a webhook. `DELETE /v1/webhooks/{id}`
func (c *Client) RemoveWebhook(ctx context.Context, id string) error {
	_, err := c.do(ctx, "DELETE", "/v1/webhooks/"+url.PathEscape(id), request{}, nil)
	return err
}

// GetWebhook: One webhook. `GET /v1/webhooks/{id}`
func (c *Client) GetWebhook(ctx context.Context, id string) (*Webhook, error) {
	out := new(Webhook)
	if _, err := c.do(ctx, "GET", "/v1/webhooks/"+url.PathEscape(id), request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// UpdateWebhook: Change a webhook's URL, events, name, or pause it (the secret stays). `PATCH /v1/webhooks/{id}`
func (c *Client) UpdateWebhook(ctx context.Context, id string, body UpdateWebhook) (*Webhook, error) {
	out := new(Webhook)
	if _, err := c.do(ctx, "PATCH", "/v1/webhooks/"+url.PathEscape(id), request{json: body}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// ListWebhookDeliveriesParams holds the optional parameters of ListWebhookDeliveries.
type ListWebhookDeliveriesParams struct {
	// 1-200, default 50
	Limit string
}

// ListWebhookDeliveries: Delivery attempts of the last 30 days, newest first. `GET /v1/webhooks/{id}/deliveries`
func (c *Client) ListWebhookDeliveries(ctx context.Context, id string, params *ListWebhookDeliveriesParams) ([]WebhookDelivery, error) {
	q := url.Values{}
	if params != nil {
		setQuery(q, "limit", params.Limit)
	}
	var out []WebhookDelivery
	if _, err := c.do(ctx, "GET", "/v1/webhooks/"+url.PathEscape(id)+"/deliveries", request{query: q}, &out); err != nil {
		return nil, err
	}
	return out, nil
}

// RotateWebhook: A new signing secret. `POST /v1/webhooks/{id}/rotate`
func (c *Client) RotateWebhook(ctx context.Context, id string) (*WebhookSecret, error) {
	out := new(WebhookSecret)
	if _, err := c.do(ctx, "POST", "/v1/webhooks/"+url.PathEscape(id)+"/rotate", request{}, out); err != nil {
		return nil, err
	}
	return out, nil
}

// TestWebhook: Send a test event now. `POST /v1/webhooks/{id}/test`
func (c *Client) TestWebhook(ctx context.Context, id string) error {
	_, err := c.do(ctx, "POST", "/v1/webhooks/"+url.PathEscape(id)+"/test", request{}, nil)
	return err
}

// ---------------------------------------------------------------- objects
// Hand-written in deploy/sdk/objects.go.in, appended by deploy/build-sdks.py.

// SandboxService is c.Sandboxes: sandboxes as SandboxHandle objects.
type SandboxService struct{ c *Client }

// Create makes one sandbox and waits until it runs (up to 10 minutes). If waiting fails, the sandbox is deleted again.
//
//	sb, err := c.Sandboxes.Create(ctx, froog.CreateSandbox{Size: froog.Ptr(froog.SizeSmall)})
//	defer sb.Delete(context.Background())
func (s SandboxService) Create(ctx context.Context, body CreateSandbox) (*SandboxHandle, error) {
	if body.Count != nil && *body.Count != 1 {
		return nil, errors.New("froog: Sandboxes.Create makes one sandbox; use CreateSandboxes for Count")
	}
	infos, err := s.c.CreateSandboxes(ctx, body, nil)
	if err != nil {
		return nil, err
	}
	if len(infos) == 0 {
		return nil, errors.New("froog: create returned no sandbox")
	}
	sb := s.c.Sandbox(infos[0].Name)
	sb.Info = &infos[0]
	if _, err := sb.WaitUntil(ctx, StateRunning, 0); err != nil {
		_ = sb.Delete(context.Background())
		return nil, err
	}
	return sb, nil
}

// Get fetches a sandbox by name.
func (s SandboxService) Get(ctx context.Context, name string) (*SandboxHandle, error) {
	sb := s.c.Sandbox(name)
	if _, err := sb.Refresh(ctx); err != nil {
		return nil, err
	}
	return sb, nil
}

// List returns sandboxes (filters as in ListSandboxes; nil for all).
func (s SandboxService) List(ctx context.Context, params *ListSandboxesParams) ([]*SandboxHandle, error) {
	infos, err := s.c.ListSandboxes(ctx, params)
	if err != nil {
		return nil, err
	}
	out := make([]*SandboxHandle, len(infos))
	for i := range infos {
		out[i] = s.c.Sandbox(infos[i].Name)
		out[i].Info = &infos[i]
	}
	return out, nil
}

// Sandbox returns a handle for a sandbox by name, without a request.
func (c *Client) Sandbox(name string) *SandboxHandle {
	sb := &SandboxHandle{Name: name, c: c}
	sb.Files = &SandboxFiles{sb: sb}
	return sb
}

// Ptr returns a pointer to v, for optional fields: froog.Ptr(froog.SizeSmall).
func Ptr[T any](v T) *T { return &v }

// SandboxHandle is a sandbox you act on by method.
type SandboxHandle struct {
	Name string
	// The last fetched state (Refresh, Create, Start, Stop update it).
	Info  *Sandbox
	Files *SandboxFiles
	c     *Client
}

// ExecOptions are the optional parts of Exec, ExecStream and Spawn.
type ExecOptions struct {
	Cwd string
	Env map[string]string
	// Fed to standard input (sent base64-encoded, so binary is fine).
	Stdin []byte
	// 0: the API's default. Not used by Spawn.
	Timeout time.Duration
	// Spawn only: keep standard input open (after Stdin, if given) for ProcessHandle.Write and CloseStdin.
	StdinOpen bool
}

func execRequest(command string, o *ExecOptions) ExecRequest {
	r := ExecRequest{Command: command}
	if o == nil {
		return r
	}
	if o.Cwd != "" {
		r.Cwd = &o.Cwd
	}
	if len(o.Env) > 0 {
		r.Env = o.Env
	}
	if o.Stdin != nil {
		s := base64.StdEncoding.EncodeToString(o.Stdin)
		r.StdinB64 = &s
	}
	if o.Timeout > 0 {
		t := uint64(o.Timeout / time.Second)
		r.TimeoutSecs = &t
	}
	return r
}

// Refresh fetches the sandbox again.
func (sb *SandboxHandle) Refresh(ctx context.Context) (*Sandbox, error) {
	info, err := sb.c.GetSandbox(ctx, sb.Name)
	if err == nil {
		sb.Info = info
	}
	return info, err
}

// WaitUntil polls until the sandbox reaches state. timeout 0 means 10 minutes.
func (sb *SandboxHandle) WaitUntil(ctx context.Context, state State, timeout time.Duration) (*Sandbox, error) {
	info, err := sb.c.WaitUntil(ctx, sb.Name, state, timeout)
	if info != nil {
		sb.Info = info
	}
	return info, err
}

// Exec runs a command and waits for it. A non-zero exit code isn't an error: check ExitCode.
func (sb *SandboxHandle) Exec(ctx context.Context, command string, opts *ExecOptions) (*ExecResult, error) {
	return sb.c.Exec(ctx, sb.Name, execRequest(command, opts))
}

// ExecStream runs a command and calls fn with each event as output arrives.
func (sb *SandboxHandle) ExecStream(ctx context.Context, command string, opts *ExecOptions, fn func(ExecEvent) error) error {
	return sb.c.ExecStream(ctx, sb.Name, execRequest(command, opts), fn)
}

// Spawn starts a background command (a job) and returns its handle.
func (sb *SandboxHandle) Spawn(ctx context.Context, command string, opts *ExecOptions) (*ProcessHandle, error) {
	r := execRequest(command, opts)
	nj := NewJob{Command: command, Cwd: r.Cwd, Env: r.Env, StdinB64: r.StdinB64}
	if opts != nil && opts.StdinOpen {
		nj.StdinOpen = Ptr(true)
	}
	job, err := sb.c.StartJob(ctx, sb.Name, nj)
	if err != nil {
		return nil, err
	}
	return &ProcessHandle{ID: job.ID, State: job.State, ExitCode: job.ExitCode, sb: sb}, nil
}

// Process reconnects to a background command by id (job_…).
func (sb *SandboxHandle) Process(id string) *ProcessHandle {
	return &ProcessHandle{ID: id, sb: sb}
}

// Processes lists the sandbox's background commands, newest first.
func (sb *SandboxHandle) Processes(ctx context.Context) ([]*ProcessHandle, error) {
	jobs, err := sb.c.ListJobs(ctx, sb.Name)
	if err != nil {
		return nil, err
	}
	out := make([]*ProcessHandle, 0, len(jobs))
	for _, j := range jobs {
		out = append(out, &ProcessHandle{ID: j.ID, State: j.State, ExitCode: j.ExitCode, sb: sb})
	}
	return out, nil
}

// WaitForPort waits until something in the sandbox listens on TCP port (POST /wait: checked inside it, 1-600 s).
// timeout 0 means 60 s. Nothing listening by then is a 408 "timeout" *Error.
func (sb *SandboxHandle) WaitForPort(ctx context.Context, port int, timeout time.Duration) error {
	if timeout == 0 {
		timeout = time.Minute
	}
	secs := uint64(timeout / time.Second)
	if secs < 1 {
		secs = 1
	} else if secs > 600 {
		secs = 600
	}
	r, err := sb.c.WaitForPort(ctx, sb.Name, WaitForPort{Port: uint16(port), TimeoutSecs: &secs})
	if err != nil {
		return err
	}
	if !r.Ready {
		return &Error{Status: 408, Code: "timeout", Message: fmt.Sprintf("nothing listens on port %d in %s after %s", port, sb.Name, timeout)}
	}
	return nil
}

// Start starts a stopped sandbox and waits until it runs. body may be nil.
func (sb *SandboxHandle) Start(ctx context.Context, body *StartSandbox) error {
	if body == nil {
		body = &StartSandbox{}
	}
	info, err := sb.c.StartSandbox(ctx, sb.Name, *body)
	if err != nil {
		return err
	}
	sb.Info = info
	_, err = sb.WaitUntil(ctx, StateRunning, 0)
	return err
}

// Stop stops it (disk snapshotted, billing stops) and waits until the stop finishes. A stop
// whose snapshot fails is refused (the sandbox keeps running): StopForce keeps the last good
// snapshot instead.
func (sb *SandboxHandle) Stop(ctx context.Context) error {
	return sb.stop(ctx, StopSandbox{})
}

// StopForce stops it even if the snapshot fails, keeping the last good snapshot, and waits.
func (sb *SandboxHandle) StopForce(ctx context.Context) error {
	return sb.stop(ctx, StopSandbox{Force: true})
}

func (sb *SandboxHandle) stop(ctx context.Context, body StopSandbox) error {
	info, err := sb.c.StopSandbox(ctx, sb.Name, body)
	if err != nil {
		return err
	}
	sb.Info = info
	if info.StopID == nil {
		_, err := sb.WaitUntil(ctx, StateStopped, 0)
		return err
	}
	until := time.Now().Add(10 * time.Minute)
	for {
		s, err := sb.c.GetStop(ctx, *info.StopID)
		if err != nil {
			return err
		}
		switch s.Status {
		case "completed":
			_, _ = sb.Refresh(ctx)
			return nil
		case "failed":
			msg := "failed"
			if s.Error != nil {
				msg = *s.Error
			}
			return &Error{Status: 409, Code: "stop_failed", Message: sb.Name + ": " + msg}
		case "failing":
			msg := "its snapshot failed"
			if s.Error != nil {
				msg = *s.Error
			}
			return &Error{Status: 409, Code: "stop_refused", Message: sb.Name + " keeps running (stop refused, retried by froog): " + msg}
		}
		if time.Now().After(until) {
			return &Error{Status: 408, Code: "timeout", Message: sb.Name + " didn't stop"}
		}
		if err := sleepCtx(ctx, pollInterval); err != nil {
			return err
		}
	}
}

// Delete deletes the sandbox (a 404 counts as done, so it's safe in a defer after an explicit delete).
func (sb *SandboxHandle) Delete(ctx context.Context) error {
	if err := sb.c.DeleteSandbox(ctx, sb.Name); err != nil && !IsNotFound(err) {
		return err
	}
	return nil
}

// Fork makes a new sandbox from a snapshot of this one and waits until it runs. body.Name is set to name.
func (sb *SandboxHandle) Fork(ctx context.Context, name string, body *ForkSandbox) (*SandboxHandle, error) {
	b := ForkSandbox{}
	if body != nil {
		b = *body
	}
	b.Name = name
	b.Count = nil
	infos, err := sb.c.ForkSandbox(ctx, sb.Name, b, nil)
	if err != nil {
		return nil, err
	}
	if len(infos) == 0 {
		return nil, errors.New("froog: fork returned no sandbox")
	}
	f := sb.c.Sandbox(infos[0].Name)
	f.Info = &infos[0]
	if _, err := f.WaitUntil(ctx, StateRunning, 0); err != nil {
		_ = f.Delete(context.Background())
		return nil, err
	}
	return f, nil
}

// Snapshot saves a snapshot; with a label it's an account template.
func (sb *SandboxHandle) Snapshot(ctx context.Context, label string) (*Snapshot, error) {
	b := CreateSnapshot{}
	if label != "" {
		b.Label = &label
	}
	return sb.c.SaveSnapshot(ctx, sb.Name, b)
}

// Host gives a port an HTTPS URL and returns it (private ones carry ?_token=).
func (sb *SandboxHandle) Host(ctx context.Context, port int, public bool) (string, error) {
	u, err := sb.c.HostPort(ctx, sb.Name, NewPublicUrl{Port: uint16(port), Public: public})
	if err != nil {
		return "", err
	}
	return u.URL, nil
}

// OpenPort opens an inbound port ("tcp" or "udp"), kept across stops and starts, on the
// sandbox's IPv6 address and on a public port of its host's IPv4 address (see Ipv4Ports), and
// returns the open ports.
func (sb *SandboxHandle) OpenPort(ctx context.Context, port int, protocol string) ([]string, error) {
	p, err := sb.c.OpenPort(ctx, sb.Name, OpenPort{Port: uint16(port), Protocol: protocol})
	if err != nil {
		return nil, err
	}
	return p.Ports, nil
}

// ClosePort closes a port opened with OpenPort and returns the open ports.
func (sb *SandboxHandle) ClosePort(ctx context.Context, port int, protocol string) ([]string, error) {
	if protocol == "" {
		protocol = "tcp"
	}
	p, err := sb.c.ClosePort(ctx, sb.Name, strconv.Itoa(port), protocol)
	if err != nil {
		return nil, err
	}
	return p.Ports, nil
}

// Ipv4Ports says where the opened ports are reached over IPv4 (Address "203.0.113.7:40000" for
// Port "27015/udp"). Empty while stopped.
func (sb *SandboxHandle) Ipv4Ports(ctx context.Context) ([]Ipv4Port, error) {
	p, err := sb.c.ListPorts(ctx, sb.Name)
	if err != nil {
		return nil, err
	}
	return p.IPv4, nil
}

// Metrics reads CPU, memory, disk and network from the running sandbox now.
func (sb *SandboxHandle) Metrics(ctx context.Context) (*SandboxMetrics, error) {
	return sb.c.GetSandboxMetrics(ctx, sb.Name)
}

// Prompt gives a coding agent in the sandbox a task and waits for its answer (Stdout). It continues the
// sandbox's current conversation unless body.New or body.ConversationID is set.
func (sb *SandboxHandle) Prompt(ctx context.Context, body PromptRequest) (*PromptResult, error) {
	return sb.c.Prompt(ctx, sb.Name, body)
}

// QueuePrompt queues a prompt and returns it at once; follow it with WatchPrompt.
func (sb *SandboxHandle) QueuePrompt(ctx context.Context, body PromptRequest) (*Prompt, error) {
	return sb.c.QueuePrompt(ctx, sb.Name, body)
}

// WatchPrompt calls fn with each of a prompt's events until it ends (polling once a second).
func (sb *SandboxHandle) WatchPrompt(ctx context.Context, promptID string, fn func(AgentEvent) error) error {
	cursor := int64(0)
	for {
		page, err := sb.c.PromptEvents(ctx, sb.Name, promptID, &PromptEventsParams{Cursor: fmt.Sprint(cursor)})
		if err != nil {
			return err
		}
		for _, e := range page.Events {
			if err := fn(e); err != nil {
				return err
			}
			if d, ok := e.Data.(map[string]any); ok && e.Type == "status" {
				if s, _ := d["status"].(string); s == "done" || s == "failed" || s == "interrupted" {
					return nil
				}
			}
		}
		cursor = page.NextCursor
		if len(page.Events) == 0 {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-time.After(time.Second):
			}
		}
	}
}

// Steer sends a message to the running turn (Claude Code takes it mid-turn; others get it as the next turn).
func (sb *SandboxHandle) Steer(ctx context.Context, message string, conversationID string) (*SteerResult, error) {
	body := SteerRequest{Message: message}
	if conversationID != "" {
		body.ConversationID = &conversationID
	}
	return sb.c.Steer(ctx, sb.Name, body)
}

// Interrupt stops running turns and cancels queued prompts: all, or one conversation's.
func (sb *SandboxHandle) Interrupt(ctx context.Context, conversationID string) ([]string, error) {
	body := InterruptRequest{}
	if conversationID != "" {
		body.ConversationID = &conversationID
	}
	r, err := sb.c.Interrupt(ctx, sb.Name, body)
	if err != nil {
		return nil, err
	}
	return r.Interrupted, nil
}

// SandboxFiles is sb.Files. Paths are relative to /home/user, or absolute.
type SandboxFiles struct{ sb *SandboxHandle }

// Read returns a file's bytes.
func (f *SandboxFiles) Read(ctx context.Context, path string) ([]byte, error) {
	return f.sb.c.ReadFile(ctx, f.sb.Name, path)
}

// Write writes a file (atomic).
func (f *SandboxFiles) Write(ctx context.Context, path string, data []byte) error {
	return f.sb.c.WriteFile(ctx, f.sb.Name, path, bytes.NewReader(data), nil)
}

// Info returns a file's details, or a directory and its entries.
func (f *SandboxFiles) Info(ctx context.Context, path string) (*FileInfo, error) {
	return f.sb.c.FileInfo(ctx, f.sb.Name, path)
}

// List returns a directory's entries.
func (f *SandboxFiles) List(ctx context.Context, path string) ([]FileEntry, error) {
	info, err := f.Info(ctx, path)
	if err != nil {
		return nil, err
	}
	return info.Entries, nil
}

// ReadText returns a file's contents as a string.
func (f *SandboxFiles) ReadText(ctx context.Context, path string) (string, error) {
	b, err := f.Read(ctx, path)
	return string(b), err
}

// Exists reports whether a file or directory exists (a 404 is false, other errors are returned).
func (f *SandboxFiles) Exists(ctx context.Context, path string) (bool, error) {
	_, err := f.Info(ctx, path)
	var e *Error
	if errors.As(err, &e) && e.Status == 404 {
		return false, nil
	}
	return err == nil, err
}

// Mkdir creates a directory and its parents. mode like "700"; "" for the default.
func (f *SandboxFiles) Mkdir(ctx context.Context, path, mode string) error {
	body := MakeDir{Path: path}
	if mode != "" {
		body.Mode = &mode
	}
	return f.sb.c.MakeDirectory(ctx, f.sb.Name, body)
}

// Move moves or renames a file or directory.
func (f *SandboxFiles) Move(ctx context.Context, from, to string) error {
	return f.sb.c.MoveFile(ctx, f.sb.Name, MoveFile{From: from, To: to})
}

// Watch calls fn with each change under path as it happens (Type "change", Event create | modify | delete, Path,
// Dir) until timeout (0: the most, an hour) or fn returns an error, which Watch then returns.
func (f *SandboxFiles) Watch(ctx context.Context, path string, recursive bool, timeout time.Duration, fn func(FileEvent) error) error {
	p := &WatchFilesParams{Recursive: strconv.FormatBool(recursive)}
	if timeout > 0 {
		p.TimeoutSecs = strconv.Itoa(int(timeout / time.Second))
	}
	return f.sb.c.WatchFiles(ctx, f.sb.Name, path, p, func(e FileEvent) error {
		switch {
		case e.Type == "change":
			return fn(e)
		case e.Type == "error" || (e.Type == "end" && e.Reason != nil && *e.Reason == "error"):
			msg := "the watch failed"
			if e.Message != nil {
				msg = *e.Message
			}
			return &Error{Status: 409, Code: "watch_failed", Message: msg}
		}
		return nil
	})
}

// Remove deletes a file, or with recursive a directory.
func (f *SandboxFiles) Remove(ctx context.Context, path string, recursive bool) error {
	var p *DeleteFileParams
	if recursive {
		p = &DeleteFileParams{Recursive: "true"}
	}
	return f.sb.c.DeleteFile(ctx, f.sb.Name, path, p)
}

// ProcessHandle is a background command (a job). Output is stdout and stderr together.
type ProcessHandle struct {
	ID string
	// As of the last request: running | paused | exited | killed | lost.
	State    string
	ExitCode *int32
	// Where Follow reads next.
	Offset uint64
	sb     *SandboxHandle
}

func (p *ProcessHandle) read(ctx context.Context, offset uint64, limit string) (*JobOutput, error) {
	out, err := p.sb.c.ReadJob(ctx, p.sb.Name, p.ID, &ReadJobParams{Offset: strconv.FormatUint(offset, 10), Limit: limit})
	if err != nil {
		return nil, err
	}
	p.State, p.ExitCode = out.State, out.ExitCode
	return out, nil
}

// Done reports whether the last known state is final.
func (p *ProcessHandle) Done() bool {
	return p.State == "exited" || p.State == "killed" || p.State == "lost"
}

// Refresh fetches its state.
func (p *ProcessHandle) Refresh(ctx context.Context) error {
	_, err := p.read(ctx, 0, "0")
	return err
}

// Logs returns all its output so far.
func (p *ProcessHandle) Logs(ctx context.Context) (string, error) {
	var sb strings.Builder
	var offset uint64
	for {
		out, err := p.read(ctx, offset, "")
		if err != nil {
			return sb.String(), err
		}
		sb.WriteString(out.Output)
		if out.NextOffset <= offset || out.NextOffset >= out.OutputSize {
			return sb.String(), nil
		}
		offset = out.NextOffset
	}
}

// Follow calls fn with output as it arrives until the command ends (poll 0 means 1 s).
func (p *ProcessHandle) Follow(ctx context.Context, poll time.Duration, fn func(string) error) error {
	if poll == 0 {
		poll = time.Second
	}
	for {
		out, err := p.read(ctx, p.Offset, "")
		if err != nil {
			return err
		}
		if out.Output != "" {
			if err := fn(out.Output); err != nil {
				return err
			}
		}
		moved := out.NextOffset > p.Offset
		p.Offset = out.NextOffset
		if p.Done() && p.Offset >= out.OutputSize {
			return nil
		}
		if !moved {
			if err := sleepCtx(ctx, poll); err != nil {
				return err
			}
		}
	}
}

// Wait polls until it ends and returns its exit code (-1 when killed or lost without one). Use a context deadline
// to give up.
func (p *ProcessHandle) Wait(ctx context.Context) (int, error) {
	for {
		if err := p.Refresh(ctx); err != nil {
			return -1, err
		}
		if p.Done() {
			if p.ExitCode == nil {
				return -1, nil
			}
			return int(*p.ExitCode), nil
		}
		if err := sleepCtx(ctx, time.Second); err != nil {
			return -1, err
		}
	}
}

// Kill stops it (TERM to its process group, KILL after 3 s).
func (p *ProcessHandle) Kill(ctx context.Context) error {
	return p.sb.c.StopJob(ctx, p.sb.Name, p.ID)
}

// Write sends data to its standard input (spawned with StdinOpen); close then closes it.
func (p *ProcessHandle) Write(ctx context.Context, data []byte, close bool) error {
	s := base64.StdEncoding.EncodeToString(data)
	body := JobStdin{DataB64: &s}
	if close {
		body.Close = Ptr(true)
	}
	return p.sb.c.WriteJobStdin(ctx, p.sb.Name, p.ID, body)
}

// CloseStdin closes its standard input: the command reads end of file.
func (p *ProcessHandle) CloseStdin(ctx context.Context) error {
	return p.sb.c.WriteJobStdin(ctx, p.sb.Name, p.ID, JobStdin{Close: Ptr(true)})
}
