// froog API client (TypeScript). Generated from https://froogcorp.com/openapi.json by deploy/build-sdks.py; don't edit.
// API version 0.12.0. Docs: https://froogcorp.com/docs/
//
//   import { Froog } from "froogdev";   // npm install froogdev, or this file: "./froog"
//   const froog = new Froog({ apiKey: process.env.FROOG_API_KEY });
//   await using sb = await froog.sandboxes.create({ size: "small" });   // deleted at the end of the scope
//   console.log((await sb.exec("uname -a")).stdout);
//
//   // or one method per operation:
//   await froog.createSandboxes({ name: "box" });
//   await froog.waitUntil("box", "running");
//   const r = await froog.exec("box", { command: "uname -a" });

export interface Account {
  allow_xlarge: boolean;
  /** Closed (`POST /account/close`): unix seconds. Reopen before `erase_at`. */
  closed_at?: number | null;
  data_retention?: DataRetention;
  /** The closing's deletion operation (`dop_…`, `GET /deletion-operations/{id}`). */
  deletion_operation?: string | null;
  email: string;
  /** When a closed account's data is erased for good (closed_at + 30 days). */
  erase_at?: number | null;
  id: string;
  max_sandboxes: number;
  /** 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 | null;
}

/** `POST /v1/account/close` and `POST /v1/account/reopen`. */
export interface AccountClosing {
  account: Account;
  operation: DeletionOperation;
}

/** 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. */
export interface AddAiCredential {
  /** Custom APIs: an https:// URL with a public DNS name. Bedrock: the AWS region (default us-east-1) goes in `region`. */
  base_url?: string | null;
  /** Custom APIs: the variable name (A-Z, 0-9, _; not a provider's). */
  name?: string | null;
  /** anthropic, openai, gemini, deepseek, mistral, openrouter, xai, bedrock or custom. */
  provider: string;
  /** Bedrock: the AWS region, e.g. us-west-2. */
  region?: string | null;
  /** 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. */
  secret_header?: string | null;
  /** 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). */
  secret_prefix?: string | null;
  /** Custom APIs: put the secret in this query parameter instead of a header (e.g. `api_key`). */
  secret_query?: string | null;
  /** 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;
}

export interface AddSshKey {
  name: string;
  public_key: string;
}

/** 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). */
export interface AgentEvent {
  /** Unix milliseconds. */
  at: number;
  conversation_id: string;
  data: unknown;
  /** Increasing; pass the last one back as `cursor`. */
  id: number;
  prompt_id: string;
  type: string;
}

/** One account of a provider in a profile. Never the secret. */
export interface AiCredential {
  /** The account the proxy uses first for this provider (the first one not resting). */
  active: boolean;
  /** Custom APIs: where the proxy sends the requests. */
  base_url?: string | null;
  created_at: number;
  /** `cred_…` */
  id: string;
  /** The last characters of the key, e.g. `…7f3a`. */
  label: string;
  limited_reason?: string | null;
  /** Hit a rate or usage limit: the proxy skips it until then (unix seconds). */
  limited_until?: number | null;
  /** Custom APIs: the variable name the sandbox gets (e.g. `STRIPE_KEY`). */
  name?: string | null;
  /** anthropic, openai, gemini, deepseek, mistral, openrouter, xai, bedrock or custom. */
  provider: string;
  /** 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`). */
  secret_header?: string | null;
  /** Custom APIs: what comes before the secret in `secret_header` (e.g. `Bearer `). */
  secret_prefix?: string | null;
  /** Custom APIs: the query parameter the proxy puts the secret in, instead of a header. */
  secret_query?: string | null;
}

/** A credential profile: a named set of AI credentials. */
export interface AiProfile {
  created_at: number;
  /** In switching order per provider. */
  credentials: Array<AiCredential>;
  /** Sandboxes get the default profile unless their environment or the sandbox names another. */
  default: boolean;
  /** Environments whose sandboxes get this profile. */
  environments: Array<string>;
  /** `aip_…` */
  id: string;
  name: string;
}

/** The account's AI proxy settings (`GET /v1/ai/proxy`). */
export interface AiProxy {
  /** Every proxied sandbox of the account is refused (403 `ai_blocked`). */
  blocked: boolean;
  /** 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: boolean;
  /** The base URL sandboxes send their AI requests to. */
  url: string;
  /** 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. */
  withhold_subscriptions: boolean;
}

/** Usage of one sandbox in the account view. */
export interface AiSandboxUsage {
  blocked: boolean;
  /** Its name (or its id once deleted). */
  sandbox: string;
  sandbox_id: string;
  totals: AiTotals;
}

/** Tokens and requests over a period. */
export interface AiTotals {
  cache_read_tokens: number;
  cache_write_tokens: number;
  /** Estimated provider cost in USD, from list prices built into froog. Not a froog charge. */
  cost_usd: number;
  /** Answers with status 400 or above (including the proxy's own refusals upstream). */
  errors: number;
  /** Input tokens not read from a cache. */
  input_tokens: number;
  output_tokens: number;
  requests: number;
}

/** The account's AI usage (`GET /v1/ai/usage`). */
export interface AiUsage {
  days: number;
  /** Most expensive first. */
  sandboxes: Array<AiSandboxUsage>;
  totals: AiTotals;
}

/** One row: a UTC day, provider and model. */
export interface AiUsageRow {
  cache_read_tokens: number;
  cache_write_tokens: number;
  /** Estimated provider cost in USD, from list prices built into froog. Not a froog charge. */
  cost_usd: number;
  /** YYYY-MM-DD (UTC). */
  day: string;
  /** Answers with status 400 or above (including the proxy's own refusals upstream). */
  errors: number;
  /** Input tokens not read from a cache. */
  input_tokens: number;
  model: string;
  output_tokens: number;
  provider: string;
  requests: number;
}

/** `GET /v1/account/alerts`: which emails the account gets, and whether an errored sandbox is rebuilt from its last snapshot by itself. */
export interface AlertSettings {
  /** Rebuild a sandbox whose VM was lost (state `error`) from its last snapshot, and email (default on). */
  auto_rebuild: boolean;
  emails: Array<EmailAlert>;
}

/** An API key as listed (the key itself is shown only once, at creation). */
export interface ApiKeyInfo {
  /** What the key may do (`scopes::ACTIONS`); `["*"]` = everything. */
  actions?: Array<string>;
  created_at: number;
  /** An organization key: the email of the member who made it (it acts as that member). */
  created_by?: string | null;
  /** Limited to sandboxes of these environments (names); None = any environment. */
  environments?: Array<string> | null;
  /** None = never expires. */
  expires_at?: number | null;
  id: string;
  label: string;
  /** Limited to sandboxes that have all of these labels; None = any labels. */
  labels?: Record<string, string> | null;
  last_used_at?: number | null;
  /** The preset its actions match, if any (read-only, full-sandbox, ci, admin). */
  preset?: string | null;
  /** Requests made with it in the last 30 days. */
  requests_30d?: number;
  /** The sandboxes it may touch (names), besides those it created; None = all. */
  sandboxes?: Array<string> | null;
  /** Sandboxes this key created that still exist. */
  sandboxes_created?: number;
}

/** One entry of the account's audit log (`GET /audit`): who changed what, from where. Kept 90 days. */
export interface AuditEvent {
  /** What happened, e.g. sandbox.create, api_key.revoke, webhook.update, billing.plan. */
  action: string;
  /** 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;
  /** The API key's name, or the account email for a dashboard session. */
  actor_label?: string | null;
  /** Unix seconds. */
  at: number;
  /** Short extra facts, e.g. the sandboxes a create or fork made. */
  detail?: string | null;
  /** `aud_…` */
  id: string;
  /** The client's IP address. */
  ip?: string | null;
  /** The request's `X-Request-Id`. */
  request_id?: string | null;
  /** What it happened to: a sandbox, key, webhook or variable name or id (never a secret). */
  target?: string | null;
}

export interface AuditList {
  /** Newest first. */
  events: Array<AuditEvent>;
  /** Pass as `cursor` for the next (older) page; None at the end. */
  next_cursor?: string | null;
}

/** `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. */
export interface AutoTopup {
  amount_usd: number;
}

/** Payment status of an account (`GET /v1/billing`). */
export interface Billing {
  /** Declined auto top-ups in a row (3 turn auto top-up off). */
  auto_topup_failures?: number;
  /** Auto top-up amount in dollars; 0 = off. */
  auto_topup_usd?: number;
  /** A payment method is on file. */
  card_on_file: boolean;
  /** Payments are set up on this froog server. */
  enabled: boolean;
  /** The account isn't billed (e.g. the operator's own account). */
  exempt: boolean;
  /** Paying from the wallet for starts past the start limits. */
  extra_starts?: ExtraStarts;
  /** Out of money: running sandboxes are stopped at this time (unix seconds) unless money is added. */
  grace_until?: number | null;
  /** This period: what the plan covers, what was used, and what's left (plan plus wallet). */
  included_usd?: number;
  /** 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). */
  keep_disks_until?: number | null;
  /** Most the card can be charged for the wallet in 24 hours (payments and auto top-ups). */
  max_wallet_per_day_usd?: number;
  /** Smallest wallet payment, dollars. */
  min_topup_usd?: number;
  /** The account's plan; None until one is chosen. */
  plan?: CurrentPlan | null;
  /** The plans on offer, cheapest first. */
  plans?: Array<PlanInfo>;
  /** Per-second rates, quoted per hour and per 730-hour month. */
  rates?: Array<Rate>;
  remaining_usd?: number;
  /** 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: boolean;
  used_usd?: number;
  /** The bonus added to wallet payments and auto top-ups by amount; None = no bonus. */
  wallet_bonus?: WalletBonus | null;
  /** The wallet: money paid in minus usage past the plan. Negative = owed. */
  wallet_usd?: number;
}

/** A Stripe-hosted page to open in a browser. */
export interface BillingUrl {
  url: string;
}

/** `POST /sandboxes/{name}/browser`: open the browser view (only Chrome). */
export interface BrowserRequest {
  /** Chrome's user data directory on the sandbox: an absolute path outside the system directories, created if missing. Default: `~/.config/froog-browser`. */
  profile?: string | null;
}

/** `POST /v1/billing/plan`: choose or change the plan. */
export interface ChoosePlan {
  /** Dollars a month: one of `Billing.plans`. */
  plan: number;
}

/** `GET /v1/ci/analytics?days=&repo=`: what the account's jobs add up to. */
export interface CiAnalytics {
  days: number;
  /** Tests that both failed and passed on the same commit. */
  flaky_tests: Array<CiFlakyTest>;
  /** One row per repository, workflow and job name; most job time first. */
  jobs: Array<CiJobStats>;
  /** Most job time first. */
  repos: Array<CiRepoStats>;
  since: number;
}

export interface CiCache {
  /** The name in `/cache=<name>`. */
  name: string;
  /** When the last default-branch job saved it. */
  saved_at: number;
  /** What it adds to the base image, in GB. */
  size_gb?: number | null;
  /** Its saved snapshot (`cache-<name>`): `DELETE /v1/saved-snapshots/{snapshot}` starts it over. */
  snapshot: string;
}

export interface CiFacet {
  name: string;
  runs: number;
}

export interface CiFlakyTest {
  /** Commits it both failed and passed on. */
  commits: number;
  failed: number;
  last_failed_at: number;
  message?: string | null;
  name: string;
  passed: number;
  repo: string;
  suite?: string | null;
}

/** One GitHub Actions job (`GET /v1/ci`, `GET /v1/ci/jobs/{id}`). */
export interface CiJob {
  /** With `/cache=`: whether its VM started from the saved cache (false: a cold start). */
  cache_hit?: boolean | null;
  /** GitHub's conclusion once completed: success | failure | cancelled | … */
  conclusion?: string | null;
  /** What its job time costs at the CI price (so far, while it runs). */
  cost_usd?: number | null;
  finished_at?: number | null;
  /** GitHub's job id. */
  github_job_id: number;
  /** The branch the run is for (GitHub's `head_branch`). */
  head_branch?: string | null;
  /** The commit the run is for. */
  head_sha?: string | null;
  html_url?: string | null;
  /** `ci_…` */
  id: string;
  job_name: string;
  /** The job's `runs-on` labels. */
  labels: Array<string>;
  queued_at: number;
  /** Why it waits, or why it didn't run. */
  reason?: string | null;
  /** owner/name */
  repo: string;
  /** CPU and memory the job used, once it completed (from the VM's 30-second samples). */
  resources?: CiResources | null;
  run_id: number;
  /** Seconds the job ran (its billed job time). */
  run_secs?: number | null;
  /** The sandbox that runs it (`ci-<repo>-<job id>`). */
  sandbox?: string | null;
  size: Size;
  started_at?: number | null;
  /** queued (waiting for a sandbox) | starting | running | completed | refused | failed */
  state: string;
  /** Test results from the JUnit XML files the job wrote, once it completed. */
  tests?: CiTestSummary | null;
  /** Seconds from queued to the sandbox running. */
  wait_secs?: number | null;
  workflow?: string | null;
}

/** `GET /v1/ci/jobs/{id}/metrics`: the job's CPU and memory over time, and its steps. */
export interface CiJobMetrics {
  /** Averaged into at most 120 buckets, oldest first. */
  points: Array<MetricsPoint>;
  resources?: CiResources | null;
  /** The job's steps, from GitHub, with their times. */
  steps: Array<CiStep>;
}

export interface CiJobStats {
  /** A better size for it, when the runs say so. */
  advice?: CiSizeAdvice | null;
  /** With `/cache=`: the share of runs that started from the cache (0-1). */
  cache_hit_rate?: number | null;
  cancelled: number;
  cost_usd: number;
  /** Average of the runs' CPU averages. */
  cpu_avg_percent?: number | null;
  failed: number;
  failure_rate: number;
  job_name: string;
  /** Highest memory any run used, as a share of its VM's. */
  memory_peak_percent?: number | null;
  minutes: number;
  /** Runs in which the kernel killed a process for want of memory. */
  oom_runs: number;
  p50_secs?: number | null;
  p90_secs?: number | null;
  repo: string;
  runs: number;
  /** The size of its latest run. */
  size: Size;
  /** Its slowest steps (by p50 over the runs), at most 5. */
  slow_steps?: Array<CiStepStats>;
  succeeded: number;
  /** Queued to running. */
  wait_p50_secs?: number | null;
  wait_p90_secs?: number | null;
  workflow?: string | null;
}

/** `GET /v1/ci/jobs/{id}/tests`. */
export interface CiJobTests {
  /** None: the job wrote no JUnit XML froog found. */
  summary?: CiTestSummary | null;
  /** Failed first, then skipped, then passed; at most 1,000. */
  tests: Array<CiTestCase>;
}

export interface CiLogMatch {
  /** The line's time, from GitHub's timestamp. */
  at?: string | null;
  branch?: string | null;
  conclusion?: string | null;
  job_id: string;
  job_name: string;
  /** 1-based line number in the job's log. */
  line: number;
  queued_at: number;
  repo: string;
  /** The step the line is in (from the log's `##[group]` markers). */
  step?: string | null;
  /** The line without its timestamp (first 500 characters). */
  text: string;
  workflow?: string | null;
}

/** `GET /v1/ci/logs/search?q=&days=&limit=&order=`: lines of the account's kept CI job logs. */
export interface CiLogSearch {
  /** Jobs whose logs were searched. */
  jobs_searched: number;
  /** Newest job first (`order=oldest`: oldest first, for where a failure first appeared). */
  matches: Array<CiLogMatch>;
  query: string;
  /** More lines matched than `limit`. */
  truncated: boolean;
}

/** A CI monitor (`GET/POST /v1/ci/monitors`): an alert when matching jobs fail or run long. */
export interface CiMonitor {
  /** Only runs for this branch. */
  branch?: string | null;
  /** failure: alert after this many failures in a row (a success starts over). */
  consecutive: number;
  /** No second alert from this monitor for this long. */
  cooldown_mins: number;
  created_at: number;
  /** duration: alert when a job has run this long. */
  duration_secs?: number | null;
  /** Email the account (webhooks listing `ci.monitor.triggered` get it either way). */
  email: boolean;
  /** `cim_…` */
  id: string;
  /** Only jobs whose name contains this. */
  job?: string | null;
  /** failure (a job failed or timed out) | duration (a job ran longer than `duration_secs`) */
  kind: string;
  last_triggered_at?: number | null;
  /** failure: only failures whose log contains this (any case), e.g. `OutOfMemory` or `SIGKILL`. */
  log_pattern?: string | null;
  name: string;
  /** Its last alerts, newest first (at most 10). */
  recent?: Array<CiMonitorEvent>;
  /** Only this repository (owner/name). */
  repo?: string | null;
  /** Failures in a row so far. */
  streak: number;
  /** Only workflows whose name contains this. */
  workflow?: string | null;
}

export interface CiMonitorEvent {
  at: number;
  job_id: string;
  reason: string;
}

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

export interface CiPeriod {
  cost_usd: number;
  /** Start of the billing period. */
  from: number;
  minutes: number;
}

export interface CiRepoStats {
  cost_usd: number;
  failed: number;
  /** failed / finished runs (0-1). */
  failure_rate: number;
  minutes: number;
  repo: string;
  runs: number;
}

/** What one job used of its VM. */
export interface CiResources {
  /** Average share of all vCPUs busy while the job ran. */
  cpu_avg_percent?: number | null;
  /** Busiest 30 seconds. */
  cpu_peak_percent?: number | null;
  memory_peak_mb?: number | null;
  memory_total_mb?: number | null;
  /** Processes the kernel killed for want of memory during the job. */
  oom_kills?: number;
}

/** One GitHub Actions workflow run: the jobs of one run that ran on froog (`GET /v1/ci/runs`, `GET /v1/ci/runs/{run_id}`). */
export interface CiRun {
  /** The GitHub user who started it. */
  actor?: string | null;
  /** What its jobs' time costs. */
  cost_usd?: number;
  /** First job started to last job finished (so far, while it runs). */
  duration_secs?: number | null;
  /** What started it: push, pull_request, schedule, workflow_dispatch, ... */
  event?: string | null;
  head_branch?: string | null;
  head_sha?: string | null;
  /** The run on GitHub. */
  html_url?: string | null;
  /** Its jobs on froog, oldest first. */
  jobs: Array<CiJob>;
  /** The workflow file, e.g. `.github/workflows/ci.yml`. */
  path?: string | null;
  /** When its first job was queued. */
  queued_at: number;
  /** owner/name */
  repo: string;
  /** GitHub's run id. */
  run_id: number;
  /** The run's number in its workflow (`#250`). */
  run_number?: number | null;
  /** GitHub's title for the run: the head commit's message, or the pull request's title. */
  title?: string | null;
  workflow?: string | null;
}

/** `GET /v1/ci/runs`: one page of runs, newest first, and what the filters can pick from. */
export interface CiRunList {
  branches: Array<string>;
  repos: Array<string>;
  runs: Array<CiRun>;
  /** Runs that match the filters, on every page. */
  total: number;
  /** The account's workflows (every run, not only the filtered ones), with their run counts. */
  workflows: Array<CiFacet>;
}

/** `PUT /v1/ci/settings`. */
export interface CiSettings {
  public_repos: boolean;
  /** GitHub logins; empty = anyone (only with public_repos). */
  public_users?: Array<string>;
}

export interface CiSizeAdvice {
  /** cost | speed | memory */
  kind: string;
  /** The `runs-on` label for it. */
  label: string;
  size: Size;
  why: string;
}

export interface CiStats {
  failed: number;
  jobs: number;
  refused: number;
  run_secs: number;
  succeeded: number;
}

export interface CiStep {
  completed_at?: number | null;
  conclusion?: string | null;
  name: string;
  number: number;
  secs?: number | null;
  started_at?: number | null;
}

export interface CiStepStats {
  name: string;
  p50_secs: number;
  p90_secs: number;
  runs: number;
}

export interface CiTestCase {
  /** The failure's message (first 500 characters). */
  message?: string | null;
  name: string;
  secs?: number | null;
  /** passed | failed | skipped */
  status: string;
  /** The JUnit `classname` (or the suite's name). */
  suite?: string | null;
}

export interface CiTestSummary {
  failed: number;
  passed: number;
  skipped: number;
  total: number;
}

/** `GET /cli-login/{code}`: what's being approved. */
export interface CliLoginInfo {
  code: string;
  device: string;
  expires_at: number;
}

/** `POST /cli-login/poll` */
export interface CliLoginPoll {
  poll_token: string;
}

/** `POST /cli-login` (no auth): start an `froog login` in the browser. */
export interface CliLoginRequest {
  /** Shown on the approval page, e.g. the computer's hostname. */
  device?: string;
}

export interface CliLoginResult {
  email?: string | null;
  key?: string | null;
  /** "pending" or "approved" */
  status: string;
}

export interface CliLoginStarted {
  /** Show this; the dashboard asks the person to check it matches. */
  code: string;
  expires_in: number;
  /** Secret: poll with it. Never show it. */
  poll_token: string;
  /** Open this in the browser. */
  url: string;
}

export interface CommandList {
  commands: Array<CommandRecord>;
  /** Pass as `cursor` for the next (older) page; None at the end. */
  next_cursor?: string | null;
}

/** A chunk of a command's stored output from `offset`. Read again from `next_offset` until `done` (the command ended and everything was read). */
export interface CommandLogs {
  /** UTF-8 text, or base64 with `encoding=base64`. */
  data: string;
  done: boolean;
  next_offset: number;
  offset: number;
  /** Bytes stored so far for this stream. */
  size: number;
  status: string;
}

/** 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. */
export interface CommandRecord {
  /** The command text (first 64 KiB). */
  command: string;
  ended_at?: number | null;
  exit_code?: number | null;
  id: string;
  /** The background job behind a stream/detached command (`froog logs`, `froog kill`). */
  job_id?: string | null;
  mode: string;
  /** The kernel killed it for using too much memory (exit code 137). */
  oom_killed?: boolean;
  /** Output past the per-command or per-account cap was dropped. */
  output_truncated: boolean;
  started_at: number;
  status: string;
  stderr_size: number;
  stdout_size: number;
}

/** One way to sign in to the account (`GET /v1/account/connections`). */
export interface Connection {
  created_at: number;
  /** The address this connection signs in with (GitHub: the account's verified primary address). */
  email?: string | null;
  /** `con_…` */
  id: string;
  /** The GitHub login, for a GitHub connection. */
  login?: string | null;
  /** Its address gets the account's emails (the account's email). */
  primary: boolean;
  /** google | github | email | agentid */
  provider: string;
}

/** A conversation: a thread of prompts with the agent's memory (the harness's own session). */
export interface Conversation {
  /** The harness of its last prompt. */
  agent: string;
  created_at: number;
  /** The one a prompt without `conversation_id` or `new` continues (the most recent). */
  current: boolean;
  id: string;
  /** The start of its last prompt. */
  last_prompt: string;
  model?: string | null;
  prompts: number;
  /** Prompts waiting for their turn. */
  queued: number;
  /** A turn is running in it now. */
  running: boolean;
  sandbox: string;
  updated_at: number;
}

/** A conversation with its prompts, oldest first. */
export interface ConversationDetail {
  /** The harness of its last prompt. */
  agent: string;
  created_at: number;
  /** The one a prompt without `conversation_id` or `new` continues (the most recent). */
  current: boolean;
  id: string;
  /** The start of its last prompt. */
  last_prompt: string;
  model?: string | null;
  prompts: number;
  /** Prompts waiting for their turn. */
  queued: number;
  /** A turn is running in it now. */
  running: boolean;
  sandbox: string;
  turns: Array<Prompt>;
  updated_at: number;
}

export interface ConversationList {
  conversations: Array<Conversation>;
}

export interface CreateAiProfile {
  /** Start with a copy of this profile's credentials. */
  copy_from?: string | null;
  /** 1-40 of a-z, 0-9 and -. */
  name: string;
}

/** `POST /v1/ci/monitors`. */
export interface CreateCiMonitor {
  branch?: string | null;
  /** 1-20, default 1. */
  consecutive?: number | null;
  /** 0-1440, default 60. */
  cooldown_mins?: number | null;
  /** Required for duration: 60 to 21600. */
  duration_secs?: number | null;
  /** Default true. */
  email?: boolean | null;
  job?: string | null;
  /** failure (default) | duration */
  kind?: string | null;
  log_pattern?: string | null;
  name: string;
  repo?: string | null;
  workflow?: string | null;
}

export interface CreateInbox {
  /** Your label for it. */
  name?: string | null;
  /** 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 | null;
  /** 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 | null;
}

export interface CreateSandbox {
  /** true: while stopped, a request to one of its public URLs, or an exec, file, job or wait call, starts it. Default false. */
  auto_start?: boolean | null;
  /** Create `count` sandboxes named `name-1..name-N`. */
  count?: number | null;
  /** 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. */
  delete_after_secs?: number | null;
  /** 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?: Record<string, string> | null;
  /** 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?: boolean | null;
  /** 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). */
  environment_name?: string | null;
  /** Delete it this many seconds after it's created, running or not (60 s to 365 days; absent = never). Shows as `expires_at`. */
  expires_in_secs?: number | null;
  /** Files written into the home directory before the sandbox counts as running (first boot only; mode 600; at most 20, 1 MiB together). */
  files?: Array<NewFile> | null;
  /** 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 | null;
  /** Key-value tags (at most 32; keys 1-63 of `a-z A-Z 0-9 . _ - /`, values up to 255 characters). */
  labels?: Record<string, string> | null;
  /** 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. */
  max_lifetime_secs?: number | null;
  /** false: keep no command history or process samples. Default: the account's setting. */
  monitoring?: boolean | null;
  name?: string | null;
  /** Operating system; default Ubuntu 24.04. */
  os?: Os | null;
  /** A git repo cloned into this sandbox only, by the setup job at boot (also with `environment: false`). */
  repo?: SandboxRepo | null;
  /** A script run once, in the background, after the first boot (as job "setup"). */
  setup?: string | null;
  size?: Size | null;
  /** false: never snapshot it; a stop erases it. Default: the account's setting. */
  snapshots?: boolean | null;
  /** 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). */
  ttl_secs?: number | null;
  /** 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 | null;
}

export interface CreateSnapshot {
  label?: string | null;
}

export interface CreatedApiKey {
  actions?: Array<string>;
  environments?: Array<string> | null;
  expires_at?: number | null;
  id: string;
  /** Shown once; store it now. */
  key: string;
  label: string;
  labels?: Record<string, string> | null;
  sandboxes?: Array<string> | null;
}

/** `POST /saved-snapshots/{name}/shares`: the code, once. */
export interface CreatedSnapshotShare {
  /** `fsh_…`: another account runs `froog new --from fsh_…`. Shown once; froog keeps only a hash. */
  code: string;
  created_at: number;
  id: string;
  name: string;
}

/** The account's plan, mirrored from its Stripe subscription. */
export interface CurrentPlan {
  /** Canceled: the plan ends with the period. */
  cancel_at_period_end: boolean;
  max_sandboxes: number;
  period_end: number;
  /** The current billing period (or the trial), unix seconds. */
  period_start: number;
  saved_snapshots: number;
  /** trialing | active | past_due | unpaid | canceled | incomplete */
  status: string;
  trial_ends_at?: number | null;
  /** Dollars a month. */
  usd: number;
}

/** `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. */
export interface DashboardLink {
  expires_at: number;
  url: string;
}

/** `GET/PATCH /account/data-retention`. Changing it needs a dashboard session, not an API key. */
export interface DataRetention {
  /** 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. */
  deletion_operation?: string | null;
  /** New sandboxes keep no command history or process samples. */
  monitoring_off: boolean;
  /** New sandboxes are never snapshotted (a stop erases them). */
  snapshots_off: boolean;
  /** Erase each sandbox's data (disk, snapshots, history) when it stops; no saved snapshots. */
  zero_data_retention: boolean;
}

/** `DELETE /sandboxes/{name}/snapshots`: the automatic snapshots that were deleted. */
export interface DeletedSnapshots {
  /** Their ids. */
  deleted: Array<string>;
  /** The snapshot a stopped (or errored) sandbox will start from, which is kept. */
  kept?: string | null;
}

/** 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}`. */
export interface DeletionOperation {
  created_at: number;
  /** Why the last attempt couldn't finish (status blocked). */
  error?: string | null;
  /** When the erasure is due (an account close: the end of the reopen window). */
  expected_by?: number | null;
  /** `dop_…` */
  id: string;
  /** account_close | zero_data_retention */
  kind: string;
  /** waiting_for_reopen_window | removing | retrying | completed | reopened */
  stage: string;
  /** pending | processing | blocked | completed | canceled. `blocked` is not final: it is retried. */
  status: string;
  updated_at: number;
}

/** `POST /sandboxes/{name}/desktop`: a link to the VM's desktop in the browser. */
export interface DesktopLink {
  expires_at: number;
  /** 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;
}

export interface EmailAlert {
  description: string;
  enabled: boolean;
  /** disk | stop_refused | unresponsive | rebuilt | plan_nudge | billing */
  kind: string;
}

/** `POST /v1/account/connections/email`: send a 6-digit code to an address to connect it. */
export interface EmailCodeRequest {
  email: string;
}

/** `POST /v1/account/connections/email/verify`: the code from the email. */
export interface EmailCodeVerify {
  code: string;
  email: string;
}

/** A secret file written into every sandbox at each start (content never returned). */
export interface EnvFile {
  /** Relative to the home directory, e.g. `.npmrc` or `app/.env`. */
  path: string;
  size: number;
  updated_at: number;
}

/** An account environment variable as listed (the value is never returned, only a preview). */
export interface EnvVar {
  name: string;
  /** e.g. "sk-a…9xQ"; the whole value if it's short and not secret-looking. */
  preview: string;
  updated_at: number;
}

/** The account's environment: what every new sandbox starts with, besides env vars. */
export interface Environment {
  /** New sandboxes use it when they name no environment. */
  default?: boolean;
  files: Array<EnvFile>;
  /** Its name (`default` for the account's first one). */
  name?: string;
  /** 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. */
  repo_setup?: Record<string, string>;
  /** Git URLs cloned into `~/<repo>` when the directory doesn't exist yet (`https://github.com/org/repo`, optionally `#branch`). */
  repos: Array<string>;
  /** Shell script run in the background at every start, after cloning (as job "setup"). */
  setup?: string | null;
  updated_at: number;
  /** Its latest version: every change makes a new one. Sandboxes stay on the version they started with until `POST /environments/{env}/upgrade`. */
  version?: number;
}

/** One environment in `GET /environments`. */
export interface EnvironmentSummary {
  created_at: number;
  /** New sandboxes use it when they name none. */
  default: boolean;
  files: number;
  name: string;
  repos: number;
  /** Sandboxes (not deleted) per version they are on, newest version first. */
  sandboxes: Array<EnvironmentVersionUse>;
  /** It has a setup script. */
  setup: boolean;
  updated_at: number;
  /** Its latest version (every change makes a new one). */
  version: number;
}

/** The answer to `POST /environments/{env}/upgrade`. */
export interface EnvironmentUpgrade {
  /** Running sandboxes that got the new version now (secret files written, files the new version dropped removed, the setup job started again). */
  applied: Array<string>;
  /** Sandboxes that get it at their next start (stopped, or not reachable right now). */
  at_next_start: Array<string>;
  environment: string;
  /** The version they are on now. */
  version: number;
}

/** One version of an environment (`GET /environments/{env}/versions`). */
export interface EnvironmentVersion {
  /** What the change was, e.g. "repos", "setup script", "file .npmrc set". */
  change: string;
  created_at: number;
  /** Sandboxes (not deleted) on this version. */
  sandboxes: number;
  version: number;
}

export interface EnvironmentVersionUse {
  sandboxes: number;
  version: number;
}

export interface ErrorBody {
  error: ErrorDetail;
}

export interface ErrorDetail {
  code: string;
  message: string;
  /** This request's id (also in the `X-Request-Id` header); quote it when reporting a problem. */
  request_id?: string | null;
}

export interface EventList {
  events: Array<AgentEvent>;
  /** The id of the last event: pass it as `cursor` to get only newer ones. */
  next_cursor: number;
}

/** One line of a streamed exec (`stream: true`): `started` first, then `stdout`/`stderr` chunks as they arrive, then exactly one `exit` or `error`. */
export type ExecEvent = {
  command_id?: string | null;
  type: "started";
} | {
  data: string;
  type: "stdout";
} | {
  data: string;
  type: "stderr";
} | {
  exit_code: number;
  oom_killed?: boolean;
  timed_out: boolean;
  type: "exit";
} | {
  code: string;
  message: string;
  type: "error";
};

export interface ExecRequest {
  command: string;
  /** Run in this directory (relative to the home directory, or absolute). */
  cwd?: string | null;
  /** Extra environment variables for this command only. */
  env?: Record<string, string> | null;
  /** 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 | null;
  /** Like `stdin`, base64-encoded (for binary input; at most 1 MiB decoded). */
  stdin_b64?: string | null;
  /** true: answer with `application/x-ndjson`, one `ExecEvent` per line as output arrives (also chosen by `Accept: application/x-ndjson`). */
  stream?: boolean | null;
  timeout_secs?: number | null;
}

export interface ExecResult {
  /** The command's record in the history (`GET /sandboxes/{name}/command-history/{id}`). */
  command_id?: string | null;
  /** 124 when it timed out. */
  exit_code: number;
  /** 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). */
  oom_killed?: boolean;
  stderr: string;
  stdout: string;
  /** It ran past `timeout_secs` and was killed; stdout and stderr hold what it printed until then. */
  timed_out?: boolean;
}

/** Starts past the start limits (per minute, hour, day), paid from the wallet. Off by default. */
export interface ExtraStarts {
  /** They can run now: on, with a paid plan (not the trial or a plan set to end) and money in the wallet. */
  available: boolean;
  /** 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: Array<StartBracket>;
  /** Turned on for this account (`PUT /billing/extra-starts`). */
  enabled: boolean;
  /** Past this multiple of the per-minute limit, a start fails even when paid. */
  max_minute_multiple: number;
}

/** `POST /feedback`: a message to the froog team (replies go to the account email). */
export interface Feedback {
  message: string;
}

/** One entry of a directory listing. `kind`: file | dir | link | other. */
export interface FileEntry {
  kind: string;
  modified: number;
  name: string;
  size: number;
}

/** 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). */
export interface FileEvent {
  dir?: boolean | null;
  event?: string | null;
  message?: string | null;
  method?: string | null;
  /** As inotify or find print it: absolute if the watched path was, else relative to ~. */
  path?: string | null;
  reason?: string | null;
  type: string;
}

/** `GET /sandboxes/{name}/files/find?path=&pattern=&regex=`: lines that contain a text. */
export interface FileFind {
  errors?: Array<string>;
  /** At most 1,000. */
  matches: Array<FileMatch>;
  truncated: boolean;
}

/** `GET /sandboxes/{name}/files?path=`: a file's details, or a directory and its entries. */
export interface FileInfo {
  entries?: Array<FileEntry> | null;
  kind: string;
  modified: number;
  path: string;
  size: number;
}

export interface FileMatch {
  /** The line, cut at 500 characters. */
  content: string;
  file: string;
  line: number;
}

/** `POST /sandboxes/{name}/files/permissions`: chmod. The owner (the sandbox user) stays. */
export interface FilePermissions {
  /** Octal, e.g. 755 or 0600. */
  mode: string;
  path: string;
  /** Also everything inside a directory. */
  recursive?: boolean;
}

/** `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). */
export interface FileReplace {
  new_value: string;
  paths: Array<string>;
  pattern: string;
}

export interface FileReplaced {
  error?: string | null;
  file: string;
  replacements?: number | null;
  success: boolean;
}

/** `GET /sandboxes/{name}/files/search?path=&pattern=`: paths whose name matches a glob. */
export interface FileSearch {
  /** Parts of the tree that couldn't be searched (the results are then incomplete). */
  errors?: Array<string>;
  /** Absolute paths, at most 10,000. */
  files: Array<string>;
  truncated: boolean;
}

/** A URL that GETs (download) or PUTs (upload, raw bytes) one file with no Authorization header. */
export interface FileUrl {
  expires_at: number;
  method: string;
  url: string;
}

export interface ForkSandbox {
  count?: number | null;
  /** Environment variables set on top of the source's own (these win). */
  env?: Record<string, string> | null;
  /** 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?: boolean | null;
  /** Move the copies to this environment's latest version. Default: exactly the source's environment and version. */
  environment_name?: string | null;
  /** Labels set on top of the source's own (these win). */
  labels?: Record<string, string> | null;
  /** Override the source's monitoring setting. */
  monitoring?: boolean | null;
  name: string;
  /** Size of the new sandboxes; defaults to the source's. */
  size?: Size | null;
  /** Snapshot id or label. Defaults to a fresh snapshot (running) or the latest one (stopped). */
  snapshot?: string | null;
  /** false: never snapshot the copies; a stop erases them. Default: the account's setting. */
  snapshots?: boolean | null;
  /** Auto-stop timer of the copies in seconds (0 = no auto-stop). Default: the source's. */
  ttl_secs?: number | null;
}

/** `POST /v1/github/connect`: where to install the app (or pick another account). */
export interface GithubConnect {
  url: string;
}

/** A GitHub App installation linked to the account. */
export interface GithubInstallation {
  /** The GitHub user or organization it is installed on. */
  account: string;
  /** User | Organization */
  account_type: string;
  created_at: number;
  /** GitHub's installation id. */
  id: number;
  /** all | selected (the repositories GitHub lets the app see) */
  repository_selection: string;
}

/** A repository the app can see (`GET /v1/github/repos`). */
export interface GithubRepo {
  /** https://github.com/owner/name.git */
  clone_url: string;
  default_branch?: string | null;
  /** owner/name */
  full_name: string;
  id: number;
  installation_id: number;
  private: boolean;
}

/** `GET /v1/github`. */
export interface GithubStatus {
  /** The `runs-on` label of CI jobs that run on froog. */
  ci_label: string;
  /** This server has a GitHub App set up (otherwise every other GitHub call is 404 github_not_configured). */
  configured: boolean;
  installations: Array<GithubInstallation>;
  /** `owner/name` of the repositories the token in each sandbox may use; empty = every repository the app can see. */
  token_repos: Array<string>;
}

/** A short-lived GitHub installation token (one hour). */
export interface GithubToken {
  expires_at: number;
  /** The GitHub account it works for. */
  owner: string;
  /** The repositories it is limited to; empty = every repository of the installation. */
  repositories: Array<string>;
  token: string;
}

/** `POST /v1/sandboxes/{name}/github-token`. */
export interface GithubTokenRequest {
  /** The GitHub user or organization (default: the first linked installation). */
  owner?: string | null;
}

/** An email address that receives mail for an agent (`/v1/inboxes`). Receive-only for now. */
export interface Inbox {
  /** The full address, e.g. `signup-k3f9qz@froogmail.com`. */
  address: string;
  created_at: number;
  id: string;
  last_message_at?: number | null;
  /** Messages kept (older than the retention period are deleted). */
  messages?: number;
  /** Your label for it (not part of the address). */
  name?: string;
  /** The sandbox it belongs to, if any. */
  sandbox?: string | null;
}

/** What the account pays for inboxes (`GET /v1/inboxes/billing`). */
export interface InboxBilling {
  /** Whether inboxes can be made and receive mail now. */
  active: boolean;
  cancel_at_period_end?: boolean;
  period_end?: number | null;
  /** One inbox a month, in US cents. */
  price_cents: number;
  /** Whether this account pays for inboxes (false: billing-exempt, or a server without payments). */
  required: boolean;
  /** Inboxes paid for this period. */
  seats: number;
  /** The Stripe subscription's status (active, past_due, ...); none without one. */
  status?: string | null;
  /** Inboxes the account has. */
  used: number;
}

/** `POST /v1/inboxes/billing/checkout`: how many inboxes to buy (default: yours plus one). */
export interface InboxCheckout {
  quantity?: number | null;
}

/** A received message. Lists leave out `text` and `html`; get one message for them. */
export interface InboxMessage {
  /** One-time codes found in the subject and body, most likely first. */
  codes?: Array<string>;
  /** The SMTP envelope sender (bounce address); may differ from `from`. */
  envelope_from?: string;
  /** The From header's address. */
  from: string;
  from_name?: string;
  html?: string | null;
  id: string;
  inbox_id: string;
  /** Verification, confirmation and sign-in links found in the body, most likely first. */
  links?: Array<string>;
  /** The first 200 characters of the text. */
  preview?: string;
  received_at: number;
  size: number;
  subject: string;
  text?: string | null;
  to: string;
}

export interface InboxMessageList {
  /** Newest first. */
  messages: Array<InboxMessage>;
}

/** `GET /v1/inboxes/{inbox}/wait`: the first matching message, or none when the wait timed out. */
export interface InboxWait {
  message?: InboxMessage | null;
}

export interface InterruptRequest {
  /** Only this conversation (default: every conversation on the sandbox). */
  conversation_id?: string | null;
}

export interface InterruptResult {
  /** The prompts stopped (running) or cancelled (queued). */
  interrupted: Array<string>;
}

/** An opened port's public IPv4 address: the host's address and a port of its own, forwarded to the sandbox's port. */
export interface Ipv4Port {
  /** Connect here over IPv4: "203.0.113.7:40001". */
  address: string;
  /** The sandbox's port, "8080/tcp". */
  port: string;
  /** The port on the host's IPv4 address that forwards to it. */
  public_port: number;
}

/** 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). */
export interface Job {
  /** First 200 characters. */
  command: string;
  /** The job's record in the command history (None for jobs froog started itself, like setup). */
  command_id?: string | null;
  exit_code?: number | null;
  id: string;
  started_at: number;
  state: string;
}

/** 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. */
export interface JobOutput {
  exit_code?: number | null;
  id: string;
  next_offset: number;
  output: string;
  output_size: number;
  started_at: number;
  state: string;
}

/** Write to a running job's standard input (one started with `stdin_open`), and/or close it. */
export interface JobStdin {
  /** Close the input after writing: the command reads end of file. */
  close?: boolean | null;
  /** Text to write (at most 1 MiB). */
  data?: string | null;
  /** Like `data`, base64-encoded (at most 1 MiB decoded). */
  data_b64?: string | null;
}

export interface JsonRpcError {
  /** -32700 parse error, -32600 invalid request, -32601 unknown method, -32602 bad params. */
  code: number;
  message: string;
}

/** 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. */
export interface JsonRpcRequest {
  id?: unknown;
  /** Always "2.0". */
  jsonrpc: string;
  /** initialize, ping, tools/list, tools/call, notifications/initialized, … */
  method: string;
  params?: unknown;
}

/** 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. */
export interface JsonRpcResponse {
  error?: JsonRpcError | null;
  id: unknown;
  jsonrpc: string;
  result?: unknown;
}

/** What an account may do now: `GET /v1/limits`. */
export interface Limits {
  /** Sandboxes at once (lower during the free trial). */
  max_sandboxes: number;
  /** Only when another plan fits the last 30 days' usage better. */
  plan_nudge?: PlanNudge | null;
  /** Running or starting now. */
  running: number;
  /** Saved snapshots included in the plan vs kept now. */
  saved_snapshot_allowance?: SavedSnapshotAllowance;
  /** Sizes the account can start. */
  sizes: Array<Size>;
  starts: StartLimits;
  /** The free trial; None when this account has none (billing off, or exempt). */
  trial?: Trial | null;
}

export interface MakeDir {
  /** Octal permissions of the new directory, e.g. 700 (default: the umask's, usually 755). */
  mode?: string | null;
  path: string;
}

/** `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. */
export interface MetricsHistory {
  points: Array<MetricsPoint>;
  /** The window, unix seconds. */
  since: number;
  /** Each point averages this many seconds (a multiple of 30; at most 1000 points). */
  step_secs: number;
  until: number;
}

export interface MetricsPoint {
  /** The bucket's start, unix seconds. */
  at: number;
  cpu_max_percent?: number | null;
  /** Share of all CPUs busy (0-100), the average and the highest sample in the bucket. */
  cpu_percent?: number | null;
  /** Of the root filesystem. */
  disk_used_percent?: number | null;
  memory_total_mb?: number | null;
  memory_used_mb?: number | null;
  /** Samples in the bucket. */
  samples: number;
}

export interface MoveFile {
  from: string;
  to: string;
}

export interface NewApiKey {
  actions?: Array<string> | null;
  /** Limit the key to sandboxes of these environments (names). Sandboxes it creates use the first one unless they name another of them. */
  environments?: Array<string> | null;
  label?: string | null;
  /** 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?: Record<string, string> | null;
  /** A key that works until revoked. */
  never_expires?: boolean;
  /** read-only | full-sandbox | ci | admin (`scopes::PRESETS`). Use this or `actions`. */
  preset?: string | null;
  /** Limit the key to these sandboxes (names), plus any it creates. */
  sandboxes?: Array<string> | null;
  /** Days until it expires (1-365). Default 90. */
  ttl_days?: number | null;
}

/** `POST /environments` */
export interface NewEnvironment {
  /** Start with a copy of this environment's latest version (repos, scripts, secret files). Default: empty. */
  copy_from?: string | null;
  /** Make it the default right away. */
  default?: boolean | null;
  name: string;
}

/** A file to write into a new sandbox (`CreateSandbox.files`). */
export interface NewFile {
  content: string;
  /** utf8 (default) or base64 (for binary files). */
  encoding?: string | null;
  /** Relative to the home directory, e.g. `app/.env`. */
  path: string;
}

/** `POST /sandboxes/{name}/files/download-url` or `/upload-url`. */
export interface NewFileUrl {
  /** Seconds the URL works (60-86400, default 900). */
  expires_in?: number | null;
  path: string;
}

/** Start a background command: `POST /sandboxes/{name}/jobs`. */
export interface NewJob {
  command: string;
  /** Run in this directory (relative to the home directory, or absolute). */
  cwd?: string | null;
  /** Extra environment variables for this command only. */
  env?: Record<string, string> | null;
  /** "detached" (default) or "stream" (`froog exec --timeout 0` follows it); shown in the history. */
  mode?: string | null;
  /** 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 | null;
  /** Like `stdin`, base64-encoded (at most 1 MiB decoded). */
  stdin_b64?: string | null;
  /** 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. */
  stdin_open?: boolean | null;
}

/** `POST /sandboxes/{name}/urls`: host a port, or change how an already hosted port is reached. */
export interface NewPublicUrl {
  port: number;
  /** 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?: boolean;
  /** Rename the URL to `https://<subdomain>.<domain>` (3-40 lowercase letters, digits, hyphens). */
  subdomain?: string | null;
  /** A label for people (at most 100 characters); "" removes it. Hosting again without it keeps the old one. */
  title?: string | null;
}

/** `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. */
export interface NewUrlToken {
  /** Who or what it's for (at most 100 characters). */
  label?: string | null;
  /** Seconds until it stops working: 60 to 30 days, default 1 day. */
  ttl_secs?: number | null;
}

export interface NewWebhook {
  events?: Array<string>;
  name?: string | null;
  url: string;
}

/** `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). */
export interface OpenPort {
  port: number;
  /** tcp (default) or udp. */
  protocol?: string;
}

/** One workspace in `GET /orgs`: your personal account first, then each organization. */
export interface OrgSummary {
  /** The active workspace: requests that name none use it (CLI, account API keys). */
  active: boolean;
  /** `personal`, or the organization id (`org_…`). */
  id: string;
  /** personal | org */
  kind: string;
  /** Members (1 for personal). */
  members?: number;
  name: string;
  /** Your role: owner | admin | member (`owner` for personal). */
  role: string;
}

/** 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"). */
export type Os = "ubuntu-24.04" | "ubuntu-26.04" | "debian-13" | "omarchy";

/** An OS sandboxes can start with: `GET /v1/images`. */
export interface OsImage {
  default: boolean;
  id: Os;
  name: string;
}

/** What choosing a plan did: a Checkout page to open (first plan), or the plan changed. */
export interface PlanChange {
  billing: Billing;
  /** Open this Stripe page to subscribe (and save a card). None = changed already. */
  url?: string | null;
}

/** A plan on offer. */
export interface PlanInfo {
  /** Sandboxes at once. */
  max_sandboxes: number;
  /** Saved snapshots included. */
  saved_snapshots: number;
  /** Dollars a month, all of it usable as running time and storage. */
  usd: number;
  /** The plan can start `xlarge` sandboxes. */
  xlarge?: boolean;
}

/** In `GET /limits`, only when another plan fits the last 30 days' usage better. */
export interface PlanNudge {
  /** The suggestion, with the sum. */
  text: string;
  /** The plan that fits, dollars a month. */
  usd: number;
  /** Running time used in the last 30 days, at list prices. */
  used_usd: number;
}

/** 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`). */
export interface PortEvent {
  message?: string | null;
  port?: number | null;
  protocol?: string | null;
  reason?: string | null;
  type: string;
}

/** The answer to `POST /sandboxes/{name}/wait`. */
export interface PortWait {
  port: number;
  /** true: something listens on the port (on any address); false: the timeout passed first. */
  ready: boolean;
  /** How long it waited, in milliseconds. */
  waited_ms: number;
}

/** 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. */
export interface Ports {
  /** 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. */
  from_urls?: Array<string>;
  /** 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?: Array<Ipv4Port>;
  /** Why some opened ports have no IPv4 address (stopped, a limit reached, the host's range full). */
  ipv4_note?: string | null;
  /** Everything open besides SSH, as "8080/tcp" or "27015/udp", sorted. */
  ports: Array<string>;
}

/** A process in a sandbox (`GET /sandboxes/{name}/processes`), busiest first. */
export interface ProcessInfo {
  command: string;
  /** The history record of the command this process belongs to, if any. */
  command_id?: string | null;
  cpu_percent: number;
  memory_mb: number;
  pid: number;
  user: string;
}

/** `live`: false when the sandbox isn't running and this is the last sample kept (`sampled_at`). */
export interface ProcessList {
  live: boolean;
  processes: Array<ProcessInfo>;
  sampled_at?: number | null;
  total: number;
}

export interface ProcessRecord {
  /** Its command line (up to 1024 characters). */
  command: string;
  /** The first sample it was missing from: it ended between `last_seen_at` and this. None while it runs. */
  ended_at?: number | null;
  /** The last sample it was in. */
  last_seen_at: number;
  pid: number;
  ppid: number;
  /** Unix seconds (from its elapsed time when first seen). */
  started_at: number;
  user: string;
}

/** `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. */
export interface ProcessRecords {
  records: Array<ProcessRecord>;
}

/** `GET /sandboxes/{name}/processes/samples`: the 12 busiest processes, sampled every 30 seconds while the sandbox runs and kept 7 days, newest first. */
export interface ProcessSamples {
  samples: Array<ProcessList>;
}

/** One prompt (a turn of a conversation). */
export interface Prompt {
  agent: string;
  /** Paths of its attachments in the sandbox. */
  attachments?: Array<string>;
  conversation_id: string;
  created_at: number;
  /** done, failed or interrupted. */
  done: boolean;
  effort?: string | null;
  /** Why it failed (failed prompts). */
  error?: string | null;
  /** e.g. provider_not_configured, timed_out, sandbox_stopped. */
  error_code?: string | null;
  exit_code?: number | null;
  fast?: boolean | null;
  finished_at?: number | null;
  id: string;
  model?: string | null;
  /** The prompt text (empty for a sandbox with monitoring off, once it started). */
  prompt: string;
  /** The agent's final answer (done prompts). */
  result?: string | null;
  sandbox: string;
  started_at?: number | null;
  /** queued | running | done | failed | interrupted */
  status: string;
}

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

export interface PromptList {
  prompts: Array<Prompt>;
}

export interface PromptRequest {
  /** The harness: claude (default), codex, gemini, opencode, pi, amp, cursor, prime, kimi or mistral. */
  agent?: string | null;
  /** Images, PDFs or other files for the agent: up to 12, 25 MB each, 64 MB in total. */
  attachments?: Array<PromptAttachment> | null;
  /** Continue this conversation (default: the sandbox's most recent one). */
  conversation_id?: string | null;
  /** Reasoning effort / thinking level, for harnesses that take one (claude, codex, pi, prime, kimi). */
  effort?: string | null;
  /** Fast mode (same model, faster output at the provider's fast rate): claude (Opus) and codex. false forces standard speed. */
  fast?: boolean | null;
  /** Passed to the harness's model flag, e.g. "opus", "gpt-6.1-sol", "anthropic/claude-sonnet-4-5" (`GET /provider-models`). */
  model?: string | null;
  /** Start a new conversation. */
  new?: boolean | null;
  prompt: string;
  timeout_secs?: number | null;
}

/** What `POST /sandboxes/{name}/prompt` (wait for the answer) returns. */
export interface PromptResult {
  conversation_id: string;
  /** 0 when the agent finished; 124 when it timed out. */
  exit_code: number;
  prompt_id: string;
  /** done | failed | interrupted | running (still running when the wait timed out) */
  status: string;
  /** Why it failed, if it did. */
  stderr: string;
  /** The agent's final answer. */
  stdout: string;
  timed_out: boolean;
}

/** One model of a harness in the catalog. */
export interface ProviderModel {
  /** Accepted `effort` levels (empty: none). */
  efforts: Array<string>;
  /** Takes `fast: true`. */
  fast_mode: boolean;
  id: string;
  title: string;
}

/** A harness and the models it accepts. */
export interface ProviderModels {
  /** The `agent` value. */
  agent: string;
  aliases: Array<string>;
  /** Takes model ids beyond `models` (e.g. `openrouter/…`, full model ids). */
  any_model: boolean;
  /** Effort levels of the harness. */
  efforts: Array<string>;
  /** Has a fast mode. */
  fast_mode: boolean;
  /** Env vars it runs on (any one). */
  keys: Array<string>;
  /** Model ids the harness listed in the sandbox (`?sandbox=`; harnesses that can list them). */
  live_models?: Array<string>;
  models: Array<ProviderModel>;
  /** Takes a message in the middle of a running turn (`steer`). */
  steer: boolean;
  /** Takes `model` at all. */
  takes_model: boolean;
  title: string;
}

/** A public HTTPS URL for a port inside a sandbox. */
export interface PublicUrl {
  created_at: number;
  port: number;
  /** false: only people with the token (in the URL, or the `__Host-froog_url` cookie) get through. */
  public?: boolean;
  /** A label for people, e.g. "API docs". */
  title?: string | null;
  /** For a private URL, includes `?_token=…`: treat it like a password. */
  url: string;
}

/** What a size costs to run, billed per second. */
export interface Rate {
  per_hour_usd: number;
  /** A full month (730 hours). */
  per_month_usd: number;
  size: Size;
}

export interface Restore {
  snapshot: string;
}

export interface Sandbox {
  /** 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 | null;
  /** 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). */
  auto_start?: boolean;
  /** 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. */
  auto_stop_at?: number | null;
  backend: string;
  /** Unix seconds. */
  created_at: number;
  /** 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). */
  created_by?: string | null;
  /** Deleted after staying stopped this many seconds. 0 = never. */
  delete_after_secs?: number;
  /** While stopped with `delete_after_secs`: when it will be deleted (unix seconds). */
  delete_at?: number | null;
  /** Set once the disk of a stopped sandbox was deleted; start and fork then fail. */
  disk_expired?: boolean;
  /** For a stopped sandbox: when its disk will be deleted (save a snapshot to keep it longer). */
  disk_expires_at?: number | null;
  /** Disk use of a running sandbox in percent (checked every 30 seconds). */
  disk_used_percent?: number | null;
  /** Names of its own environment variables (values are never returned). */
  env?: Array<string>;
  /** false: it runs without its environment (`--no-environment`). */
  environment?: boolean;
  /** The environment it belongs to (`default` unless it was created with another), also when it runs without it (`environment: false`). */
  environment_name?: string | null;
  /** 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`). */
  environment_version?: number | null;
  error?: string | null;
  /** A deadline: deleted at this time (unix seconds) whatever its state, like a TTL from creation (`expires_in_secs`). Saved snapshots stay. */
  expires_at?: number | null;
  /** 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). */
  forked_from?: SandboxOrigin | null;
  ipv4?: string | null;
  /** 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 | null;
  /** Your own key-value tags (`label=key=value` filters lists). */
  labels?: Record<string, string>;
  last_active_at: number;
  /** 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. */
  last_reboot?: number | null;
  /** Stopped this many seconds after each start, busy or not. 0 = no limit. */
  max_lifetime_secs?: number;
  /** false: no command history or process samples. */
  monitoring?: boolean;
  name: string;
  os?: Os;
  /** 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). */
  period_cost_usd?: number | null;
  /** While creating or starting: what takes the time, e.g. copying its disk from another host. */
  progress?: string | null;
  /** The repo cloned into this sandbox at create (`CreateSandbox.repo`). */
  repo?: SandboxRepo | null;
  /** Why setup failed: its error lines (a failed clone, a script's exit code). */
  setup_error?: string | null;
  /** The setup job (repos, the environment's setup script, `new --setup`) of the last boot: none | running | done | failed. */
  setup_status?: string;
  size: Size;
  /** false: never snapshotted; a stop erases it (no start, fork or restore). */
  snapshots?: boolean;
  /** SSH port at `ipv4` (VMs on a shared host each have their own). */
  ssh_port?: number;
  started_at?: number | null;
  state: State;
  /** While running with `max_lifetime_secs`: when it will be stopped (unix seconds). */
  stop_at?: number | null;
  /** Why the stop was refused: the last snapshot error. */
  stop_error?: string | null;
  /** Only in the answer to `POST /sandboxes/{name}/stop`: the stop's id, for `GET /stops/{id}`. */
  stop_id?: string | null;
  /** 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. */
  stop_refused_at?: number | null;
  /** 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. */
  ttl_secs: number;
  /** 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. */
  unresponsive_since?: number | null;
  /** In an organization: `shared` (every member can use it) or `private` (only its creator). */
  visibility?: string | null;
  /** 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). */
  warm_until?: number | null;
}

/** One sandbox's AI settings and usage (`GET /v1/sandboxes/{name}/ai`). */
export interface SandboxAi {
  /** Its AI requests are refused. */
  blocked: boolean;
  days: number;
  /** The profile it uses (its own, its environment's, or the default). */
  profile?: string | null;
  /** Set on the sandbox itself (else it follows its environment). */
  profile_set: boolean;
  /** It holds placeholders now (the proxy was on at its last start). */
  proxied: boolean;
  sandbox: string;
  totals: AiTotals;
  usage: Array<AiUsageRow>;
}

/** `GET /sandboxes/{name}/metrics`: one sample read from the running sandbox now. */
export interface SandboxMetrics {
  cpu_count: number;
  /** Share of all CPUs busy over half a second (0-100). */
  cpu_percent: number;
  disk_total_bytes: number;
  /** Of the root filesystem. */
  disk_used_bytes: number;
  /** Share of the last minute some task waited for disk I/O (0-1). */
  io_pressure: number;
  memory_total_bytes: number;
  memory_used_bytes: number;
  /** Received and sent since boot, all interfaces but loopback. */
  network_rx_bytes: number;
  network_tx_bytes: number;
  /** Unix seconds. */
  sampled_at: number;
}

/** `Sandbox.forked_from`: the sandbox and snapshot a fork (or a start from a saved snapshot) copied its disk from. */
export interface SandboxOrigin {
  /** Unix seconds: when the copy was made. */
  at: number;
  /** The snapshot's name, when it has one. */
  label?: string | null;
  /** The source sandbox's name (it may have been deleted or renamed since). */
  sandbox: string;
  /** The snapshot's id (`snap_…`); absent for a fork of the source's state at that moment. */
  snapshot?: string | null;
}

/** 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. */
export interface SandboxRepo {
  /** Branch or tag to check out (default: the repo's default branch). */
  branch?: string | null;
  /** Directory relative to the home directory (default: the repo's name). */
  dir?: string | null;
  /** `https://`, `ssh://` or `git@` URL. */
  url: string;
}

/** `GET /sandboxes/{name}/usage`: one sandbox's running time and cost in a window. */
export interface SandboxUsage {
  /** At list prices (saved snapshots are billed per account, not here). */
  cost_usd: number;
  /** The window, unix seconds (`to` is at most now). */
  from: number;
  /** One line per size it ran at. */
  lines: Array<UsageLine>;
  name: string;
  /** Running time in the window, billed per second. */
  seconds: number;
  to: number;
}

/** A saved snapshot of the account (`GET /saved-snapshots`): a template for `new --from NAME`. It outlives its source sandbox. */
export interface SavedSnapshot {
  created_at: number;
  id: string;
  name: string;
  /** A copy is kept off the host, on separate storage (encrypted). */
  offsite?: boolean;
  os: Os;
  /** It came from another account's share code (`new --from fsh_…`); `source` is the code's id. */
  shared?: boolean;
  size: Size;
  size_gb?: number | null;
  /** The sandbox it was saved from. */
  source: string;
  /** The source sandbox was deleted (the saved snapshot stays). */
  source_deleted: boolean;
  /** creating | available */
  status: string;
}

/** Saved snapshots the plan includes and how many the account keeps (`GET /limits`). */
export interface SavedSnapshotAllowance {
  /** Above the included ones, billed per day. */
  extra: number;
  included: number;
  price_per_extra_day_usd: number;
  /** Saved snapshots (names) the account keeps now. */
  used: number;
}

/** Which environment's sandboxes get which profile (`PUT /v1/ai/environments/{env}`). */
export interface SetAiEnvironmentProfile {
  /** A profile name or id; null: the account's default profile. */
  profile?: string | null;
}

/** `PUT /environment/files` */
export interface SetEnvFile {
  content: string;
  path: string;
}

export interface SetEnvVar {
  value: string;
}

/** `PUT /v1/billing/extra-starts`. */
export interface SetExtraStarts {
  enabled: boolean;
}

/** `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. */
export interface SetPorts {
  ports: Array<string>;
}

/** `PUT /v1/account/primary`: the connection whose address gets the account's emails. */
export interface SetPrimary {
  /** A connection id (`con_…`). */
  connection: string;
}

/** `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": "…"}`. */
export interface SetTelemetry {
  endpoint: string;
  headers?: Record<string, string>;
}

/** `PUT /v1/github/token-repos`: limit the token in each sandbox to these repositories. */
export interface SetTokenRepos {
  /** `owner/name`; empty = every repository the app can see. */
  repos: Array<string>;
}

/** Machine sizes. Mapped to provider server types by the server's config. */
export type Size = "small" | "default" | "large" | "xlarge" | "standard";

export interface Snapshot {
  created_at: number;
  id: string;
  label?: string | null;
  /** 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?: boolean;
  /** The sandbox it belongs to (in account-wide lists and single lookups). */
  sandbox?: string | null;
  /** Saved snapshots are kept until deleted and billed per day; automatic ones are free. */
  saved: boolean;
  size_gb?: number | null;
  status: string;
}

/** 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. */
export interface SnapshotShare {
  created_at: number;
  /** `shr_…` */
  id: string;
  last_used_at?: number | null;
  /** The saved snapshot's name. */
  name: string;
  /** Set once revoked: the code creates nothing any more. */
  revoked_at?: number | null;
  /** Sandboxes created from it so far. */
  uses: number;
}

export interface SshInfo {
  host: string;
  /** OpenSSH public host key line, to pin in known_hosts. */
  host_key: string;
  port: number;
  user: string;
}

export interface SshKey {
  created_at: number;
  id: string;
  name: string;
  public_key: string;
}

/** Starts up to `up_to` times the included number cost `price_usd` each. */
export interface StartBracket {
  price_usd: number;
  up_to: number;
}

/** Sandbox starts (create, start, fork, restore) allowed per window; 0 = no limit. */
export interface StartLimits {
  per_day: number;
  per_hour: number;
  per_minute: number;
  used_day: number;
  used_hour: number;
  used_minute: number;
}

/** Optional body of `POST /sandboxes/{name}/start`. */
export interface StartSandbox {
  /** Environment variables set on top of the sandbox's own (these win; kept). */
  env?: Record<string, string> | null;
  /** 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?: boolean | null;
  /** Move it to this environment's latest version (it also gets the environment again). */
  environment_name?: string | null;
  /** Start at a different size. */
  size?: Size | null;
  /** A new auto-stop timer in seconds (0 = no auto-stop). Default: keep it. */
  ttl_secs?: number | null;
}

export interface StartSubscriptionLogin {
  /** 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;
}

export type State = "creating" | "running" | "stopping" | "stopped" | "starting" | "restoring" | "deleting" | "deleted" | "error";

export interface SteerRequest {
  /** The conversation to steer (default: the sandbox's current one). */
  conversation_id?: string | null;
  message: string;
}

export interface SteerResult {
  conversation_id: string;
  /** `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;
  /** The harness took the message into the running turn. */
  native: boolean;
  /** The running prompt the message went to. */
  prompt_id: string;
  queued_prompt_id?: string | null;
}

/** 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. */
export interface Stop {
  ended_at?: number | null;
  error?: string | null;
  /** `stop_…` */
  id: string;
  /** Unix seconds. */
  requested_at: number;
  /** The sandbox's name. */
  sandbox: string;
  /** 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;
}

/** Optional body of `POST /sandboxes/{name}/stop`. */
export interface StopSandbox {
  /** 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?: boolean;
}

/** A subscription sign-in (Claude Pro/Max for Claude Code, ChatGPT for Codex). */
export interface Subscription {
  /** The harness it signs in. */
  agent: string;
  connected: boolean;
  connected_at?: number | null;
  /** A sign-in in progress. */
  login?: SubscriptionLogin | null;
  /** claude | chatgpt */
  provider: string;
  title: string;
}

export interface SubscriptionCode {
  code: string;
}

/** 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. */
export interface SubscriptionLogin {
  error?: string | null;
  /** Paste the code the vendor's page shows into `POST …/login/code`. */
  needs_code: boolean;
  provider: string;
  /** The sandbox the vendor's own CLI runs the sign-in in. */
  sandbox: string;
  started_at: number;
  /** starting | waiting_for_user | connected | failed */
  status: string;
  url?: string | null;
  user_code?: string | null;
}

/** `GET /telemetry`: the account's OpenTelemetry endpoint (header values are never returned). */
export interface Telemetry {
  endpoint: string;
  /** Header names only. */
  headers: Array<string>;
  last_error?: string | null;
  last_export_at?: number | null;
}

export interface TelemetrySaved {
  telemetry: Telemetry;
  test: TelemetryTest;
}

/** The collector's answer to the test export sent on save. */
export interface TelemetryTest {
  message?: string | null;
  ok: boolean;
  status?: number | null;
}

export interface Trial {
  /** In the trial now. Before choosing a plan this is false and `ends_at` None: the trial starts with the first plan. */
  active: boolean;
  /** The card already had a free trial on another account (`trial_blocked`): the plan is paid from the start. */
  blocked?: boolean;
  days: number;
  /** Unix seconds; None until the trial starts. */
  ends_at?: number | null;
  /** Hours of running time the trial includes (all sandboxes together). */
  hours?: number;
  /** Hours used so far. */
  hours_used?: number;
  /** 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. */
  max_auto_stop_secs?: number;
  /** Sandboxes at once during the trial. */
  max_sandboxes: number;
}

export interface UpdateAiProfile {
  /** true: make it the account's default profile. */
  default?: boolean | null;
  /** Rename. */
  name?: string | null;
}

/** Change the proxy settings (`PUT /v1/ai/proxy`); fields left out stay. */
export interface UpdateAiProxy {
  blocked?: boolean | null;
  enabled?: boolean | null;
  withhold_subscriptions?: boolean | null;
}

/** `PATCH /v1/account/alerts`: `{"emails": {"plan_nudge": false}, "auto_rebuild": true}`; kinds left out stay as they are. */
export interface UpdateAlertSettings {
  auto_rebuild?: boolean | null;
  emails?: Record<string, boolean>;
}

export interface UpdateDataRetention {
  /** Turning zero data retention on erases the data of every stopped sandbox: send "delete stopped sandbox data" to confirm. */
  confirmation?: string | null;
  monitoring_off?: boolean | null;
  snapshots_off?: boolean | null;
  zero_data_retention?: boolean | null;
}

/** `PATCH /environment`: fields left out stay as they are; `setup: ""` removes the script. */
export interface UpdateEnvironment {
  /** true: make it the default (the one new sandboxes use when they name none). */
  default?: boolean | null;
  /** Rename it (sandboxes stay on their versions). */
  name?: string | null;
  /** Setup scripts per repo directory; `""` removes one. Scripts not named stay. */
  repo_setup?: Record<string, string> | null;
  repos?: Array<string> | null;
  setup?: string | null;
}

/** `PATCH /v1/inboxes/{inbox}`: rename it, or attach it to a sandbox (`""` detaches it). */
export interface UpdateInbox {
  name?: string | null;
  sandbox?: string | null;
}

/** `PATCH /sandboxes/{name}`: rename, change the auto-stop timer, or change labels. */
export interface UpdateSandbox {
  /** true: while stopped, a request to one of its public URLs, or an exec, file, job or wait call, starts it; false: off. */
  auto_start?: boolean | null;
  /** Delete it after staying stopped this many seconds (0 = never). */
  delete_after_secs?: number | null;
  /** Delete it this many seconds from now, running or not (60 s to 365 days); 0 removes the deadline. */
  expires_in_secs?: number | null;
  /** Labels to set; a null value removes that label. Labels not named stay. */
  labels?: Record<string, string | null> | null;
  /** Stop it this many seconds after each start (0 = no limit). Set while it runs, it counts from that start. */
  max_lifetime_secs?: number | null;
  name?: string | null;
  /** 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. */
  ttl_secs?: number | null;
}

/** Change a sandbox's AI settings (`PUT /v1/sandboxes/{name}/ai`). */
export interface UpdateSandboxAi {
  /** 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?: boolean | null;
  /** 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 | null;
}

/** `PATCH /webhooks/{id}`: change what's given. The signing secret stays (rotate changes it). */
export interface UpdateWebhook {
  enabled?: boolean | null;
  /** Empty = every event. */
  events?: Array<string> | null;
  /** An empty name removes it. */
  name?: string | null;
  url?: string | null;
}

/** Optional body of `POST /environments/{env}/upgrade`. */
export interface UpgradeEnvironment {
  /** Only these sandboxes (names). Default: every sandbox of the environment on an older version. */
  sandboxes?: Array<string> | null;
}

/** A viewer token of a private URL (`GET /sandboxes/{name}/urls/{port}/tokens`). */
export interface UrlToken {
  created_at: number;
  expires_at: number;
  /** `utk_…`, for revoking it. */
  id: string;
  label?: string | null;
  /** 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 | null;
  /** Only when created: the URL with `?_token=` for this token. */
  url?: string | null;
}

export interface Usage {
  /** Usage past the plan, taken from the wallet. */
  billable_usd: number;
  /** The period is the free trial: nothing is charged before this time (unix seconds). */
  free_trial_until?: number | null;
  /** 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?: number;
  /** What the plan paid for this period covers. */
  included_usd?: number;
  lines: Array<UsageLine>;
  /** "YYYY-MM" of the period's start. */
  month: string;
  saved_snapshot_days: number;
  snapshot_cost_usd: number;
  to?: number;
  /** Everything used in the period, at list prices. */
  total_usd: number;
}

export interface UsageLine {
  /** `froog`: a sandbox's running time. `ci`: CI job time (`runs-on: froog`), priced per vCPU minute. */
  backend: string;
  cost_usd: number;
  /** Running time (or CI job time) in the period, billed per second. */
  seconds: number;
  size: Size;
}

/** `POST /sandboxes/{name}/wait`: wait until something listens on a TCP port inside the sandbox. */
export interface WaitForPort {
  port: number;
  /** Give up after this many seconds (1-600, default 60). Time spent starting counts. */
  timeout_secs?: number | null;
}

export interface Waitlist {
  email: string;
  note?: string | null;
}

/** 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. */
export interface WalletBonus {
  /** What some amounts get. */
  examples: Array<WalletQuote>;
  from_percent: number;
  from_usd: number;
  to_percent: number;
  to_usd: number;
}

/** `POST /v1/billing/wallet/pay`: charge the saved card and add the money to the wallet. */
export interface WalletPay {
  /** Whole dollars, from `Billing.min_topup_usd` to `Billing.max_wallet_per_day_usd` (also the most charged in any 24 hours). */
  amount_usd: number;
}

/** `GET /v1/billing/wallet/quote?amount_usd=N`: what a payment of N dollars puts in the wallet. */
export interface WalletQuote {
  amount_usd: number;
  bonus_percent: number;
  bonus_usd: number;
  credited_usd: number;
}

/** A webhook endpoint (the signing secret is shown only when created or rotated). */
export interface Webhook {
  created_at: number;
  /** false: paused, nothing is sent (test pings still are). */
  enabled?: boolean;
  /** Empty = every event. */
  events: Array<string>;
  id: string;
  /** Your own name for it (up to 100 characters). */
  name?: string | null;
  updated_at?: number | null;
  url: string;
}

/** One delivery attempt of a webhook (`GET /webhooks/{id}/deliveries`, kept 30 days). */
export interface WebhookDelivery {
  /** 1-8 (`froog-attempt`). */
  attempt: number;
  /** Unix seconds. */
  created_at: number;
  duration_ms: number;
  /** What went wrong (a connection error, a non-2xx answer). */
  error?: string | null;
  /** sandbox.running, …, or ping. */
  event: string;
  /** The event id (`evt_…`, also the `froog-delivery` header): the same on every attempt. */
  event_id: string;
  /** When the next attempt is due, if one is. */
  next_attempt_at?: number | null;
  /** The endpoint's HTTP status; none when it couldn't be reached. */
  response_code?: number | null;
  /** The sandbox it was about (none for a ping). */
  sandbox?: string | null;
  /** delivered | failed (another attempt follows unless this was the last). */
  status: string;
}

export interface WebhookSecret {
  events: Array<string>;
  id: string;
  name?: string | null;
  /** Verify `froog-signature` with this; shown once. */
  secret: string;
  url: string;
}

export class FroogError extends Error {
  status: number;
  code: string;
  /** The request's id (X-Request-Id); quote it when reporting a problem. */
  requestId?: string;
  constructor(status: number, code: string, message: string, requestId?: string) {
    super(message);
    this.name = "FroogError";
    this.status = status;
    this.code = code;
    this.requestId = requestId;
  }
}

export interface FroogOptions {
  /** An API key (froog_…). Default: process.env.FROOG_API_KEY where there is one. */
  apiKey?: string;
  /** Default https://api.froogcorp.com */
  baseUrl?: string;
  /** The organization requests act in (id or name, or "personal"), sent as X-Froog-Org. Default: process.env.FROOG_ORG; none: the active one. */
  org?: string;
  fetch?: typeof fetch;
}

type Query = Record<string, string | number | boolean | string[] | undefined>;

export class Froog {
  private apiKey?: string;
  private baseUrl: string;
  private org?: string;
  private fetchFn: typeof fetch;
  /** Sandboxes as objects: `await froog.sandboxes.create({...})`, `await froog.sandboxes.get(name)`. */
  readonly sandboxes: Sandboxes = new Sandboxes(this);

  constructor(opts: FroogOptions = {}) {
    const env = (globalThis as any).process?.env ?? {};
    this.apiKey = opts.apiKey ?? env.FROOG_API_KEY;
    this.baseUrl = (opts.baseUrl ?? env.FROOG_API_URL ?? "https://api.froogcorp.com").replace(/\/$/, "");
    this.org = opts.org ?? env.FROOG_ORG;
    this.fetchFn = opts.fetch ?? fetch.bind(globalThis);
  }

  private async call(method: string, path: string, opts: { query?: Query; json?: unknown; raw?: BodyInit; rawResponse?: boolean; auth?: boolean; idempotencyKey?: string } = {}): Promise<any> {
    const url = new URL(this.baseUrl + path);
    for (const [k, v] of Object.entries(opts.query ?? {})) {
      if (Array.isArray(v)) for (const x of v) url.searchParams.append(k, x);
      else if (v !== undefined) url.searchParams.set(k, String(v));
    }
    const headers: Record<string, string> = { "user-agent": "froog-ts" };
    if (opts.auth !== false && this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
    if (opts.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
    if (this.org) headers["x-froog-org"] = this.org;
    let body: BodyInit | undefined;
    if (opts.json !== undefined) { headers["content-type"] = "application/json"; body = JSON.stringify(opts.json); }
    if (opts.raw !== undefined) { headers["content-type"] = "application/octet-stream"; body = opts.raw; }
    const res = await this.fetchFn(url, { method, headers, body });
    if (!res.ok) await this.fail(res);
    if (opts.rawResponse) return new Uint8Array(await res.arrayBuffer());
    const text = await res.text();
    return text ? JSON.parse(text) : undefined;
  }

  private async fail(res: Response): Promise<never> {
    let code = "http_" + res.status, message = res.statusText;
    const requestId = res.headers.get("x-request-id") ?? undefined;
    try { const e = await res.json(); code = e.error?.code ?? code; message = e.error?.message ?? message; } catch {}
    throw new FroogError(res.status, code, message, requestId);
  }

  /** A GET answered with NDJSON: each line as it arrives, until the stream ends. */
  private async *stream(path: string, query: Query = {}): AsyncGenerator<any> {
    const url = new URL(this.baseUrl + path);
    for (const [k, v] of Object.entries(query)) {
      if (Array.isArray(v)) for (const x of v) url.searchParams.append(k, x);
      else if (v !== undefined) url.searchParams.set(k, String(v));
    }
    const headers: Record<string, string> = { "user-agent": "froog-ts", accept: "application/x-ndjson" };
    if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
    if (this.org) headers["x-froog-org"] = this.org;
    const res = await this.fetchFn(url, { headers });
    if (!res.ok) await this.fail(res);
    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let buf = "";
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      buf += decoder.decode(value, { stream: true });
      let i: number;
      while ((i = buf.indexOf("\n")) >= 0) {
        const line = buf.slice(0, i).trim();
        buf = buf.slice(i + 1);
        if (line) yield JSON.parse(line);
      }
    }
    if (buf.trim()) yield JSON.parse(buf);
  }

  /** Run a command and get its output as it arrives: `started`, then `stdout`/`stderr` chunks, then `exit` (or `error`).
   *  `for await (const e of froog.execStream("box", { command: "make" })) if (e.type === "stdout") process.stdout.write(e.data);` */
  async *execStream(name: string, body: ExecRequest): AsyncGenerator<ExecEvent> {
    const headers: Record<string, string> = { "user-agent": "froog-ts", "content-type": "application/json", accept: "application/x-ndjson" };
    if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
    if (this.org) headers["x-froog-org"] = this.org;
    const res = await this.fetchFn(new URL(`${this.baseUrl}/v1/sandboxes/${encodeURIComponent(name)}/exec`), { method: "POST", headers, body: JSON.stringify({ ...body, stream: true }) });
    if (!res.ok) await this.fail(res);
    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let buf = "";
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      buf += decoder.decode(value, { stream: true });
      let i: number;
      while ((i = buf.indexOf("\n")) >= 0) {
        const line = buf.slice(0, i).trim();
        buf = buf.slice(i + 1);
        if (line) yield JSON.parse(line) as ExecEvent;
      }
    }
    if (buf.trim()) yield JSON.parse(buf) as ExecEvent;
  }

  /** Poll until the sandbox reaches `state` (e.g. "running" after create or start). */
  async waitUntil(name: string, state: State, timeoutMs = 600_000): Promise<Sandbox> {
    const until = Date.now() + timeoutMs;
    for (;;) {
      const sb = await this.getSandbox(name);
      if (sb.state === state) return sb;
      if (sb.state === "error") throw new FroogError(409, "sandbox_error", `${name}: ${sb.error ?? "error"}`);
      if (Date.now() > until) throw new FroogError(408, "timeout", `${name} didn't become ${state}`);
      await new Promise((r) => setTimeout(r, 2000));
    }
  }

  /** 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` */
  mcp(body: JsonRpcRequest): Promise<JsonRpcResponse> {
    return this.call("POST", `/mcp`, { json: body });
  }

  /** The signed-in account. `GET /v1/account` */
  getAccount(): Promise<Account> {
    return this.call("GET", `/v1/account`);
  }

  /** Alerts settings: which emails the account gets, and automatic rebuilds. `GET /v1/account/alerts` */
  getAlertSettings(): Promise<AlertSettings> {
    return this.call("GET", `/v1/account/alerts`);
  }

  /** Turn kinds of email, or automatic rebuilds, on or off. `PATCH /v1/account/alerts` */
  updateAlertSettings(body: UpdateAlertSettings): Promise<AlertSettings> {
    return this.call("PATCH", `/v1/account/alerts`, { json: body });
  }

  /** 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` */
  closeAccount(): Promise<AccountClosing> {
    return this.call("POST", `/v1/account/close`);
  }

  /** 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` */
  listConnections(): Promise<Array<Connection>> {
    return this.call("GET", `/v1/account/connections`);
  }

  /** Send a 6-digit code to an address to add it as a sign-in connection. `POST /v1/account/connections/email` */
  sendConnectionEmailCode(body: EmailCodeRequest): Promise<void> {
    return this.call("POST", `/v1/account/connections/email`, { json: body });
  }

  /** Add the address with the code from the email. `POST /v1/account/connections/email/verify` */
  verifyConnectionEmailCode(body: EmailCodeVerify): Promise<Connection> {
    return this.call("POST", `/v1/account/connections/email/verify`, { json: body });
  }

  /** Remove a sign-in connection (not the last one: 409 last_connection). `DELETE /v1/account/connections/{id}` */
  removeConnection(id: string): Promise<void> {
    return this.call("DELETE", `/v1/account/connections/${encodeURIComponent(id)}`);
  }

  /** Data controls: zero data retention, snapshots off, monitoring off. `GET /v1/account/data-retention` */
  getDataRetention(): Promise<DataRetention> {
    return this.call("GET", `/v1/account/data-retention`);
  }

  /** Change data controls (dashboard session only). `PATCH /v1/account/data-retention` */
  updateDataRetention(body: UpdateDataRetention): Promise<DataRetention> {
    return this.call("PATCH", `/v1/account/data-retention`, { json: body });
  }

  /** Make a connection primary: its address gets the account's emails. `PUT /v1/account/primary` */
  setPrimaryConnection(body: SetPrimary): Promise<Connection> {
    return this.call("PUT", `/v1/account/primary`, { json: body });
  }

  /** Reopen a closed account within its 30-day window (the plan isn't restored). `POST /v1/account/reopen` */
  reopenAccount(): Promise<AccountClosing> {
    return this.call("POST", `/v1/account/reopen`);
  }

  /** The credential profile an environment's sandboxes get. `PUT /v1/ai/environments/{env}` */
  setAiEnvironmentProfile(env: string, body: SetAiEnvironmentProfile): Promise<AiProfile> {
    return this.call("PUT", `/v1/ai/environments/${encodeURIComponent(env)}`, { json: body });
  }

  /** Credential profiles and their accounts (never the keys). `GET /v1/ai/profiles` */
  listAiProfiles(): Promise<Array<AiProfile>> {
    return this.call("GET", `/v1/ai/profiles`);
  }

  /** Create a credential profile, empty or as a copy. `POST /v1/ai/profiles` */
  createAiProfile(body: CreateAiProfile): Promise<AiProfile> {
    return this.call("POST", `/v1/ai/profiles`, { json: body });
  }

  /** Delete a profile (not the default, not one an environment uses). `DELETE /v1/ai/profiles/{profile}` */
  deleteAiProfile(profile: string): Promise<void> {
    return this.call("DELETE", `/v1/ai/profiles/${encodeURIComponent(profile)}`);
  }

  /** One credential profile (name or id). `GET /v1/ai/profiles/{profile}` */
  getAiProfile(profile: string): Promise<AiProfile> {
    return this.call("GET", `/v1/ai/profiles/${encodeURIComponent(profile)}`);
  }

  /** Rename a profile, or make it the default. `PATCH /v1/ai/profiles/{profile}` */
  updateAiProfile(profile: string, body: UpdateAiProfile): Promise<AiProfile> {
    return this.call("PATCH", `/v1/ai/profiles/${encodeURIComponent(profile)}`, { json: body });
  }

  /** Add a provider key or custom API to a profile (another account when the provider has one). `POST /v1/ai/profiles/{profile}/credentials` */
  addAiCredential(profile: string, body: AddAiCredential): Promise<AiCredential> {
    return this.call("POST", `/v1/ai/profiles/${encodeURIComponent(profile)}/credentials`, { json: body });
  }

  /** Remove a key from a profile. `DELETE /v1/ai/profiles/{profile}/credentials/{id}` */
  removeAiCredential(profile: string, id: string): Promise<void> {
    return this.call("DELETE", `/v1/ai/profiles/${encodeURIComponent(profile)}/credentials/${encodeURIComponent(id)}`);
  }

  /** Use this account first for its provider. `PUT /v1/ai/profiles/{profile}/credentials/{id}/active` */
  switchAiCredential(profile: string, id: string): Promise<AiProfile> {
    return this.call("PUT", `/v1/ai/profiles/${encodeURIComponent(profile)}/credentials/${encodeURIComponent(id)}/active`);
  }

  /** The AI proxy: on or off, blocked, subscriptions withheld, its URL. `GET /v1/ai/proxy` */
  getAiProxy(): Promise<AiProxy> {
    return this.call("GET", `/v1/ai/proxy`);
  }

  /** Turn the AI proxy on or off (sandboxes switch at their next start), block every proxied sandbox. `PUT /v1/ai/proxy` */
  updateAiProxy(body: UpdateAiProxy): Promise<AiProxy> {
    return this.call("PUT", `/v1/ai/proxy`, { json: body });
  }

  /** Proxied AI usage per sandbox: requests, tokens, estimated cost. `GET /v1/ai/usage` */
  getAiUsage(query?: { days?: string | number | boolean }): Promise<AiUsage> {
    return this.call("GET", `/v1/ai/usage`, { query: query });
  }

  /** API keys (secrets are never returned). `GET /v1/api-keys` */
  listApiKeys(): Promise<Array<ApiKeyInfo>> {
    return this.call("GET", `/v1/api-keys`);
  }

  /** Create an API key: a preset or actions, sandboxes or labels, expiry. `POST /v1/api-keys` */
  createApiKey(body: NewApiKey): Promise<CreatedApiKey> {
    return this.call("POST", `/v1/api-keys`, { json: body });
  }

  /** Revoke an API key. `DELETE /v1/api-keys/{id}` */
  revokeApiKey(id: string): Promise<void> {
    return this.call("DELETE", `/v1/api-keys/${encodeURIComponent(id)}`);
  }

  /** A new secret for a key (same id, scope and expiry). `POST /v1/api-keys/{id}/rotate` */
  rotateApiKey(id: string): Promise<CreatedApiKey> {
    return this.call("POST", `/v1/api-keys/${encodeURIComponent(id)}/rotate`);
  }

  /** The account's audit log, newest first (90 days): who created, deleted or changed what, from which IP. `GET /v1/audit` */
  listAuditEvents(query?: { action?: string | number | boolean; sandbox?: string | number | boolean; since?: string | number | boolean; until?: string | number | boolean; sort?: string | number | boolean; limit?: string | number | boolean; cursor?: string | number | boolean }): Promise<AuditList> {
    return this.call("GET", `/v1/audit`, { query: query });
  }

  /** Every matching audit entry of the last 90 days as one CSV or NDJSON file. `GET /v1/audit/export` */
  exportAudit(query?: { format?: string | number | boolean; action?: string | number | boolean; sandbox?: string | number | boolean; since?: string | number | boolean; until?: string | number | boolean; sort?: string | number | boolean }): Promise<Uint8Array> {
    return this.call("GET", `/v1/audit/export`, { query: query, rawResponse: true });
  }

  /** Plan, rates, this period's usage, wallet and auto top-up. `GET /v1/billing` */
  getBilling(): Promise<Billing> {
    return this.call("GET", `/v1/billing`);
  }

  /** Turn extra starts on or off: starts past the start limits paid from the wallet. `PUT /v1/billing/extra-starts` */
  setExtraStarts(body: SetExtraStarts): Promise<Billing> {
    return this.call("PUT", `/v1/billing/extra-starts`, { json: body });
  }

  /** Cancel the plan at the end of the period. `DELETE /v1/billing/plan` */
  cancelPlan(query?: { keep_disks?: string | number | boolean }): Promise<Billing> {
    return this.call("DELETE", `/v1/billing/plan`, { query: query });
  }

  /** Choose or change the plan (the first one returns a Stripe Checkout page). `POST /v1/billing/plan` */
  choosePlan(body: ChoosePlan): Promise<PlanChange> {
    return this.call("POST", `/v1/billing/plan`, { json: body });
  }

  /** A Stripe page to change the card and download invoices. `POST /v1/billing/portal` */
  billingPortal(): Promise<BillingUrl> {
    return this.call("POST", `/v1/billing/portal`);
  }

  /** End the free trial now and start the plan. `POST /v1/billing/trial/end` */
  endTrial(): Promise<Billing> {
    return this.call("POST", `/v1/billing/trial/end`);
  }

  /** Set auto top-up (0 = off). `PUT /v1/billing/wallet/auto-topup` */
  setAutoTopup(body: AutoTopup): Promise<Billing> {
    return this.call("PUT", `/v1/billing/wallet/auto-topup`, { json: body });
  }

  /** Charge the saved card and add the money to the wallet. `POST /v1/billing/wallet/pay` */
  walletPay(body: WalletPay): Promise<Billing> {
    return this.call("POST", `/v1/billing/wallet/pay`, { json: body });
  }

  /** What a wallet payment adds, with its bonus. `GET /v1/billing/wallet/quote` */
  walletQuote(query: { amount_usd: string | number | boolean }): Promise<WalletQuote> {
    return this.call("GET", `/v1/billing/wallet/quote`, { query: query });
  }

  /** CI on froog: setup, 7-day stats and the last GitHub Actions jobs (runs-on: froog). `GET /v1/ci` */
  getCiOverview(query?: { repo?: string | number | boolean; state?: string | number | boolean; limit?: string | number | boolean }): Promise<CiOverview> {
    return this.call("GET", `/v1/ci`, { query: query });
  }

  /** 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` */
  getCiAnalytics(query?: { days?: string | number | boolean; repo?: string | number | boolean }): Promise<CiAnalytics> {
    return this.call("GET", `/v1/ci/analytics`, { query: query });
  }

  /** One CI job: state, reason, wait, run time and sandbox. `GET /v1/ci/jobs/{id}` */
  getCiJob(id: string): Promise<CiJob> {
    return this.call("GET", `/v1/ci/jobs/${encodeURIComponent(id)}`);
  }

  /** One CI job's log from GitHub (text, the last 1 MiB). `GET /v1/ci/jobs/{id}/logs` */
  getCiJobLog(id: string): Promise<Uint8Array> {
    return this.call("GET", `/v1/ci/jobs/${encodeURIComponent(id)}/logs`, { rawResponse: true });
  }

  /** One CI job's CPU and memory over time, and its steps with their times. `GET /v1/ci/jobs/{id}/metrics` */
  getCiJobMetrics(id: string): Promise<CiJobMetrics> {
    return this.call("GET", `/v1/ci/jobs/${encodeURIComponent(id)}/metrics`);
  }

  /** One CI job's test results, from the JUnit XML files it wrote (failed first). `GET /v1/ci/jobs/{id}/tests` */
  getCiJobTests(id: string): Promise<CiJobTests> {
    return this.call("GET", `/v1/ci/jobs/${encodeURIComponent(id)}/tests`);
  }

  /** Search the kept logs of finished CI jobs, line by line. `GET /v1/ci/logs/search` */
  searchCiLogs(query?: { q?: string | number | boolean; days?: string | number | boolean; limit?: string | number | boolean; order?: string | number | boolean }): Promise<CiLogSearch> {
    return this.call("GET", `/v1/ci/logs/search`, { query: query });
  }

  /** CI monitors: alerts when matching jobs fail or run long, with their last alerts. `GET /v1/ci/monitors` */
  listCiMonitors(): Promise<Array<CiMonitor>> {
    return this.call("GET", `/v1/ci/monitors`);
  }

  /** Add a CI monitor (email and the ci.monitor.triggered webhook event). `POST /v1/ci/monitors` */
  createCiMonitor(body: CreateCiMonitor): Promise<CiMonitor> {
    return this.call("POST", `/v1/ci/monitors`, { json: body });
  }

  /** Remove a CI monitor. `DELETE /v1/ci/monitors/{id}` */
  deleteCiMonitor(id: string): Promise<void> {
    return this.call("DELETE", `/v1/ci/monitors/${encodeURIComponent(id)}`);
  }

  /** 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` */
  listCiRuns(query?: { repo?: string | number | boolean; state?: string | number | boolean; workflow?: string | number | boolean; branch?: string | number | boolean; q?: string | number | boolean; limit?: string | number | boolean; offset?: string | number | boolean }): Promise<CiRunList> {
    return this.call("GET", `/v1/ci/runs`, { query: query });
  }

  /** One CI workflow run (GitHub's run id) and its jobs on froog. `GET /v1/ci/runs/{run_id}` */
  getCiRun(run_id: string): Promise<CiRun> {
    return this.call("GET", `/v1/ci/runs/${encodeURIComponent(run_id)}`);
  }

  /** Let jobs from public repositories run, optionally only for listed GitHub users. `PUT /v1/ci/settings` */
  setCiSettings(body: CiSettings): Promise<CiSettings> {
    return this.call("PUT", `/v1/ci/settings`, { json: body });
  }

  /** Start a browser login for the CLI. `POST /v1/cli-login` */
  startCliLogin(body: CliLoginRequest): Promise<CliLoginStarted> {
    return this.call("POST", `/v1/cli-login`, { json: body, auth: false });
  }

  /** Poll a CLI login (202 until approved). `POST /v1/cli-login/poll` */
  pollCliLogin(body: CliLoginPoll): Promise<CliLoginResult> {
    return this.call("POST", `/v1/cli-login/poll`, { json: body, auth: false });
  }

  /** Deny a CLI login. `DELETE /v1/cli-login/{code}` */
  denyCliLogin(code: string): Promise<void> {
    return this.call("DELETE", `/v1/cli-login/${encodeURIComponent(code)}`);
  }

  /** Describe a pending CLI login. `GET /v1/cli-login/{code}` */
  getCliLogin(code: string): Promise<CliLoginInfo> {
    return this.call("GET", `/v1/cli-login/${encodeURIComponent(code)}`);
  }

  /** Approve a CLI login (creates a key). `POST /v1/cli-login/{code}/approve` */
  approveCliLogin(code: string): Promise<void> {
    return this.call("POST", `/v1/cli-login/${encodeURIComponent(code)}/approve`);
  }

  /** A link that opens the dashboard signed in: single use, 15 minutes (full-access keys and sessions only). `POST /v1/dashboard-link` */
  createDashboardLink(): Promise<DashboardLink> {
    return this.call("POST", `/v1/dashboard-link`);
  }

  /** A deletion operation (dop_…): an account close or a zero data retention erasure. `GET /v1/deletion-operations/{id}` */
  getDeletionOperation(id: string): Promise<DeletionOperation> {
    return this.call("GET", `/v1/deletion-operations/${encodeURIComponent(id)}`);
  }

  /** Account environment variables (names and previews). `GET /v1/env` */
  listEnv(): Promise<Array<EnvVar>> {
    return this.call("GET", `/v1/env`);
  }

  /** Remove an account environment variable. `DELETE /v1/env/{name}` */
  removeEnv(name: string): Promise<void> {
    return this.call("DELETE", `/v1/env/${encodeURIComponent(name)}`);
  }

  /** Set an account environment variable. `PUT /v1/env/{name}` */
  setEnv(name: string, body: SetEnvVar): Promise<void> {
    return this.call("PUT", `/v1/env/${encodeURIComponent(name)}`, { json: body });
  }

  /** Repos, secret files and setup script every new sandbox gets. `GET /v1/environment` */
  getEnvironment(): Promise<Environment> {
    return this.call("GET", `/v1/environment`);
  }

  /** Change repos or the setup script. `PATCH /v1/environment` */
  updateEnvironment(body: UpdateEnvironment): Promise<Environment> {
    return this.call("PATCH", `/v1/environment`, { json: body });
  }

  /** Remove a secret file. `DELETE /v1/environment/files` */
  removeEnvironmentFile(query: { path: string | number | boolean }): Promise<void> {
    return this.call("DELETE", `/v1/environment/files`, { query: query });
  }

  /** Write a secret file. `PUT /v1/environment/files` */
  setEnvironmentFile(body: SetEnvFile): Promise<void> {
    return this.call("PUT", `/v1/environment/files`, { json: body });
  }

  /** Named environments: default marker, latest version, sandboxes per version. `GET /v1/environments` */
  listEnvironments(): Promise<Array<EnvironmentSummary>> {
    return this.call("GET", `/v1/environments`);
  }

  /** Create an environment (empty, or a copy of another's latest version). `POST /v1/environments` */
  createEnvironment(body: NewEnvironment): Promise<Environment> {
    return this.call("POST", `/v1/environments`, { json: body });
  }

  /** Delete an environment (its sandboxes keep their version; not the default). `DELETE /v1/environments/{env}` */
  deleteEnvironment(env: string): Promise<void> {
    return this.call("DELETE", `/v1/environments/${encodeURIComponent(env)}`);
  }

  /** One environment: repos, setup scripts, secret files (names and sizes), latest version. `GET /v1/environments/{env}` */
  getNamedEnvironment(env: string): Promise<Environment> {
    return this.call("GET", `/v1/environments/${encodeURIComponent(env)}`);
  }

  /** Change repos or setup scripts (a new version), rename, or make it the default. `PATCH /v1/environments/{env}` */
  updateNamedEnvironment(env: string, body: UpdateEnvironment): Promise<Environment> {
    return this.call("PATCH", `/v1/environments/${encodeURIComponent(env)}`, { json: body });
  }

  /** Remove a secret file (a new version). `DELETE /v1/environments/{env}/files` */
  removeNamedEnvironmentFile(env: string, query: { path: string | number | boolean }): Promise<void> {
    return this.call("DELETE", `/v1/environments/${encodeURIComponent(env)}/files`, { query: query });
  }

  /** Write a secret file (a new version). `PUT /v1/environments/{env}/files` */
  setNamedEnvironmentFile(env: string, body: SetEnvFile): Promise<void> {
    return this.call("PUT", `/v1/environments/${encodeURIComponent(env)}/files`, { json: body });
  }

  /** Move its sandboxes to the latest version (running ones now, stopped ones at their next start). `POST /v1/environments/{env}/upgrade` */
  upgradeEnvironment(env: string, body: UpgradeEnvironment): Promise<EnvironmentUpgrade> {
    return this.call("POST", `/v1/environments/${encodeURIComponent(env)}/upgrade`, { json: body });
  }

  /** An environment's versions, newest first, with sandboxes on each. `GET /v1/environments/{env}/versions` */
  listEnvironmentVersions(env: string): Promise<Array<EnvironmentVersion>> {
    return this.call("GET", `/v1/environments/${encodeURIComponent(env)}/versions`);
  }

  /** Send feedback to the froog team. `POST /v1/feedback` */
  sendFeedback(body: Feedback): Promise<void> {
    return this.call("POST", `/v1/feedback`, { json: body });
  }

  /** Download through a signed link. `GET /v1/file-urls/{token}` */
  downloadSigned(token: string): Promise<Uint8Array> {
    return this.call("GET", `/v1/file-urls/${encodeURIComponent(token)}`, { rawResponse: true, auth: false });
  }

  /** Upload through a signed link (raw bytes). `PUT /v1/file-urls/{token}` */
  uploadSigned(token: string, data: BodyInit): Promise<void> {
    return this.call("PUT", `/v1/file-urls/${encodeURIComponent(token)}`, { raw: data, auth: false });
  }

  /** The GitHub connection: linked installations and the repositories the sandbox token is limited to. `GET /v1/github` */
  getGithub(): Promise<GithubStatus> {
    return this.call("GET", `/v1/github`);
  }

  /** A link that installs the froog GitHub App on a GitHub account or organization. `POST /v1/github/connect` */
  connectGithub(): Promise<GithubConnect> {
    return this.call("POST", `/v1/github/connect`);
  }

  /** Unlink a GitHub installation from the account. `DELETE /v1/github/installations/{id}` */
  disconnectGithubInstallation(id: string): Promise<void> {
    return this.call("DELETE", `/v1/github/installations/${encodeURIComponent(id)}`);
  }

  /** Repositories the linked installations can see. `GET /v1/github/repos` */
  listGithubRepos(): Promise<Array<GithubRepo>> {
    return this.call("GET", `/v1/github/repos`);
  }

  /** Limit the GitHub token in each sandbox to these repositories (empty: all the app can see). `PUT /v1/github/token-repos` */
  setGithubTokenRepos(body: SetTokenRepos): Promise<GithubStatus> {
    return this.call("PUT", `/v1/github/token-repos`, { json: body });
  }

  /** Operating systems a sandbox can run. `GET /v1/images` */
  listImages(): Promise<Array<OsImage>> {
    return this.call("GET", `/v1/images`);
  }

  /** Your agent inboxes. `GET /v1/inboxes` */
  listInboxes(): Promise<Array<Inbox>> {
    return this.call("GET", `/v1/inboxes`);
  }

  /** 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` */
  createInbox(body: CreateInbox): Promise<Inbox> {
    return this.call("POST", `/v1/inboxes`, { json: body });
  }

  /** What you pay for inboxes: seats paid for, inboxes used, the subscription's status. `GET /v1/inboxes/billing` */
  getInboxBilling(): Promise<InboxBilling> {
    return this.call("GET", `/v1/inboxes/billing`);
  }

  /** A Stripe Checkout page that buys inboxes (default: yours plus one). `POST /v1/inboxes/billing/checkout` */
  buyInboxes(body: InboxCheckout): Promise<BillingUrl> {
    return this.call("POST", `/v1/inboxes/billing/checkout`, { json: body });
  }

  /** 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}` */
  deleteInbox(inbox: string): Promise<void> {
    return this.call("DELETE", `/v1/inboxes/${encodeURIComponent(inbox)}`);
  }

  /** One inbox (by id, address, or the part before @). `GET /v1/inboxes/{inbox}` */
  getInbox(inbox: string): Promise<Inbox> {
    return this.call("GET", `/v1/inboxes/${encodeURIComponent(inbox)}`);
  }

  /** Rename an inbox, or attach it to a sandbox ("" detaches it). `PATCH /v1/inboxes/{inbox}` */
  updateInbox(inbox: string, body: UpdateInbox): Promise<Inbox> {
    return this.call("PATCH", `/v1/inboxes/${encodeURIComponent(inbox)}`, { json: body });
  }

  /** Received messages, newest first, without their bodies. `GET /v1/inboxes/{inbox}/messages` */
  listInboxMessages(inbox: string, query?: { since?: string | number | boolean; limit?: string | number | boolean }): Promise<InboxMessageList> {
    return this.call("GET", `/v1/inboxes/${encodeURIComponent(inbox)}/messages`, { query: query });
  }

  /** Delete a message. `DELETE /v1/inboxes/{inbox}/messages/{id}` */
  deleteInboxMessage(inbox: string, id: string): Promise<void> {
    return this.call("DELETE", `/v1/inboxes/${encodeURIComponent(inbox)}/messages/${encodeURIComponent(id)}`);
  }

  /** One message with its text, HTML, codes and links. `GET /v1/inboxes/{inbox}/messages/{id}` */
  getInboxMessage(inbox: string, id: string): Promise<InboxMessage> {
    return this.call("GET", `/v1/inboxes/${encodeURIComponent(inbox)}/messages/${encodeURIComponent(id)}`);
  }

  /** The message as received (message/rfc822). `GET /v1/inboxes/{inbox}/messages/{id}/raw` */
  getInboxMessageRaw(inbox: string, id: string): Promise<Uint8Array> {
    return this.call("GET", `/v1/inboxes/${encodeURIComponent(inbox)}/messages/${encodeURIComponent(id)}/raw`, { rawResponse: true });
  }

  /** 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` */
  waitForInboxMessage(inbox: string, query?: { since?: string | number | boolean; after?: string | number | boolean; sender?: string | number | boolean; subject?: string | number | boolean; timeout?: string | number | boolean }): Promise<InboxWait> {
    return this.call("GET", `/v1/inboxes/${encodeURIComponent(inbox)}/wait`, { query: query });
  }

  /** What the account can run now: sandboxes at once, sizes, trial, start limits. `GET /v1/limits` */
  getLimits(): Promise<Limits> {
    return this.call("GET", `/v1/limits`);
  }

  /** The harnesses, their models, effort levels and fast mode. `GET /v1/provider-models` */
  providerModels(): Promise<Array<ProviderModels>> {
    return this.call("GET", `/v1/provider-models`, { auth: false });
  }

  /** Sandboxes, oldest first, filtered and paged. `GET /v1/sandboxes` */
  listSandboxes(query?: { label?: string | string[]; name?: string | number | boolean; name_prefix?: string | number | boolean; state?: string | number | boolean; sort?: string | number | boolean; limit?: string | number | boolean; cursor?: string | number | boolean }): Promise<Array<Sandbox>> {
    return this.call("GET", `/v1/sandboxes`, { query: query });
  }

  /** Create sandboxes (202; poll until running). `POST /v1/sandboxes` */
  createSandboxes(body: CreateSandbox, idempotencyKey?: string): Promise<Array<Sandbox>> {
    return this.call("POST", `/v1/sandboxes`, { json: body, idempotencyKey: idempotencyKey });
  }

  /** Delete a sandbox (saved snapshots stay). `DELETE /v1/sandboxes/{name}` */
  deleteSandbox(name: string): Promise<void> {
    return this.call("DELETE", `/v1/sandboxes/${encodeURIComponent(name)}`);
  }

  /** One sandbox. `GET /v1/sandboxes/{name}` */
  getSandbox(name: string): Promise<Sandbox> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}`);
  }

  /** Rename; change the auto-stop timer (counting from now while running), lifetime, auto-delete, auto-start or labels. `PATCH /v1/sandboxes/{name}` */
  updateSandbox(name: string, body: UpdateSandbox): Promise<Sandbox> {
    return this.call("PATCH", `/v1/sandboxes/${encodeURIComponent(name)}`, { json: body });
  }

  /** A sandbox's AI profile, proxy state, block, and usage per day and model. `GET /v1/sandboxes/{name}/ai` */
  getSandboxAi(name: string, query?: { days?: string | number | boolean }): Promise<SandboxAi> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/ai`, { query: query });
  }

  /** Block or unblock a sandbox's AI requests, or give it another profile. `PUT /v1/sandboxes/{name}/ai` */
  updateSandboxAi(name: string, body: UpdateSandboxAi): Promise<SandboxAi> {
    return this.call("PUT", `/v1/sandboxes/${encodeURIComponent(name)}/ai`, { json: body });
  }

  /** A link to the browser view: only Chrome, 1920x1080. `POST /v1/sandboxes/{name}/browser` */
  getBrowserLink(name: string, body: BrowserRequest, query?: { theme?: string | number | boolean }): Promise<DesktopLink> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/browser`, { json: body, query: query });
  }

  /** Commands run in the sandbox, newest first (90 days, also while stopped). `GET /v1/sandboxes/{name}/command-history` */
  listCommands(name: string, query?: { mode?: string | number | boolean; sort?: string | number | boolean; limit?: string | number | boolean; cursor?: string | number | boolean }): Promise<CommandList> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/command-history`, { query: query });
  }

  /** One command's record. `GET /v1/sandboxes/{name}/command-history/{id}` */
  getCommand(name: string, id: string): Promise<CommandRecord> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/command-history/${encodeURIComponent(id)}`);
  }

  /** A command's stored output from an offset (at most 1 MiB). `GET /v1/sandboxes/{name}/command-history/{id}/logs` */
  readCommandLogs(name: string, id: string, query?: { stream?: string | number | boolean; offset?: string | number | boolean; encoding?: string | number | boolean }): Promise<CommandLogs> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/command-history/${encodeURIComponent(id)}/logs`, { query: query });
  }

  /** Conversations, most recent first. `GET /v1/sandboxes/{name}/conversations` */
  listConversations(name: string): Promise<ConversationList> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/conversations`);
  }

  /** Delete a conversation and its history. `DELETE /v1/sandboxes/{name}/conversations/{id}` */
  deleteConversation(name: string, id: string): Promise<void> {
    return this.call("DELETE", `/v1/sandboxes/${encodeURIComponent(name)}/conversations/${encodeURIComponent(id)}`);
  }

  /** A conversation and its turns (id or current). `GET /v1/sandboxes/{name}/conversations/{id}` */
  getConversation(name: string, id: string): Promise<ConversationDetail> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/conversations/${encodeURIComponent(id)}`);
  }

  /** A link to the desktop (1920x1080) in froog's viewer. `POST /v1/sandboxes/{name}/desktop` */
  getDesktopLink(name: string, query?: { theme?: string | number | boolean }): Promise<DesktopLink> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/desktop`, { query: query });
  }

  /** Every conversation's events: a JSON page, or a stream (Accept: text/event-stream or application/x-ndjson). `GET /v1/sandboxes/{name}/events` */
  listEvents(name: string, query?: { conversation?: string | number | boolean; cursor?: string | number | boolean; limit?: string | number | boolean }): Promise<EventList> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/events`, { query: query });
  }

  /** Run a command and wait for it; with stream: true, NDJSON events as output arrives. `POST /v1/sandboxes/{name}/exec` */
  exec(name: string, body: ExecRequest): Promise<ExecResult> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/exec`, { json: body });
  }

  /** Delete a file or directory. `DELETE /v1/sandboxes/{name}/files` */
  deleteFile(name: string, query: { path: string | number | boolean; recursive?: string | number | boolean }): Promise<void> {
    return this.call("DELETE", `/v1/sandboxes/${encodeURIComponent(name)}/files`, { query: query });
  }

  /** A file's details or a directory listing. `GET /v1/sandboxes/{name}/files` */
  fileInfo(name: string, query: { path: string | number | boolean }): Promise<FileInfo> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/files`, { query: query });
  }

  /** Unpack a .tar.gz into a directory (raw bytes, up to 100 MiB). `PUT /v1/sandboxes/{name}/files/archive` */
  uploadArchive(name: string, data: BodyInit, query: { path: string | number | boolean }): Promise<void> {
    return this.call("PUT", `/v1/sandboxes/${encodeURIComponent(name)}/files/archive`, { raw: data, query: query });
  }

  /** Download a file (raw bytes, up to 100 MiB). `GET /v1/sandboxes/{name}/files/content` */
  readFile(name: string, query: { path: string | number | boolean }): Promise<Uint8Array> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/files/content`, { query: query, rawResponse: true });
  }

  /** Upload a file (raw bytes, up to 100 MiB, atomic). `PUT /v1/sandboxes/{name}/files/content` */
  writeFile(name: string, data: BodyInit, query: { path: string | number | boolean; mode?: string | number | boolean }): Promise<void> {
    return this.call("PUT", `/v1/sandboxes/${encodeURIComponent(name)}/files/content`, { raw: data, query: query });
  }

  /** A link that downloads a file without an API key. `POST /v1/sandboxes/{name}/files/download-url` */
  createDownloadUrl(name: string, body: NewFileUrl): Promise<FileUrl> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/files/download-url`, { json: body });
  }

  /** Lines that contain a text. `GET /v1/sandboxes/{name}/files/find` */
  findInFiles(name: string, query: { pattern: string | number | boolean; path: string | number | boolean; regex?: string | number | boolean }): Promise<FileFind> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/files/find`, { query: query });
  }

  /** Create a directory (and parents). `POST /v1/sandboxes/{name}/files/mkdir` */
  makeDirectory(name: string, body: MakeDir): Promise<void> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/files/mkdir`, { json: body });
  }

  /** Move or rename. `POST /v1/sandboxes/{name}/files/move` */
  moveFile(name: string, body: MoveFile): Promise<void> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/files/move`, { json: body });
  }

  /** Change a file's or directory's permissions (chmod). `POST /v1/sandboxes/{name}/files/permissions` */
  setFilePermissions(name: string, body: FilePermissions): Promise<void> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/files/permissions`, { json: body });
  }

  /** Replace a text in files, byte for byte. `POST /v1/sandboxes/{name}/files/replace` */
  replaceInFiles(name: string, body: FileReplace): Promise<Array<FileReplaced>> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/files/replace`, { json: body });
  }

  /** Files whose name matches a glob. `GET /v1/sandboxes/{name}/files/search` */
  searchFiles(name: string, query: { pattern: string | number | boolean; path: string | number | boolean }): Promise<FileSearch> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/files/search`, { query: query });
  }

  /** A link that takes a PUT of a file without an API key. `POST /v1/sandboxes/{name}/files/upload-url` */
  createUploadUrl(name: string, body: NewFileUrl): Promise<FileUrl> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/files/upload-url`, { json: body });
  }

  /** Stream file changes under a path (NDJSON: ready, then create / modify / delete, then end). `GET /v1/sandboxes/{name}/files/watch` (NDJSON: `for await (const e of ...)`) */
  watchFiles(name: string, query: { path: string | number | boolean; recursive?: string | number | boolean; timeout_secs?: string | number | boolean }): AsyncGenerator<FileEvent> {
    return this.stream(`/v1/sandboxes/${encodeURIComponent(name)}/files/watch`, query);
  }

  /** New sandboxes from a snapshot of this one. `POST /v1/sandboxes/{name}/fork` */
  forkSandbox(name: string, body: ForkSandbox, idempotencyKey?: string): Promise<Array<Sandbox>> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/fork`, { json: body, idempotencyKey: idempotencyKey });
  }

  /** A one-hour GitHub token for the sandbox (what its git credential helper uses; needs the environment). `POST /v1/sandboxes/{name}/github-token` */
  getSandboxGithubToken(name: string, body: GithubTokenRequest): Promise<GithubToken> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/github-token`, { json: body });
  }

  /** Stop running turns and cancel queued prompts (one conversation, or all). `POST /v1/sandboxes/{name}/interrupt` */
  interrupt(name: string, body: InterruptRequest): Promise<InterruptResult> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/interrupt`, { json: body });
  }

  /** Background commands. `GET /v1/sandboxes/{name}/jobs` */
  listJobs(name: string): Promise<Array<Job>> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/jobs`);
  }

  /** Start a background command. `POST /v1/sandboxes/{name}/jobs` */
  startJob(name: string, body: NewJob): Promise<Job> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/jobs`, { json: body });
  }

  /** A background command's state and output from an offset. `GET /v1/sandboxes/{name}/jobs/{id}` */
  readJob(name: string, id: string, query?: { offset?: string | number | boolean; limit?: string | number | boolean }): Promise<JobOutput> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/jobs/${encodeURIComponent(id)}`, { query: query });
  }

  /** Write to a background command's open standard input (started with stdin_open), or close it. `POST /v1/sandboxes/{name}/jobs/{id}/stdin` */
  writeJobStdin(name: string, id: string, body: JobStdin): Promise<void> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/jobs/${encodeURIComponent(id)}/stdin`, { json: body });
  }

  /** Stop a background command. `POST /v1/sandboxes/{name}/jobs/{id}/stop` */
  stopJob(name: string, id: string): Promise<void> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/jobs/${encodeURIComponent(id)}/stop`);
  }

  /** CPU, memory, disk and network of a running sandbox, read now. `GET /v1/sandboxes/{name}/metrics` */
  getSandboxMetrics(name: string): Promise<SandboxMetrics> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/metrics`);
  }

  /** CPU, memory and disk every 30 seconds, kept 7 days, averaged per step. `GET /v1/sandboxes/{name}/metrics/history` */
  getSandboxMetricsHistory(name: string, query?: { since?: string | number | boolean; until?: string | number | boolean; step?: string | number | boolean }): Promise<MetricsHistory> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/metrics/history`, { query: query });
  }

  /** 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` */
  listPorts(name: string): Promise<Ports> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/ports`);
  }

  /** 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` */
  openPort(name: string, body: OpenPort): Promise<Ports> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/ports`, { json: body });
  }

  /** Set the opened ports, replacing the ones opened before (kept across stops and starts). `PUT /v1/sandboxes/{name}/ports` */
  setPorts(name: string, body: SetPorts): Promise<Ports> {
    return this.call("PUT", `/v1/sandboxes/${encodeURIComponent(name)}/ports`, { json: body });
  }

  /** Stream TCP ports starting or stopping to listen (NDJSON: ready, open / close, end). `GET /v1/sandboxes/{name}/ports/watch` (NDJSON: `for await (const e of ...)`) */
  watchPorts(name: string, query?: { timeout_secs?: string | number | boolean }): AsyncGenerator<PortEvent> {
    return this.stream(`/v1/sandboxes/${encodeURIComponent(name)}/ports/watch`, query);
  }

  /** Close an opened port (IPv6 and IPv4). `DELETE /v1/sandboxes/{name}/ports/{port}/{protocol}` */
  closePort(name: string, port: string, protocol: string): Promise<Ports> {
    return this.call("DELETE", `/v1/sandboxes/${encodeURIComponent(name)}/ports/${encodeURIComponent(port)}/${encodeURIComponent(protocol)}`);
  }

  /** Processes, busiest first (a stopped sandbox: the sample taken at stop). `GET /v1/sandboxes/{name}/processes` */
  listProcesses(name: string): Promise<ProcessList> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/processes`);
  }

  /** Processes seen in the 30-second samples, with start and end times, kept 72 hours. `GET /v1/sandboxes/{name}/processes/records` */
  listProcessRecords(name: string, query?: { since?: string | number | boolean; until?: string | number | boolean; running?: string | number | boolean; limit?: string | number | boolean }): Promise<ProcessRecords> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/processes/records`, { query: query });
  }

  /** The 12 busiest processes every 30 seconds, kept 7 days, newest first. `GET /v1/sandboxes/{name}/processes/samples` */
  listProcessSamples(name: string, query?: { since?: string | number | boolean; until?: string | number | boolean; limit?: string | number | boolean }): Promise<ProcessSamples> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/processes/samples`, { query: query });
  }

  /** Prompt a coding agent and wait for its answer (continues the current conversation). `POST /v1/sandboxes/{name}/prompt` */
  prompt(name: string, body: PromptRequest): Promise<PromptResult> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/prompt`, { json: body });
  }

  /** Prompts, newest first. `GET /v1/sandboxes/{name}/prompts` */
  listPrompts(name: string, query?: { conversation?: string | number | boolean; limit?: string | number | boolean }): Promise<PromptList> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/prompts`, { query: query });
  }

  /** Queue a prompt for a coding agent (returns at once; follow its events). `POST /v1/sandboxes/{name}/prompts` */
  queuePrompt(name: string, body: PromptRequest): Promise<Prompt> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/prompts`, { json: body });
  }

  /** One prompt: status (queued, running, done, failed, interrupted), result, error. `GET /v1/sandboxes/{name}/prompts/{id}` */
  getPrompt(name: string, id: string): Promise<Prompt> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/prompts/${encodeURIComponent(id)}`);
  }

  /** 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` */
  promptEvents(name: string, id: string, query?: { cursor?: string | number | boolean; limit?: string | number | boolean }): Promise<EventList> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/prompts/${encodeURIComponent(id)}/events`, { query: query });
  }

  /** The model catalog plus the models the harnesses list in this sandbox. `GET /v1/sandboxes/{name}/provider-models` */
  sandboxProviderModels(name: string): Promise<Array<ProviderModels>> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/provider-models`);
  }

  /** Roll the sandbox back to a snapshot. `POST /v1/sandboxes/{name}/restore` */
  restoreSandbox(name: string, body: Restore): Promise<Sandbox> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/restore`, { json: body });
  }

  /** Delete every automatic snapshot (saved ones and the one a stopped sandbox starts from stay). `DELETE /v1/sandboxes/{name}/snapshots` */
  deleteAutoSnapshots(name: string): Promise<DeletedSnapshots> {
    return this.call("DELETE", `/v1/sandboxes/${encodeURIComponent(name)}/snapshots`);
  }

  /** A sandbox's snapshots, oldest first, paged. `GET /v1/sandboxes/{name}/snapshots` */
  listSnapshots(name: string, query?: { sort?: string | number | boolean; limit?: string | number | boolean; cursor?: string | number | boolean }): Promise<Array<Snapshot>> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/snapshots`, { query: query });
  }

  /** Save a snapshot (with a label: an account template). `POST /v1/sandboxes/{name}/snapshots` */
  saveSnapshot(name: string, body: CreateSnapshot): Promise<Snapshot> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/snapshots`, { json: body });
  }

  /** Delete a snapshot. `DELETE /v1/sandboxes/{name}/snapshots/{snap}` */
  deleteSnapshot(name: string, snap: string): Promise<void> {
    return this.call("DELETE", `/v1/sandboxes/${encodeURIComponent(name)}/snapshots/${encodeURIComponent(snap)}`);
  }

  /** One snapshot by id or label; latest is the newest available one. `GET /v1/sandboxes/{name}/snapshots/{snap}` */
  getSnapshot(name: string, snap: string): Promise<Snapshot> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/snapshots/${encodeURIComponent(snap)}`);
  }

  /** A file's details, or a folder's entries, inside a snapshot (the sandbox may be stopped). `GET /v1/sandboxes/{name}/snapshots/{snap}/files` */
  snapshotFileInfo(name: string, snap: string, query: { path: string | number | boolean }): Promise<FileInfo> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/snapshots/${encodeURIComponent(snap)}/files`, { query: query });
  }

  /** Download a file (raw bytes) or a folder (tar) out of a snapshot. `GET /v1/sandboxes/{name}/snapshots/{snap}/files/download` */
  downloadFromSnapshot(name: string, snap: string, query: { path: string | number | boolean }): Promise<Uint8Array> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/snapshots/${encodeURIComponent(snap)}/files/download`, { query: query, rawResponse: true });
  }

  /** Where and how to SSH in (host, port, pinned host key). `GET /v1/sandboxes/{name}/ssh` */
  getSshInfo(name: string): Promise<SshInfo> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/ssh`);
  }

  /** 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` */
  startSandbox(name: string, body?: StartSandbox): Promise<Sandbox> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/start`, { json: body });
  }

  /** 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` */
  steer(name: string, body: SteerRequest): Promise<SteerResult> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/steer`, { json: body });
  }

  /** 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` */
  stopSandbox(name: string, body?: StopSandbox): Promise<Sandbox> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/stop`, { json: body });
  }

  /** Record activity (last_active_at); the auto-stop deadline doesn't move (PATCH ttl_secs does). `POST /v1/sandboxes/{name}/touch` */
  touchSandbox(name: string): Promise<void> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/touch`);
  }

  /** Public URLs of the sandbox's ports. `GET /v1/sandboxes/{name}/urls` */
  listUrls(name: string): Promise<Array<PublicUrl>> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/urls`);
  }

  /** Host a port (private with a token unless public), or rename or title it. `POST /v1/sandboxes/{name}/urls` */
  hostPort(name: string, body: NewPublicUrl): Promise<PublicUrl> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/urls`, { json: body });
  }

  /** Remove a public URL. `DELETE /v1/sandboxes/{name}/urls/{port}` */
  removeUrl(name: string, port: string): Promise<void> {
    return this.call("DELETE", `/v1/sandboxes/${encodeURIComponent(name)}/urls/${encodeURIComponent(port)}`);
  }

  /** Live viewer tokens of a private URL (secrets are never returned). `GET /v1/sandboxes/{name}/urls/{port}/tokens` */
  listUrlTokens(name: string, port: string): Promise<Array<UrlToken>> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/urls/${encodeURIComponent(port)}/tokens`);
  }

  /** 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` */
  createUrlToken(name: string, port: string, body: NewUrlToken): Promise<UrlToken> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/urls/${encodeURIComponent(port)}/tokens`, { json: body });
  }

  /** Revoke a viewer token. `DELETE /v1/sandboxes/{name}/urls/{port}/tokens/{id}` */
  revokeUrlToken(name: string, port: string, id: string): Promise<void> {
    return this.call("DELETE", `/v1/sandboxes/${encodeURIComponent(name)}/urls/${encodeURIComponent(port)}/tokens/${encodeURIComponent(id)}`);
  }

  /** One sandbox's running time and cost in a window, at list prices. `GET /v1/sandboxes/{name}/usage` */
  getSandboxUsage(name: string, query?: { since?: string | number | boolean; until?: string | number | boolean }): Promise<SandboxUsage> {
    return this.call("GET", `/v1/sandboxes/${encodeURIComponent(name)}/usage`, { query: query });
  }

  /** Wait until something listens on a TCP port inside the sandbox (ready: false after the timeout). `POST /v1/sandboxes/{name}/wait` */
  waitForPort(name: string, body: WaitForPort): Promise<PortWait> {
    return this.call("POST", `/v1/sandboxes/${encodeURIComponent(name)}/wait`, { json: body });
  }

  /** Saved snapshots of the account (templates for from). `GET /v1/saved-snapshots` */
  listSavedSnapshots(): Promise<Array<SavedSnapshot>> {
    return this.call("GET", `/v1/saved-snapshots`);
  }

  /** Remove a saved snapshot. `DELETE /v1/saved-snapshots/{name}` */
  deleteSavedSnapshot(name: string): Promise<void> {
    return this.call("DELETE", `/v1/saved-snapshots/${encodeURIComponent(name)}`);
  }

  /** One saved snapshot and its status. `GET /v1/saved-snapshots/{name}` */
  getSavedSnapshot(name: string): Promise<SavedSnapshot> {
    return this.call("GET", `/v1/saved-snapshots/${encodeURIComponent(name)}`);
  }

  /** A file's details, or a folder's entries, inside a saved snapshot. `GET /v1/saved-snapshots/{name}/files` */
  savedSnapshotFileInfo(name: string, query: { path: string | number | boolean }): Promise<FileInfo> {
    return this.call("GET", `/v1/saved-snapshots/${encodeURIComponent(name)}/files`, { query: query });
  }

  /** Download a file (raw bytes) or a folder (tar) out of a saved snapshot. `GET /v1/saved-snapshots/{name}/files/download` */
  downloadFromSavedSnapshot(name: string, query: { path: string | number | boolean }): Promise<Uint8Array> {
    return this.call("GET", `/v1/saved-snapshots/${encodeURIComponent(name)}/files/download`, { query: query, rawResponse: true });
  }

  /** A saved snapshot's share codes (not the codes themselves). `GET /v1/saved-snapshots/{name}/shares` */
  listSnapshotShares(name: string): Promise<Array<SnapshotShare>> {
    return this.call("GET", `/v1/saved-snapshots/${encodeURIComponent(name)}/shares`);
  }

  /** Make a share code another account can create sandboxes from (shown once). `POST /v1/saved-snapshots/{name}/shares` */
  shareSavedSnapshot(name: string): Promise<CreatedSnapshotShare> {
    return this.call("POST", `/v1/saved-snapshots/${encodeURIComponent(name)}/shares`);
  }

  /** Revoke a share code. `DELETE /v1/saved-snapshots/{name}/shares/{id}` */
  revokeSnapshotShare(name: string, id: string): Promise<void> {
    return this.call("DELETE", `/v1/saved-snapshots/${encodeURIComponent(name)}/shares/${encodeURIComponent(id)}`);
  }

  /** Every snapshot of the account, newest first, with its sandbox, paged. `GET /v1/snapshots` */
  listAccountSnapshots(query?: { sort?: string | number | boolean; limit?: string | number | boolean; cursor?: string | number | boolean }): Promise<Array<Snapshot>> {
    return this.call("GET", `/v1/snapshots`, { query: query });
  }

  /** SSH public keys that can log in to your sandboxes. `GET /v1/ssh-keys` */
  listSshKeys(): Promise<Array<SshKey>> {
    return this.call("GET", `/v1/ssh-keys`);
  }

  /** Add an SSH public key. `POST /v1/ssh-keys` */
  addSshKey(body: AddSshKey): Promise<SshKey> {
    return this.call("POST", `/v1/ssh-keys`, { json: body });
  }

  /** Remove an SSH key. `DELETE /v1/ssh-keys/{id}` */
  removeSshKey(id: string): Promise<void> {
    return this.call("DELETE", `/v1/ssh-keys/${encodeURIComponent(id)}`);
  }

  /** Platform status and 90-day uptime. `GET /v1/status` */
  getStatus(): Promise<void> {
    return this.call("GET", `/v1/status`, { auth: false });
  }

  /** A stop's progress: pending, failing (retried), completed, superseded or failed. `GET /v1/stops/{id}` */
  getStop(id: string): Promise<Stop> {
    return this.call("GET", `/v1/stops/${encodeURIComponent(id)}`);
  }

  /** Subscription sign-ins (Claude, ChatGPT): connected or not. `GET /v1/subscriptions` */
  listSubscriptions(): Promise<Array<Subscription>> {
    return this.call("GET", `/v1/subscriptions`);
  }

  /** Sign out of a subscription (every sandbox loses it at its next start). `DELETE /v1/subscriptions/{provider}` */
  signOutSubscription(provider: string): Promise<void> {
    return this.call("DELETE", `/v1/subscriptions/${encodeURIComponent(provider)}`);
  }

  /** Cancel the sign-in in progress. `DELETE /v1/subscriptions/{provider}/login` */
  cancelSubscriptionLogin(provider: string): Promise<void> {
    return this.call("DELETE", `/v1/subscriptions/${encodeURIComponent(provider)}/login`);
  }

  /** The sign-in in progress (stores the credential once it's done). `GET /v1/subscriptions/{provider}/login` */
  getSubscriptionLogin(provider: string): Promise<SubscriptionLogin> {
    return this.call("GET", `/v1/subscriptions/${encodeURIComponent(provider)}/login`);
  }

  /** Start a subscription sign-in in one of your running sandboxes. `POST /v1/subscriptions/{provider}/login` */
  startSubscriptionLogin(provider: string, body: StartSubscriptionLogin): Promise<SubscriptionLogin> {
    return this.call("POST", `/v1/subscriptions/${encodeURIComponent(provider)}/login`, { json: body });
  }

  /** Claude: paste the code the sign-in page showed. `POST /v1/subscriptions/{provider}/login/code` */
  sendSubscriptionCode(provider: string, body: SubscriptionCode): Promise<SubscriptionLogin> {
    return this.call("POST", `/v1/subscriptions/${encodeURIComponent(provider)}/login/code`, { json: body });
  }

  /** Stop the OpenTelemetry export. `DELETE /v1/telemetry` */
  removeTelemetry(): Promise<void> {
    return this.call("DELETE", `/v1/telemetry`);
  }

  /** The OpenTelemetry endpoint (null when none). `GET /v1/telemetry` */
  getTelemetry(): Promise<Telemetry | null> {
    return this.call("GET", `/v1/telemetry`);
  }

  /** Set the OpenTelemetry endpoint; sends a test export. `PUT /v1/telemetry` */
  setTelemetry(body: SetTelemetry): Promise<TelemetrySaved> {
    return this.call("PUT", `/v1/telemetry`, { json: body });
  }

  /** Usage this billing period: per-second running time, snapshots, what the plan covers. `GET /v1/usage` */
  getUsage(query?: { month?: string | number | boolean }): Promise<Usage> {
    return this.call("GET", `/v1/usage`, { query: query });
  }

  /** Join the waitlist. `POST /v1/waitlist` */
  joinWaitlist(body: Waitlist): Promise<void> {
    return this.call("POST", `/v1/waitlist`, { json: body, auth: false });
  }

  /** Webhooks. `GET /v1/webhooks` */
  listWebhooks(): Promise<Array<Webhook>> {
    return this.call("GET", `/v1/webhooks`);
  }

  /** Add a webhook (the signing secret is returned once). `POST /v1/webhooks` */
  addWebhook(body: NewWebhook): Promise<WebhookSecret> {
    return this.call("POST", `/v1/webhooks`, { json: body });
  }

  /** Remove a webhook. `DELETE /v1/webhooks/{id}` */
  removeWebhook(id: string): Promise<void> {
    return this.call("DELETE", `/v1/webhooks/${encodeURIComponent(id)}`);
  }

  /** One webhook. `GET /v1/webhooks/{id}` */
  getWebhook(id: string): Promise<Webhook> {
    return this.call("GET", `/v1/webhooks/${encodeURIComponent(id)}`);
  }

  /** Change a webhook's URL, events, name, or pause it (the secret stays). `PATCH /v1/webhooks/{id}` */
  updateWebhook(id: string, body: UpdateWebhook): Promise<Webhook> {
    return this.call("PATCH", `/v1/webhooks/${encodeURIComponent(id)}`, { json: body });
  }

  /** Delivery attempts of the last 30 days, newest first. `GET /v1/webhooks/{id}/deliveries` */
  listWebhookDeliveries(id: string, query?: { limit?: string | number | boolean }): Promise<Array<WebhookDelivery>> {
    return this.call("GET", `/v1/webhooks/${encodeURIComponent(id)}/deliveries`, { query: query });
  }

  /** A new signing secret. `POST /v1/webhooks/{id}/rotate` */
  rotateWebhook(id: string): Promise<WebhookSecret> {
    return this.call("POST", `/v1/webhooks/${encodeURIComponent(id)}/rotate`);
  }

  /** Send a test event now. `POST /v1/webhooks/{id}/test` */
  testWebhook(id: string): Promise<void> {
    return this.call("POST", `/v1/webhooks/${encodeURIComponent(id)}/test`);
  }

}

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

/** Options of `sb.exec` and `sb.execStream`. */
export interface ExecOptions {
  cwd?: string;
  env?: Record<string, string>;
  /** Text, or bytes (sent base64-encoded), fed to the command's standard input. */
  stdin?: string | Uint8Array;
  /** Seconds. */
  timeout?: number;
}

/** Thrown by `exec(..., { check: true })` and `process.wait({ check: true })` when the command exits non-zero. */
export class CommandError extends FroogError {
  command: string;
  exitCode: number | null;
  stdout: string;
  stderr: string;
  constructor(command: string, exitCode: number | null, stdout = "", stderr = "") {
    super(0, "command_failed", `${JSON.stringify(command)} exited with ${exitCode}: ${(stderr || stdout).trim().slice(-500)}`);
    this.name = "CommandError";
    this.command = command;
    this.exitCode = exitCode;
    this.stdout = stdout;
    this.stderr = stderr;
  }
}

function toBase64(b: Uint8Array): string {
  const B = (globalThis as any).Buffer;
  if (B) return B.from(b).toString("base64");
  let s = "";
  for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]);
  return btoa(s);
}

function execBody(command: string, o: ExecOptions = {}): ExecRequest {
  const body: ExecRequest = { command };
  if (o.cwd !== undefined) body.cwd = o.cwd;
  if (o.env) body.env = o.env;
  if (o.stdin instanceof Uint8Array) body.stdin_b64 = toBase64(o.stdin);
  else if (o.stdin !== undefined) body.stdin = o.stdin;
  if (o.timeout !== undefined) body.timeout_secs = Math.floor(o.timeout);
  return body;
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const DONE = ["exited", "killed", "lost"];
declare global {
  // Declared here too, so this file type-checks without lib "esnext.disposable"; `await using` needs a runtime that has it.
  interface SymbolConstructor {
    readonly asyncDispose: unique symbol;
  }
}

/** How long to wait (`wait: false` to return at once) and whether leaving `await using` / `use` deletes it. */
export interface WaitOptions {
  wait?: boolean;
  /** Milliseconds. Default 600000. */
  timeoutMs?: number;
}

/** `froog.sandboxes`: create, get and list sandboxes as `SandboxHandle` objects. */
export class Sandboxes {
  constructor(private froog: Froog) {}

  /** Create one sandbox and wait until it runs. `keep: false` (the default) deletes it when an `await using` block or `use` ends. */
  create(body: CreateSandbox = {}, opts: WaitOptions & { keep?: boolean; idempotencyKey?: string } = {}): Promise<SandboxHandle> {
    return SandboxHandle.create(this.froog, body, opts);
  }

  /** Fetch a sandbox by name. Its handle is kept (not deleted) at the end of a block unless you set `keep = false`. */
  get(name: string): Promise<SandboxHandle> {
    return SandboxHandle.get(this.froog, name);
  }

  async list(query?: Parameters<Froog["listSandboxes"]>[0]): Promise<SandboxHandle[]> {
    return (await this.froog.listSandboxes(query)).map((i) => new SandboxHandle(this.froog, i.name, i));
  }

  /** Create a sandbox, run `fn` with it, then delete it (unless `keep`), also when `fn` throws. For runtimes without `await using`. */
  async use<T>(body: CreateSandbox, fn: (sb: SandboxHandle) => Promise<T>, opts: WaitOptions & { keep?: boolean; idempotencyKey?: string } = {}): Promise<T> {
    const sb = await this.create(body, opts);
    try {
      return await fn(sb);
    } finally {
      await sb.close();
    }
  }
}

/** A sandbox you act on by method. `await using sb = await froog.sandboxes.create({ size: "small" });` deletes it at the end of the scope. */
export class SandboxHandle {
  readonly files: SandboxFiles;
  /** false: `close()` (and the end of an `await using` scope) deletes the sandbox. */
  keep: boolean;

  constructor(readonly froog: Froog, readonly name: string, public info?: Sandbox, keep = true) {
    this.keep = keep;
    this.files = new SandboxFiles(this);
  }

  static async create(froog: Froog, body: CreateSandbox = {}, opts: WaitOptions & { keep?: boolean; idempotencyKey?: string } = {}): Promise<SandboxHandle> {
    if (body.count != null && body.count !== 1) throw new Error("sandboxes.create makes one sandbox; use froog.createSandboxes for count");
    const [info] = await froog.createSandboxes(body, opts.idempotencyKey);
    const sb = new SandboxHandle(froog, info.name, info, opts.keep ?? false);
    if (opts.wait !== false) {
      try {
        await sb.waitUntil("running", opts.timeoutMs);
      } catch (e) {
        await sb.close().catch(() => {});
        throw e;
      }
    }
    return sb;
  }

  static async get(froog: Froog, name: string): Promise<SandboxHandle> {
    return new SandboxHandle(froog, name, await froog.getSandbox(name));
  }

  /** Delete it unless `keep`. */
  async close(): Promise<void> {
    if (this.keep) return;
    try {
      await this.delete();
    } catch (e) {
      if (!(e instanceof FroogError && e.status === 404)) throw e;
    }
  }

  [Symbol.asyncDispose](): Promise<void> {
    return this.close();
  }

  async refresh(): Promise<Sandbox> {
    return (this.info = await this.froog.getSandbox(this.name));
  }

  async state(): Promise<State> {
    return (await this.refresh()).state;
  }

  async waitUntil(state: State, timeoutMs?: number): Promise<Sandbox> {
    return (this.info = await this.froog.waitUntil(this.name, state, timeoutMs));
  }

  /** Run a command and wait for it. `check: true` throws `CommandError` when it exits non-zero. */
  async exec(command: string, opts: ExecOptions & { check?: boolean } = {}): Promise<ExecResult> {
    const r = await this.froog.exec(this.name, execBody(command, opts));
    if (opts.check && r.exit_code !== 0) throw new CommandError(command, r.exit_code, r.stdout, r.stderr);
    return r;
  }

  /** Run a command and get events as output arrives (`started`, `stdout`/`stderr`, then `exit` or `error`). */
  execStream(command: string, opts: ExecOptions = {}): AsyncGenerator<ExecEvent> {
    return this.froog.execStream(this.name, execBody(command, opts));
  }

  /** Start a background command (a job): `await p.wait()`, `await p.logs()`, `await p.kill()`. With `stdinOpen`, its
   * standard input stays open (after `stdin`, if given): `await p.write(...)`, then `await p.closeStdin()`. */
  async spawn(command: string, opts: Omit<ExecOptions, "timeout"> & { stdinOpen?: boolean } = {}): Promise<ProcessHandle> {
    const b = execBody(command, opts);
    const job: NewJob = { command, cwd: b.cwd, env: b.env, stdin: b.stdin, stdin_b64: b.stdin_b64 };
    if (opts.stdinOpen) job.stdin_open = true;
    return new ProcessHandle(this, await this.froog.startJob(this.name, job));
  }

  /** Reconnect to a background command by id (`job_…`). */
  process(id: string): ProcessHandle {
    return new ProcessHandle(this, { id });
  }

  async processes(): Promise<ProcessHandle[]> {
    return (await this.froog.listJobs(this.name)).map((j) => new ProcessHandle(this, j));
  }

  /** Wait until something in the sandbox listens on TCP `port` (`POST /wait`, 1-600 s). Throws a 408 `timeout` FroogError if nothing does. */
  async waitForPort(port: number, timeoutSecs = 60): Promise<void> {
    const p = Math.floor(port);
    const r = await this.froog.waitForPort(this.name, { port: p, timeout_secs: Math.max(1, Math.min(600, Math.floor(timeoutSecs))) });
    if (!r.ready) throw new FroogError(408, "timeout", `nothing listens on port ${p} in ${this.name} after ${timeoutSecs} s`);
  }

  async start(body: StartSandbox = {}, opts: WaitOptions = {}): Promise<this> {
    this.info = await this.froog.startSandbox(this.name, body);
    if (opts.wait !== false) await this.waitUntil("running", opts.timeoutMs);
    return this;
  }

  /** Stop it (disk snapshotted, billing stops) and, unless `wait: false`, wait until the stop finishes.
   * A stop whose snapshot fails is refused (it keeps running); `force: true` keeps the last good snapshot. */
  async stop(opts: WaitOptions & { force?: boolean } = {}): Promise<this> {
    this.info = await this.froog.stopSandbox(this.name, opts.force ? { force: true } : undefined);
    if (opts.wait === false) return this;
    const until = Date.now() + (opts.timeoutMs ?? 600_000);
    const stopId = this.info.stop_id;
    if (!stopId) {
      await this.waitUntil("stopped", opts.timeoutMs);
      return this;
    }
    for (;;) {
      const s = await this.froog.getStop(stopId);
      if (s.status === "completed") break;
      if (s.status === "failed") throw new FroogError(409, "stop_failed", `${this.name}: ${s.error ?? "failed"}`);
      if (s.status === "failing") throw new FroogError(409, "stop_refused", `${this.name} keeps running (stop refused, retried by froog): ${s.error ?? ""}`);
      if (Date.now() > until) throw new FroogError(408, "timeout", `${this.name} didn't stop`);
      await sleep(2000);
    }
    await this.refresh().catch(() => {});
    return this;
  }

  async delete(): Promise<void> {
    await this.froog.deleteSandbox(this.name);
  }

  /** A new sandbox from a snapshot of this one; waits until it runs. */
  async fork(name: string, body: Omit<ForkSandbox, "name" | "count"> = {}, opts: WaitOptions & { keep?: boolean; idempotencyKey?: string } = {}): Promise<SandboxHandle> {
    const [info] = await this.froog.forkSandbox(this.name, { ...body, name }, opts.idempotencyKey);
    const sb = new SandboxHandle(this.froog, info.name, info, opts.keep ?? false);
    if (opts.wait !== false) {
      try {
        await sb.waitUntil("running", opts.timeoutMs);
      } catch (e) {
        await sb.close().catch(() => {});
        throw e;
      }
    }
    return sb;
  }

  snapshot(label?: string): Promise<Snapshot> {
    return this.froog.saveSnapshot(this.name, label ? { label } : {});
  }

  /** Give a port an HTTPS URL and return it (private ones carry `?_token=`). */
  async host(port: number, opts: Omit<NewPublicUrl, "port"> = {}): Promise<string> {
    return (await this.froog.hostPort(this.name, { ...opts, port })).url;
  }

  /** Open an inbound port (kept across stops and starts): on the sandbox's IPv6 address, and on a public port of its host's IPv4 address (see ipv4Ports); returns the open ports. */
  async openPort(port: number, protocol: "tcp" | "udp" = "tcp"): Promise<string[]> {
    return (await this.froog.openPort(this.name, { port, protocol })).ports;
  }

  /** Close a port opened with openPort; returns the open ports. */
  async closePort(port: number, protocol: "tcp" | "udp" = "tcp"): Promise<string[]> {
    return (await this.froog.closePort(this.name, String(port), protocol)).ports;
  }

  /** Inbound ports open besides SSH, e.g. ["8080/tcp", "27015/udp"]. */
  async ports(): Promise<string[]> {
    return (await this.froog.listPorts(this.name)).ports;
  }

  /** Where the opened ports are reached over IPv4 ({ port: "27015/udp", public_port: 40000, address: "203.0.113.7:40000" }). Empty while stopped. */
  async ipv4Ports(): Promise<Ipv4Port[]> {
    return (await this.froog.listPorts(this.name)).ipv4 ?? [];
  }

  metrics(): Promise<SandboxMetrics> {
    return this.froog.getSandboxMetrics(this.name);
  }

  /** Give a coding agent in the sandbox a task and wait for its answer (`stdout`). It continues the sandbox's
   * current conversation unless `new: true` or `conversation_id`. */
  prompt(prompt: string, opts: Omit<PromptRequest, "prompt"> = {}): Promise<PromptResult> {
    return this.froog.prompt(this.name, { ...opts, prompt });
  }

  /** Queue a prompt and return it at once; follow it with `watchPrompt`. */
  queuePrompt(prompt: string, opts: Omit<PromptRequest, "prompt"> = {}): Promise<Prompt> {
    return this.froog.queuePrompt(this.name, { ...opts, prompt });
  }

  /** A prompt's events (status, text, tool_call, tool_result, steer, ...) until it ends. */
  async *watchPrompt(promptId: string, intervalMs = 1000): AsyncGenerator<AgentEvent> {
    let cursor = 0;
    for (;;) {
      const page = await this.froog.promptEvents(this.name, promptId, { cursor });
      for (const e of page.events) {
        yield e;
        const s = (e.data as { status?: string } | null)?.status;
        if (e.type === "status" && (s === "done" || s === "failed" || s === "interrupted")) return;
      }
      cursor = page.next_cursor;
      if (page.events.length === 0) await new Promise((r) => setTimeout(r, intervalMs));
    }
  }

  async conversations(): Promise<Conversation[]> {
    return (await this.froog.listConversations(this.name)).conversations;
  }

  /** Send a message to the running turn (Claude Code takes it mid-turn; other agents get it as the next turn). */
  steer(message: string, conversationId?: string): Promise<SteerResult> {
    return this.froog.steer(this.name, { message, conversation_id: conversationId });
  }

  /** Stop running turns and cancel queued prompts: all, or one conversation's. */
  async interrupt(conversationId?: string): Promise<string[]> {
    return (await this.froog.interrupt(this.name, { conversation_id: conversationId })).interrupted;
  }
}

/** `sb.files`: paths are relative to /home/user, or absolute. */
export class SandboxFiles {
  constructor(private sb: SandboxHandle) {}

  read(path: string): Promise<Uint8Array> {
    return this.sb.froog.readFile(this.sb.name, { path });
  }

  async readText(path: string): Promise<string> {
    return new TextDecoder().decode(await this.read(path));
  }

  /** Write text or bytes (atomic). `mode` like "755". */
  write(path: string, data: string | Uint8Array, mode?: string): Promise<void> {
    const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
    return this.sb.froog.writeFile(this.sb.name, bytes as BodyInit, mode === undefined ? { path } : { path, mode });
  }

  info(path: string): Promise<FileInfo> {
    return this.sb.froog.fileInfo(this.sb.name, { path });
  }

  async list(path = "."): Promise<FileEntry[]> {
    return (await this.info(path)).entries ?? [];
  }

  async exists(path: string): Promise<boolean> {
    try {
      await this.info(path);
      return true;
    } catch (e) {
      if (e instanceof FroogError && e.status === 404) return false;
      throw e;
    }
  }

  remove(path: string, opts: { recursive?: boolean } = {}): Promise<void> {
    return this.sb.froog.deleteFile(this.sb.name, opts.recursive ? { path, recursive: true } : { path });
  }

  mkdir(path: string, mode?: string): Promise<void> {
    return this.sb.froog.makeDirectory(this.sb.name, mode === undefined ? { path } : { path, mode });
  }

  move(from: string, to: string): Promise<void> {
    return this.sb.froog.moveFile(this.sb.name, { from, to });
  }

  /** Changes under `path` as they happen (`event`: create | modify | delete), until `timeoutSecs` (default and at
   *  most 3600) or you stop iterating: `for await (const e of sb.files.watch("src")) console.log(e.event, e.path)`. */
  async *watch(path = ".", opts: { recursive?: boolean; timeoutSecs?: number } = {}): AsyncGenerator<FileEvent> {
    const q: { path: string; recursive?: boolean; timeout_secs?: number } = { path };
    if (opts.recursive === false) q.recursive = false;
    if (opts.timeoutSecs !== undefined) q.timeout_secs = Math.floor(opts.timeoutSecs);
    for await (const e of this.sb.froog.watchFiles(this.sb.name, q)) {
      if (e.type === "change") yield e;
      else if (e.type === "error" || (e.type === "end" && e.reason === "error")) throw new FroogError(409, "watch_failed", e.message ?? "the watch failed");
    }
  }
}

/** A background command (a job). Output is stdout and stderr together. */
export class ProcessHandle {
  readonly id: string;
  /** Where `follow()` reads next. */
  offset = 0;
  constructor(readonly sandbox: SandboxHandle, public info: Partial<Job> & { id: string }) {
    this.id = info.id;
  }

  private async read(offset: number, limit?: number): Promise<JobOutput> {
    const q: { offset: number; limit?: number } = { offset };
    if (limit !== undefined) q.limit = limit;
    const out = await this.sandbox.froog.readJob(this.sandbox.name, this.id, q);
    this.info = { ...this.info, state: out.state, exit_code: out.exit_code, started_at: out.started_at };
    return out;
  }

  async refresh(): Promise<Partial<Job> & { id: string }> {
    await this.read(0, 0);
    return this.info;
  }

  get exitCode(): number | null | undefined {
    return this.info.exit_code;
  }

  /** All its output so far. */
  async logs(): Promise<string> {
    let s = "", offset = 0;
    for (;;) {
      const out = await this.read(offset);
      s += out.output;
      if (out.next_offset <= offset || out.next_offset >= out.output_size) return s;
      offset = out.next_offset;
    }
  }

  /** Yield output as it arrives until the command ends. */
  async *follow(pollMs = 1000): AsyncGenerator<string> {
    for (;;) {
      const out = await this.read(this.offset);
      if (out.output) yield out.output;
      const moved = out.next_offset > this.offset;
      this.offset = out.next_offset;
      if (DONE.includes(out.state) && this.offset >= out.output_size) return;
      if (!moved) await sleep(pollMs);
    }
  }

  /** Wait until it ends; resolves to the exit code (null when killed or lost). */
  async wait(opts: { timeoutMs?: number; pollMs?: number; check?: boolean } = {}): Promise<number | null> {
    const until = opts.timeoutMs === undefined ? Infinity : Date.now() + opts.timeoutMs;
    while (!DONE.includes((await this.refresh()).state ?? "")) {
      if (Date.now() > until) throw new FroogError(408, "timeout", `${this.id} still running`);
      await sleep(opts.pollMs ?? 1000);
    }
    const code = this.info.exit_code ?? null;
    if (opts.check && code !== 0) throw new CommandError(this.info.command ?? this.id, code, await this.logs());
    return code;
  }

  /** Stop it (TERM to its process group, KILL after 3 s). */
  kill(): Promise<void> {
    return this.sandbox.froog.stopJob(this.sandbox.name, this.id);
  }

  /** Write text or bytes to its standard input (spawned with `stdinOpen: true`); `close: true` then closes it. */
  write(data: string | Uint8Array, opts: { close?: boolean } = {}): Promise<void> {
    const body: JobStdin = typeof data === "string" ? { data } : { data_b64: toBase64(data) };
    if (opts.close) body.close = true;
    return this.sandbox.froog.writeJobStdin(this.sandbox.name, this.id, body);
  }

  /** Close its standard input: the command reads end of file. */
  closeStdin(): Promise<void> {
    return this.sandbox.froog.writeJobStdin(this.sandbox.name, this.id, { close: true });
  }
}
